Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e96ca22ef3 | ||
|
|
44afaa3133 | ||
|
|
ce46b5d97e | ||
|
|
ddb298f631 | ||
|
|
0a5ef0a61e |
@@ -13,6 +13,10 @@ You are the Lead Systems Architect specializing in Godot 4 and idiomatic GDScrip
|
|||||||
3. **Design:** Define API payload shapes, static GDScript type hints, node hierarchies, and signal flows.
|
3. **Design:** Define API payload shapes, static GDScript type hints, node hierarchies, and signal flows.
|
||||||
4. **Log:** Append trade-offs to `docs/architecture/tech_debt_and_optimizations.md`.
|
4. **Log:** Append trade-offs to `docs/architecture/tech_debt_and_optimizations.md`.
|
||||||
|
|
||||||
|
## Feature worktree
|
||||||
|
|
||||||
|
Before writing a feature document, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the user has not chosen yet, ask the orchestrator to get that preference before writing. Use the supplied path whether it is a worktree or the current checkout; do not create a worktree or switch branches yourself. Follow `AGENTS.md`.
|
||||||
|
|
||||||
### Shared Stage inspector UI
|
### Shared Stage inspector UI
|
||||||
For an in-stage selected-object inspector, first read `docs/specs/stage_panel_ui_system.md`
|
For an in-stage selected-object inspector, first read `docs/specs/stage_panel_ui_system.md`
|
||||||
and inspect `scripts/shared/stage_ui/` plus the `stage_panels` theme tokens. Use the existing
|
and inspect `scripts/shared/stage_ui/` plus the `stage_panels` theme tokens. Use the existing
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ Clothing currently has no standalone format. Author clothing as layered shapes i
|
|||||||
|
|
||||||
## Review workflow
|
## Review workflow
|
||||||
|
|
||||||
|
Before creating an asset review bundle, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it before writing. Use the selected path whether it is a worktree or the current checkout; do not create a worktree or switch branches yourself. Follow `AGENTS.md`.
|
||||||
|
|
||||||
Create every unapproved deliverable under `content/review/artist/<request-slug>/`. Use descriptive, versioned filenames and never overwrite an existing production asset unless the user explicitly requests replacement.
|
Create every unapproved deliverable under `content/review/artist/<request-slug>/`. Use descriptive, versioned filenames and never overwrite an existing production asset unless the user explicitly requests replacement.
|
||||||
|
|
||||||
Each review bundle must contain:
|
Each review bundle must contain:
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Wait for the user to answer before analyzing or proposing feature work.
|
|||||||
|
|
||||||
## Editing limits
|
## Editing limits
|
||||||
|
|
||||||
|
- **Worktree choice:** Before preparing an accepted phase brief or updating the backlog, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it before writing. Use the supplied path whether it is a worktree or the current checkout. Do not create a worktree, switch branches, or write into another checkout yourself. Follow `AGENTS.md`.
|
||||||
- You may edit only `docs/ideas/feature_backlog.md` and accepted phase briefs under `docs/plans/` during normal use. Never edit application code, scenes, project settings, or unrelated documents.
|
- You may edit only `docs/ideas/feature_backlog.md` and accepted phase briefs under `docs/plans/` during normal use. Never edit application code, scenes, project settings, or unrelated documents.
|
||||||
- Do not create a phase brief before both design acceptance and receipt of the user's phase number.
|
- Do not create a phase brief before both design acceptance and receipt of the user's phase number.
|
||||||
- Keep idea discussion conversational and concise. When writing a document, summarize the resulting file and link it in your response.
|
- Keep idea discussion conversational and concise. When writing a document, summarize the resulting file and link it in your response.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ You are a Godot 4 / GDScript 2.0 implementation developer. Adhere to coding stan
|
|||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
|
- **Worktree choice:** Before persistent edits, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it before editing. Use the supplied path whether it is a worktree or the current checkout. Do not create a worktree, switch branches, merge, or edit another checkout yourself. Follow `AGENTS.md` for the selected path.
|
||||||
- **Targeted Edits Only:** Do NOT output full unchanged files. Output only modified functions, concise diffs, or specific node configurations.
|
- **Targeted Edits Only:** Do NOT output full unchanged files. Output only modified functions, concise diffs, or specific node configurations.
|
||||||
- **Static Typing:** Enforce explicit type hints for all signatures and variables.
|
- **Static Typing:** Enforce explicit type hints for all signatures and variables.
|
||||||
- **Decoupling:** Follow "call down, signal up".
|
- **Decoupling:** Follow "call down, signal up".
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ model_reasoning_effort = "high"
|
|||||||
sandbox_mode = "read-only"
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = """
|
||||||
|
## Worktree choice
|
||||||
|
|
||||||
|
You are read-only and must not create worktrees or write project files. For any handoff that will create project files, confirm that the orchestrator has asked the user whether to use a worktree and supplied the selected project path and branch. Do not require a worktree when the user chooses the current checkout.
|
||||||
|
|
||||||
|
## 0. Editor Visual Style Guide
|
||||||
|
|
||||||
|
Before making, mocking, reviewing, or suggesting any Prop Editor or Stickman Editor UI/UX
|
||||||
|
change, read `docs/guides/editor_visual_style_guide.md`. Use its documented implemented
|
||||||
|
patterns and shared theme ownership as the starting point, then check relevant feature specs
|
||||||
|
and current controls for workflow-specific behavior. Clearly distinguish current behavior
|
||||||
|
from recommendations. If a proposal intentionally departs from the guide, explain why and
|
||||||
|
identify the departure in the proposal or spec. Follow explicit user direction when it differs.
|
||||||
|
|
||||||
## 1. Kid-Friendly UI Guardrails (10-Year-Old Persona)
|
## 1. Kid-Friendly UI Guardrails (10-Year-Old Persona)
|
||||||
|
|
||||||
* **Shared Stage Inspector UI:** Before proposing an in-stage selected-object inspector, read
|
* **Shared Stage Inspector UI:** Before proposing an in-stage selected-object inspector, read
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ You are a technical writer for Godot 4 / GDScript projects. You document game ar
|
|||||||
|
|
||||||
1. **Targeted Edits Only:** Touch only the specific markdown sections affected by recent changes. Never output full unchanged files.
|
1. **Targeted Edits Only:** Touch only the specific markdown sections affected by recent changes. Never output full unchanged files.
|
||||||
2. **Scannable & Technical:** Lead directly with code blocks, tables, and structured lists. Eliminate marketing fluff or generic introductions.
|
2. **Scannable & Technical:** Lead directly with code blocks, tables, and structured lists. Eliminate marketing fluff or generic introductions.
|
||||||
|
3. **Worktree Choice:** Before any persistent documentation work, confirm that the orchestrator has asked the user whether this task should use a Git worktree and supplied the selected project path and branch. If the preference is missing, ask the orchestrator to get it first. Use the selected path whether it is a worktree or the current checkout; do not create a worktree yourself.
|
||||||
"""
|
"""
|
||||||
@@ -7,6 +7,10 @@ description: "Create or revise Stickman Studio props, stickmen, embedded clothin
|
|||||||
|
|
||||||
Use the `artist` subagent for project graphical-asset and animation work. This skill manages delegation, review, approval, and handoff; it does not replace the Artist's visual judgment.
|
Use the `artist` subagent for project graphical-asset and animation work. This skill manages delegation, review, approval, and handoff; it does not replace the Artist's visual judgment.
|
||||||
|
|
||||||
|
## Choose a worktree before creating files
|
||||||
|
|
||||||
|
Before creating an asset review bundle or other project files, ask whether the user wants this task to use a Git worktree unless they have already stated a preference. For small, self-contained asset changes, offer the current checkout as the default. Wait for the answer before creating a worktree. Follow `AGENTS.md`, and pass Artist the absolute path and branch the user selected, whether that is a worktree or the current checkout. Do not require a worktree when the user declines one.
|
||||||
|
|
||||||
## Delegate to Artist
|
## Delegate to Artist
|
||||||
|
|
||||||
Spawn `artist` with `agent_type: "artist"`. Reuse the same Artist for revisions while it is available. Tell it that other agents may share the workspace and that it must preserve unrelated changes.
|
Spawn `artist` with `agent_type: "artist"`. Reuse the same Artist for revisions while it is available. Tell it that other agents may share the workspace and that it must preserve unrelated changes.
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ Treat the user's clear statement that they are ready to work on the discussed, s
|
|||||||
|
|
||||||
## Stage 3: Write and hand off the brief
|
## Stage 3: Write and hand off the brief
|
||||||
|
|
||||||
Once the user is ready and the phase number is known, have Designer create the accepted product/design brief at `docs/plans/phase_<number>_<feature_slug>.md`, following `docs/plans/README.md`. The brief should cover the goal and audience, desired child outcome, behavior/flow and feedback, story or directing value where relevant, success criteria, included and excluded scope, known risks/dependencies, project references, and remaining questions for the spec-pipeline. Keep architecture/API decisions for the architect's later technical specification in `docs/specs/`.
|
Once the user is ready and the phase number is known, ask whether the brief and later feature work should use a Git worktree unless the user has already stated a preference for this task. Follow the choice and the `Feature branches and worktrees` rules in `AGENTS.md`; do not create a worktree before the user answers. Use the selected checkout consistently for the brief, later specs, implementation, and verification. Pass its absolute path and current branch to Designer. Do not switch branches or write outside the selected checkout.
|
||||||
|
|
||||||
|
Have Designer create the accepted product/design brief at `docs/plans/phase_<number>_<feature_slug>.md`, following `docs/plans/README.md`. The brief should cover the goal and audience, desired child outcome, behavior/flow and feedback, story or directing value where relevant, success criteria, included and excluded scope, known risks/dependencies, project references, and remaining questions for the spec-pipeline. Keep architecture/API decisions for the architect's later technical specification in `docs/specs/`.
|
||||||
|
|
||||||
Do not overwrite an existing phase brief without the user's direction. Confirm the resulting file exists, update the matching backlog entry to `Planned` with a link to the brief, then tell the user where the plan is using a clickable local file link. Stop there; this skill creates the design handoff, not the technical spec or implementation.
|
Do not overwrite an existing phase brief without the user's direction. Confirm the resulting file exists, update the matching backlog entry to `Planned` with a link to the brief, then tell the user where the plan is using a clickable local file link. Stop there; this skill creates the design handoff, not the technical spec or implementation.
|
||||||
|
|||||||
@@ -5,8 +5,17 @@ description: "Stickman Studio feature workflow: UI/UX and architecture specs, im
|
|||||||
|
|
||||||
## Execution Logic
|
## Execution Logic
|
||||||
|
|
||||||
|
### Feature branch and worktree setup
|
||||||
|
|
||||||
|
Before creating a worktree for a task, ask the user whether they want one and wait for the answer. For small, self-contained fixes or documentation changes, offer the current checkout as the default; for larger feature phases, explain the benefit of a dedicated worktree. Do not create a worktree before the user chooses. Keep the user's choice for the task from the first persistent artifact through implementation and verification.
|
||||||
|
|
||||||
|
Follow the `Feature branches and worktrees` rules in `AGENTS.md`: inspect `git status --short`, the current branch, and worktree list before the first persistent change. If the user chooses the current checkout, stay on its branch. If they choose a worktree, reuse a matching one or create `.worktrees/<feature-slug>` on `feature/<feature-slug>` from the integration branch, including the phase number when known. Do not switch branches, transfer uncommitted changes without direction, or merge automatically.
|
||||||
|
|
||||||
|
Pass every delegated agent the absolute selected project path and its branch, whether that path is a worktree or the current checkout. Tell each agent to use it for all repository reads, writes, Godot runs, and visual checks. Do not stop file-writing work solely because the user chose not to use a worktree.
|
||||||
|
|
||||||
1. **Phase 0a: UI/UX Proposal & Alternative Ideas (User-Facing Features Only)**
|
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.
|
- `@uiux` analyzes the user's initial idea for cognitive friction, accessibility, and kid-friendliness.
|
||||||
|
- Before proposing, mocking, reviewing, or suggesting UI/UX changes, `@uiux` reads `docs/guides/editor_visual_style_guide.md` and checks the relevant implemented controls/specs. Treat the guide as the current visual baseline; distinguish implemented behavior from recommendations and explain intentional departures. This also applies to direct `@uiux` requests outside the pipeline.
|
||||||
- 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.
|
- 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.
|
||||||
- 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.
|
- 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.
|
- `@uiux` **STOP:** Waits for user feedback/selection before writing the UI spec.
|
||||||
@@ -15,6 +24,7 @@ description: "Stickman Studio feature workflow: UI/UX and architecture specs, im
|
|||||||
2. **Phase 0b: UI Spec Generation**
|
2. **Phase 0b: UI Spec Generation**
|
||||||
- User approves an option or provides feedback.
|
- User approves an option or provides feedback.
|
||||||
- `@uiux` outputs the UI/UX specification (control layouts, widget behaviors, visual feedback states, and cursor logic).
|
- `@uiux` outputs the UI/UX specification (control layouts, widget behaviors, visual feedback states, and cursor logic).
|
||||||
|
- Carry forward the style guide's applicable patterns and token ownership. If the approved direction departs from them, record the reason and the specific difference in the spec.
|
||||||
- For Stage inspector work, name which shared components and theme tokens the design uses or proposes to extend. Treat dialogs and other editor panels as separate workflows unless their interaction model fits the inspector kit.
|
- For Stage inspector work, name which shared components and theme tokens the design uses or proposes to extend. Treat dialogs and other editor panels as separate workflows unless their interaction model fits the inspector kit.
|
||||||
|
|
||||||
3. **Phase 1a: Architecture Proposal & Clarification**
|
3. **Phase 1a: Architecture Proposal & Clarification**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Godot 4+ specific ignores
|
# Godot 4+ specific ignores
|
||||||
.godot/
|
.godot/
|
||||||
|
.worktrees/
|
||||||
.images/
|
.images/
|
||||||
/android/
|
/android/
|
||||||
addons/
|
addons/
|
||||||
@@ -8,6 +9,7 @@ addons/
|
|||||||
*.import
|
*.import
|
||||||
# Local scratch captures, logs, and temporary browser profiles
|
# Local scratch captures, logs, and temporary browser profiles
|
||||||
/tmp/
|
/tmp/
|
||||||
|
/work/
|
||||||
content/review/artist/
|
content/review/artist/
|
||||||
content/examples/
|
content/examples/
|
||||||
/tests/godot_test_user_data/
|
/tests/godot_test_user_data/
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
- Headless tests + command-line scene playback exist (see "Command-line testing & debug playback" below); the GUI editor runs via `run/main_scene`.
|
- Headless tests + command-line scene playback exist (see "Command-line testing & debug playback" below); the GUI editor runs via `run/main_scene`.
|
||||||
- **Main scene:** `res://scenes/main.tscn` (set as `run/main_scene` in `project.godot`; hosts the Stickman Editor)
|
- **Main scene:** `res://scenes/main.tscn` (set as `run/main_scene` in `project.godot`; hosts the Stickman Editor)
|
||||||
|
|
||||||
|
## Feature branches and worktrees
|
||||||
|
|
||||||
|
- Before creating a Git worktree for a task, ask the user whether they want this task to use one. Do not create a worktree until they answer. For small, self-contained fixes and documentation changes, offer the current checkout as the default; for larger feature phases, explain why a worktree may help and let the user choose.
|
||||||
|
- Read-only discussion and proposals do not need a worktree. Before the first persistent change, inspect `git status --short`, `git branch --show-current`, and `git worktree list`.
|
||||||
|
- If the user chooses the current checkout, keep working on its current branch. Do not switch branches, stash, reset, clean, move, or overwrite unrelated work.
|
||||||
|
- If the user chooses a worktree, reuse a matching one when present. Otherwise, create `.worktrees/<feature-slug>` with branch `feature/<feature-slug>`, based on the integration branch (usually `main`). Include the phase number in the slug when known, such as `phase-20-stage-search`. Example: `git worktree add .worktrees/phase-20-stage-search -b feature/phase-20-stage-search main`. Confirm the path and branch with `git worktree list` and `git branch --show-current`.
|
||||||
|
- If relevant uncommitted changes need to be carried into a chosen worktree, ask how to handle them before proceeding.
|
||||||
|
- Pass every subagent the absolute path and branch the user chose, whether that is a worktree or the current checkout. All project reads, writes, Godot commands, and visual checks must target that path. Do not require a worktree when the user chose the current checkout.
|
||||||
|
- Do not merge feature branches automatically. Keep each feature branch available for review and merge only when the user asks or the active workflow explicitly authorizes it.
|
||||||
|
- `.worktrees/` is a local worktree container and must remain gitignored.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
- `scripts/` and `scenes/` stay as separate roots with matching feature groups. Editor controllers live in `scripts/editors/{stickman,prop,stage}`; stage, stickman runtime, props, terrain, and speech systems live in `scripts/{stage,characters/stickman,props,terrain,speech}`.
|
- `scripts/` and `scenes/` stay as separate roots with matching feature groups. Editor controllers live in `scripts/editors/{stickman,prop,stage}`; stage, stickman runtime, props, terrain, and speech systems live in `scripts/{stage,characters/stickman,props,terrain,speech}`.
|
||||||
@@ -78,6 +89,10 @@ the configured `run/main_scene`.
|
|||||||
custom UI/UX agent defined in `.codex/agents/uiux.toml`. Treat that prefix as an explicit
|
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
|
delegation request. The UI/UX agent is read-only; carry its design guidance into implementation
|
||||||
separately when implementation is requested.
|
separately when implementation is requested.
|
||||||
|
- Before making, mocking, reviewing, or suggesting Prop or Stickman UI/UX changes, have `@uiux`
|
||||||
|
read `docs/guides/editor_visual_style_guide.md` and consult the relevant implemented patterns
|
||||||
|
and feature specs. Treat the guide as the current visual baseline; call out intentional
|
||||||
|
departures and distinguish current behavior from recommendations.
|
||||||
- For new or revised child-facing dialogs, popups, and short setup flows, follow the
|
- 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
|
[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
|
and controls, clear borders, blue primary actions with bold white labels, comfortable button
|
||||||
|
|||||||
@@ -0,0 +1,857 @@
|
|||||||
|
{
|
||||||
|
"version": "1.7",
|
||||||
|
"scene_name": "Plain Background",
|
||||||
|
"sky_id": "sky_starry_night",
|
||||||
|
"background_kind": "sky",
|
||||||
|
"plain_color": "#F5EFE0",
|
||||||
|
"camera": {
|
||||||
|
"position": {
|
||||||
|
"x": -43.8016738891602,
|
||||||
|
"y": -691.851379394531
|
||||||
|
},
|
||||||
|
"zoom": 0.605000019073486
|
||||||
|
},
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"file_id": "background_ground_1",
|
||||||
|
"type": "background_ground",
|
||||||
|
"position": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 672.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"size": {
|
||||||
|
"x": 6592.0,
|
||||||
|
"y": 192.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_background_grass",
|
||||||
|
"color": "#72a84cff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "scenery_artwork_1",
|
||||||
|
"type": "scenery_artwork",
|
||||||
|
"position": {
|
||||||
|
"x": -1472.0,
|
||||||
|
"y": -320.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "res://content/props/art_storefront-v1.prop",
|
||||||
|
"display_name": "Sunny Storefront",
|
||||||
|
"depth": "near",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"last_known_local_bounds": {
|
||||||
|
"position": {
|
||||||
|
"x": -871.896057128906,
|
||||||
|
"y": -826.405822753906
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"x": 1743.79211425781,
|
||||||
|
"y": 1652.81164550781
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "scenery_artwork_2",
|
||||||
|
"type": "scenery_artwork",
|
||||||
|
"position": {
|
||||||
|
"x": -321.925048828125,
|
||||||
|
"y": -864.47802734375
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "res://content/props/art_town_house-v1.prop",
|
||||||
|
"display_name": "Town House",
|
||||||
|
"depth": "near",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"last_known_local_bounds": {
|
||||||
|
"position": {
|
||||||
|
"x": -958.074951171875,
|
||||||
|
"y": -1375.52197265625
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"x": 1916.14990234375,
|
||||||
|
"y": 2751.0439453125
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "scenery_artwork_3",
|
||||||
|
"type": "scenery_artwork",
|
||||||
|
"position": {
|
||||||
|
"x": 1408.0,
|
||||||
|
"y": -320.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "res://content/props/art_storefront-v1.prop",
|
||||||
|
"display_name": "Sunny Storefront",
|
||||||
|
"depth": "near",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"last_known_local_bounds": {
|
||||||
|
"position": {
|
||||||
|
"x": -871.896057128906,
|
||||||
|
"y": -826.405822753906
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"x": 1743.79211425781,
|
||||||
|
"y": 1652.81164550781
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "scenery_artwork_4",
|
||||||
|
"type": "scenery_artwork",
|
||||||
|
"position": {
|
||||||
|
"x": -2368.0,
|
||||||
|
"y": -256.0
|
||||||
|
},
|
||||||
|
"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": -939.5,
|
||||||
|
"y": -289.301696777344
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"x": 1879.0,
|
||||||
|
"y": 578.603393554688
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "scenery_artwork_5",
|
||||||
|
"type": "scenery_artwork",
|
||||||
|
"position": {
|
||||||
|
"x": 704.0,
|
||||||
|
"y": -256.0
|
||||||
|
},
|
||||||
|
"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": -939.5,
|
||||||
|
"y": -289.301696777344
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"x": 1879.0,
|
||||||
|
"y": 578.603393554688
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "scenery_artwork_6",
|
||||||
|
"type": "scenery_artwork",
|
||||||
|
"position": {
|
||||||
|
"x": 2304.0,
|
||||||
|
"y": -256.0
|
||||||
|
},
|
||||||
|
"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": -939.5,
|
||||||
|
"y": -289.301696777344
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"x": 1879.0,
|
||||||
|
"y": 578.603393554688
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "background_ground_2",
|
||||||
|
"type": "background_ground",
|
||||||
|
"position": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 352.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"size": {
|
||||||
|
"x": 6848.0,
|
||||||
|
"y": 832.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_background_grass",
|
||||||
|
"color": "#72a84cff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_1",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -3264.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_2",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -3072.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_3",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -2880.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_4",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -2688.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_5",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -2496.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_6",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -2304.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_7",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -2112.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_8",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -1920.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_9",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -1728.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_10",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -1536.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_11",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -1344.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_12",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -1152.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_13",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -960.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_14",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -768.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_15",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -576.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_16",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -384.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_17",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": -192.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_18",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_19",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 192.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_20",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 384.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_21",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 576.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_22",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 768.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_23",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 960.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_24",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 1152.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_25",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 1344.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_26",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 1536.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_27",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 1728.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_28",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 1920.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_29",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 2112.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_30",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 2304.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_31",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 2496.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_32",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 2688.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_33",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 2880.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_34",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 3072.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_35",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 3264.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "terrain_36",
|
||||||
|
"type": "terrain",
|
||||||
|
"position": {
|
||||||
|
"x": 3456.0,
|
||||||
|
"y": 768.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"asset_id": "ground",
|
||||||
|
"scale": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_ground_sidewalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "background_ground_3",
|
||||||
|
"type": "background_ground",
|
||||||
|
"position": {
|
||||||
|
"x": 160.0,
|
||||||
|
"y": 864.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"size": {
|
||||||
|
"x": 6912.0,
|
||||||
|
"y": 192.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_background_concrete",
|
||||||
|
"color": "#a6aaa3ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "background_ground_4",
|
||||||
|
"type": "background_ground",
|
||||||
|
"position": {
|
||||||
|
"x": -2896.0,
|
||||||
|
"y": 848.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"size": {
|
||||||
|
"x": 800.0,
|
||||||
|
"y": 160.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_background_concrete",
|
||||||
|
"color": "#a6aaa3ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "background_ground_5",
|
||||||
|
"type": "background_ground",
|
||||||
|
"position": {
|
||||||
|
"x": -3264.0,
|
||||||
|
"y": 864.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"size": {
|
||||||
|
"x": 320.0,
|
||||||
|
"y": 192.0
|
||||||
|
},
|
||||||
|
"finish_id": "finish_background_concrete",
|
||||||
|
"color": "#a6aaa3ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "stickman_1",
|
||||||
|
"type": "stickman",
|
||||||
|
"position": {
|
||||||
|
"x": -2784.0,
|
||||||
|
"y": 351.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"stk_path": "res://content/stickmen/break.stk",
|
||||||
|
"facing_profile": "FORWARD",
|
||||||
|
"action_queue": [
|
||||||
|
{
|
||||||
|
"type": "move_to",
|
||||||
|
"target": {
|
||||||
|
"x": 2027.27270507813,
|
||||||
|
"y": 683.355224609375
|
||||||
|
},
|
||||||
|
"style": "walk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "stickman_2",
|
||||||
|
"type": "stickman",
|
||||||
|
"position": {
|
||||||
|
"x": -672.0,
|
||||||
|
"y": 352.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"stk_path": "res://content/stickmen/break.stk",
|
||||||
|
"facing_profile": "FORWARD",
|
||||||
|
"action_queue": [
|
||||||
|
{
|
||||||
|
"type": "move_to",
|
||||||
|
"target": {
|
||||||
|
"x": -3161.15698242188,
|
||||||
|
"y": 701.537109375
|
||||||
|
},
|
||||||
|
"style": "walk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_id": "stickman_3",
|
||||||
|
"type": "stickman",
|
||||||
|
"position": {
|
||||||
|
"x": 3392.0,
|
||||||
|
"y": 351.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"stk_path": "res://content/stickmen/break.stk",
|
||||||
|
"facing_profile": "FORWARD",
|
||||||
|
"action_queue": [
|
||||||
|
{
|
||||||
|
"type": "move_to",
|
||||||
|
"target": {
|
||||||
|
"x": -3333.0576171875,
|
||||||
|
"y": 726.330444335938
|
||||||
|
},
|
||||||
|
"style": "run"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event_rules": [],
|
||||||
|
"metadata": {
|
||||||
|
"created_unix": 1792022400,
|
||||||
|
"modified_unix": 1792732500.154,
|
||||||
|
"autosave": false,
|
||||||
|
"source_path": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,9 @@ This document tracks known technical debt, optimization opportunities, and minor
|
|||||||
| 76 | **Camera-aware Sky redraws vector motifs while framing changes** — the former static Sky drew motifs only when the preset or viewport changed. | Low | Planned | The Phase 19 Sky addition keeps one screen-space `StageSkyView`, redraws only when a bound camera's effective framing changes, and caches reusable cloud/moon curve samples where practical. Measure sustained pan/zoom cost in a rendered Stage before adding a raster or shader path. The fixed reference frame and bounded response can saturate at distant camera positions; that is an accepted composition trade-off, not scene state to persist. See `docs/specs/phase_19_scenery_backgrounds_surface_finishes.md#phase-19-addition-camera-aware-sky-details`. (2026-10-21) |
|
| 76 | **Camera-aware Sky redraws vector motifs while framing changes** — the former static Sky drew motifs only when the preset or viewport changed. | Low | Planned | The Phase 19 Sky addition keeps one screen-space `StageSkyView`, redraws only when a bound camera's effective framing changes, and caches reusable cloud/moon curve samples where practical. Measure sustained pan/zoom cost in a rendered Stage before adding a raster or shader path. The fixed reference frame and bounded response can saturate at distant camera positions; that is an accepted composition trade-off, not scene state to persist. See `docs/specs/phase_19_scenery_backgrounds_surface_finishes.md#phase-19-addition-camera-aware-sky-details`. (2026-10-21) |
|
||||||
| 77 | **Starter surfaces guarantee a finite pan envelope, not arbitrary camera framing** — the two bundled sets use a fixed authored width to keep their scene data simple. | Low | Implemented; visual review pending | The authored envelope is a 1,280-logical-pixel viewport at zoom 0.5, ±1,000 world units of travel, and 100 units of margin per side. User review prompted an opaque Town Grass Background ground and separate scale-1 tile rows. Twenty-five 192-unit tiles cover the 4,760-unit minimum; Asphalt has 27 tiles (5,184 units) so the road extends one tile beyond the sidewalk at both ends. Navigation merges contiguous tile tops and routes the 32-unit curb step at the exposed ends without merging Ground objects or colliders. The focused suite passes 515 assertions, including the curb step and PLAY landing. The camera remains unrestricted, so wider viewports, farther pans, and zoomed-out shots can reveal finite ends. Rendered seam review remains pending. See docs/specs/phase_19_starter_scene_composition.md. (2026-10-21)
|
| 77 | **Starter surfaces guarantee a finite pan envelope, not arbitrary camera framing** — the two bundled sets use a fixed authored width to keep their scene data simple. | Low | Implemented; visual review pending | The authored envelope is a 1,280-logical-pixel viewport at zoom 0.5, ±1,000 world units of travel, and 100 units of margin per side. User review prompted an opaque Town Grass Background ground and separate scale-1 tile rows. Twenty-five 192-unit tiles cover the 4,760-unit minimum; Asphalt has 27 tiles (5,184 units) so the road extends one tile beyond the sidewalk at both ends. Navigation merges contiguous tile tops and routes the 32-unit curb step at the exposed ends without merging Ground objects or colliders. The focused suite passes 515 assertions, including the curb step and PLAY landing. The camera remains unrestricted, so wider viewports, farther pans, and zoomed-out shots can reveal finite ends. Rendered seam review remains pending. See docs/specs/phase_19_starter_scene_composition.md. (2026-10-21)
|
||||||
| 78 | **Plain Background adds a second backdrop kind while Sky remains a separate renderer** — live Stage, starter previews, and chooser cards must agree on kind and remembered color. | Medium | Spec prepared; implementation pending | Phase 19 composes one shared `StageBackgroundView` from the existing Sky view and an opaque fill. Scene v1.7 stores active kind, last Sky ID, and confirmed plain color; all older scenes migrate as Sky. `SandboxStage` owns the transient custom-picker draft, which must never enter Save or autosave. The existing chooser's four-Sky layout and 42 px compact swatches need targeted extension, and grid/gizmo contrast needs theme-backed treatment for light and dark fills. Rendered pan/zoom/resize, legacy migration, and every draft-cancel exit remain verification gates. See `docs/specs/phase_19_plain_background.md`. (2026-10-22) |
|
| 78 | **Plain Background adds a second backdrop kind while Sky remains a separate renderer** — live Stage, starter previews, and chooser cards must agree on kind and remembered color. | Medium | Spec prepared; implementation pending | Phase 19 composes one shared `StageBackgroundView` from the existing Sky view and an opaque fill. Scene v1.7 stores active kind, last Sky ID, and confirmed plain color; all older scenes migrate as Sky. `SandboxStage` owns the transient custom-picker draft, which must never enter Save or autosave. The existing chooser's four-Sky layout and 42 px compact swatches need targeted extension, and grid/gizmo contrast needs theme-backed treatment for light and dark fills. Rendered pan/zoom/resize, legacy migration, and every draft-cancel exit remain verification gates. See `docs/specs/phase_19_plain_background.md`. (2026-10-22) |
|
||||||
|
| 79 | **Editor chrome has two existing token sources and scene-level literals** — PropTheme owns current Prop UI values while Stickman scenes hardcode bars, labels, and borders. | Medium | Spec prepared; implementation pending | Add one validated `EditorVisualTheme` source for shared semantic chrome and editor-local `Theme` instances. Retain PropTheme for prop material/primitive/canvas values and keep Stickman canvas drawing tokens in a separate namespace. Migrate legacy Prop `ui` accessors deliberately so a chrome role has one final authority; use SandboxTheme values as a visual reference without a runtime dependency or Stage inspector component reuse. See `docs/specs/editor_visual_harmonization.md`. (2026-10-23) |
|
||||||
|
| 80 | **Stickman panel nodes currently own both document shapes and local view state** — rebuilding a panel for a focused layout would reset selection, zoom, and pan and risk save/preview drift. | Medium | Spec prepared; implementation pending | Move the same ten BodyPartPanel instances between Overview and Focus hosts, retain one permanent WholeStickmanPreview sibling, and defer switching while a drag or popup is active. The controller owns session-only view choice and active part; manually chosen views survive resize, and narrow Overview remains scrollable. A future document-model extraction is warranted only if new editor features make live-node ownership limiting. See `docs/specs/editor_visual_harmonization.md`. (2026-10-23) |
|
||||||
|
| 81 | **Stickman authored transforms lack a complete dirty-state boundary** — preview translation emits `part_moved`, while rotation and scale currently have no commit signal; Save/Load/Clear do not track unsaved work. | Medium | Spec prepared; implementation pending | Add a controller-observable transform commit path for position, rotation, and scale, plus `_dirty`/`_current_path` and Save/Discard Changes/Cancel transactions for replacement and close. Keep Overview/Focus, guide, grid, selection, and zoom/pan as session UI state that never marks dirty. Preserve the `.stk` format. See `docs/specs/editor_visual_harmonization.md`. (2026-10-23) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
# Editor Visual Style Guide
|
||||||
|
|
||||||
|
**Scope:** Establish a practical visual and interaction direction for the Prop Editor and Stickman Editor, using the Sandbox Stage UI as a product reference.
|
||||||
|
**Status:** Guidance for current and future editor UI. Prop and Stickman share `EditorVisualTheme` for editor chrome and selected reusable controls; Stage-only layouts and components remain Stage-specific unless explicitly generalized.
|
||||||
|
|
||||||
|
## 1. Product direction
|
||||||
|
|
||||||
|
Both editors should feel like approachable creative workspaces in the same Stickman Studio product:
|
||||||
|
|
||||||
|
- Keep the canvas or preview as the main work surface. Put tools and settings around it, not on top of the artwork unless a temporary task requires an overlay.
|
||||||
|
- Make the current task and mode easy to recognize. Use direct labels such as **Arrange**, **Reshape**, **Attach**, **Save**, and **Cancel**.
|
||||||
|
- Prefer visible feedback on the object over abstract state: selection outlines, vertex handles, previews, mode coloring, and status text.
|
||||||
|
- Preserve a stable layout while the child works. Opening an inspector section, showing help, or changing a mode should not make the active control jump away from the pointer.
|
||||||
|
- Make recovery and destructive actions predictable. Show what is about to happen and keep a visible way to cancel.
|
||||||
|
- Share a visual language, not necessarily identical panel layouts. A vector canvas, a modal dialog, an in-stage inspector card, and a stickman-part panel have different jobs.
|
||||||
|
|
||||||
|
The Sandbox Stage establishes a useful baseline for hierarchy, contrast, child-facing language, control states, safe viewport behavior, and theme tokens. Its in-stage inspector and capture bar are Stage patterns; they should be adapted only when an editor workflow has the same interaction needs.
|
||||||
|
|
||||||
|
## 2. Current implementation map
|
||||||
|
|
||||||
|
| Surface | Current implementation | Reuse implication |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Sandbox Stage | SandboxTheme loads validated tokens from [sandbox_theme.json](../../sandbox_theme.json). Stage inspector controls live under [scripts/shared/stage_ui](../../scripts/shared/stage_ui/). | Use as the product reference for semantic colors, readable type, focus, action roles, and reusable theme access. |
|
||||||
|
| Stage inspector | The approved [Stage panel UI system](../specs/stage_panel_ui_system.md) targets in-stage selected-object cards. | Do not insert StageInspectorPanel into editor inspectors by default. Borrow fitting rules and state behavior; use its components only if the layout and ownership match. |
|
||||||
|
| Stage and editor dialogs | [StageDialogStyler](../../scripts/shared/stage_ui/stage_dialog_styler.gd) draws the shared confirmation card over the active scene while retaining native dialog signals. The style reads `popups.confirmation` from SandboxTheme. | Use this shared card for Prop and Stickman confirmation, recovery, and short action dialogs. Native file picker content remains native. |
|
||||||
|
| Shared editor chrome | [EditorVisualTheme](../../scripts/shared/editor_ui/editor_visual_theme.gd) loads validated tokens from [editor_visual_theme.json](../../editor_visual_theme.json) and builds each editor's local control Theme. `EditorSelectionDropdown` and `EditorContextMenu` supply shared selection and canvas-menu styling. | Use shared semantic tokens and controls for matching editor workflows. Keep editor-specific document behavior in its owner. |
|
||||||
|
| Prop Editor | [PropTheme](../../scripts/editors/prop/prop_theme.gd) reads [prop_theme.json](../../prop_theme.json) for material, geometry, and canvas defaults; editor chrome comes from `EditorVisualTheme`. The main scene uses split containers, a scrollable inspector, modes, and a nested Attach preview. | Keep Prop artwork/material values and canvas contrast in `PropTheme`; use shared editor tokens for chrome, inspector states, and selection dropdown styling. |
|
||||||
|
| Stickman Editor | [stickman_editor.gd](../../scripts/editors/stickman/stickman_editor.gd) uses `EditorVisualTheme` for chrome and coordinates ten BodyPartPanels in a two-column Overview beside the Whole Stickman preview, plus a Focus view for one large panel. | Overview columns and panels expand to the workspace size, retaining a 230 × 160 px minimum and scrolling when space is short. Keep the splitter draggable, preserve panel state, and show the part title inside its panel in both views. |
|
||||||
|
| Child-facing dialogs | [Dialog Design Principles](../specs/dialog_design_principles.md) documents the rounded, labeled-action pattern and stable resizing behavior. | Apply these rules to editor dialogs, adapting ownership and host behavior to each editor. |
|
||||||
|
|
||||||
|
The Prop Editor user guide at [prop_editor_user_guide.md](prop_editor_user_guide.md) describes its current controls and layout. Use it to avoid documenting proposed changes as existing behavior.
|
||||||
|
|
||||||
|
## 3. Shared visual system
|
||||||
|
|
||||||
|
### 3.1 Semantic color roles
|
||||||
|
|
||||||
|
Use color to reinforce meaning consistently. Keep labels, shapes, icons, or state text alongside color so meaning survives grayscale, low contrast, and color-vision differences.
|
||||||
|
|
||||||
|
| Role | Sandbox Stage reference | Editor guidance |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Main background | global.bg_color #171B24F2 | Use a dark neutral around the work area; avoid competing with authored art. |
|
||||||
|
| Raised surface | global.surface_color #252B38FA | Use for toolbars, inspector shells, and cards. Distinguish adjacent surfaces by value, border, or spacing. |
|
||||||
|
| Primary text | global.text_color #F5F7FA | Use for titles, control names, and important values. |
|
||||||
|
| Secondary text | global.muted_text_color #AAB2C0 | Use for helper text and secondary status; keep readable at the chosen size. |
|
||||||
|
| EDIT accent | mode_colors.edit #22C6FF | Shared editor accent for active modes, selection accents, and editor guidance through `EditorVisualTheme`. |
|
||||||
|
| DIRECT accent | mode_colors.direct #FFB300 | Reserved for the Stage's directing mode. Do not use as a generic warning color in editors. |
|
||||||
|
| PLAY accent | mode_colors.play #33DD77 | Reserved for Stage playback state. |
|
||||||
|
| Primary action | Stage dialog/action blue #087DBA | Use for the single most important forward action in a dialog. |
|
||||||
|
| Destructive action | Stage danger #E66B75 with dark red surfaces | Use only for destructive actions; pair with explicit wording and a cancel path. |
|
||||||
|
| Focus | Stage dialog focus #8BD9FF | Give keyboard focus an outline that remains visible over selected, hovered, and dark states. |
|
||||||
|
| Canvas grid | Stage tokens vary with canvas treatment; Prop canvas currently uses #FFFFFF26. | Keep grid lines subtle. The work, selection, and handles must dominate the grid. |
|
||||||
|
|
||||||
|
These values are current Stage reference values, not a mandate to paste literal hex values into editor controls. Resolve colors through validated theme accessors. If an editor needs a warmer or quieter local accent, define a semantic editor token and document its mapping to Stage meaning.
|
||||||
|
|
||||||
|
### 3.2 Shared chrome and intentional editor differences
|
||||||
|
|
||||||
|
Both standalone editors now use `EditorVisualTheme` for shared chrome roles, sizing, and local control Themes. Keep these domain-specific values in their existing owner:
|
||||||
|
|
||||||
|
- `PropTheme` retains material tints, primitive defaults, outline widths, and the Prop canvas surface/grid. Keep its neutral charcoal canvas distinct from the blue-tinted editor panels. Stickman canvas grid and selection colors remain in `EditorVisualTheme.stickman_canvas` tokens.
|
||||||
|
- `EditorVisualTheme` owns common editor surfaces, text, EDIT accent, focus, action roles, menu sizes, and minimum targets. `SandboxTheme` continues to own Stage-specific panels and dialog tokens.
|
||||||
|
|
||||||
|
Preserve these boundaries when extending the UI: share semantic chrome roles where workflows match, and keep canvas, material, and runtime-domain colors local to their owner.
|
||||||
|
|
||||||
|
### 3.3 Type, spacing, and shape
|
||||||
|
|
||||||
|
Use these Stage examples as starting points, then tune them for the denser editor workspace:
|
||||||
|
|
||||||
|
- Stage inspector hierarchy: title **24 px**, section **20 px**, body **20 px**, helper **16 px**.
|
||||||
|
- Stage inspector shell: **12 px** padding, **8 px** header gap, **12 px** section gap, **10 px** corner radius.
|
||||||
|
- Stage choice row: **56 px** minimum height; Stage toggle row: **64 px** minimum, increasing when helper text is visible.
|
||||||
|
- Large child-facing dialogs: title about **28–30 px**, body about **20 px**, button about **20 px**, **2 px** borders, **12–18 px** corner radii, and generous footer spacing.
|
||||||
|
- Stage generally uses dark surfaces with a restrained border. The dialog guide's stronger borders and rounder controls are for modal emphasis, not a requirement to put every editor widget inside a thick outline.
|
||||||
|
|
||||||
|
For compact inspectors, shorten the hierarchy only after checking it at the intended window size. Prefer a clear label and value over shrinking type until it becomes difficult to read. Use container spacing and alignment rather than per-control position nudges.
|
||||||
|
|
||||||
|
### 3.4 State styling contract
|
||||||
|
|
||||||
|
Every interactive control should have distinct, inspectable states:
|
||||||
|
|
||||||
|
| State | Expected feedback |
|
||||||
|
| --- | --- |
|
||||||
|
| Neutral | Stable surface and legible label; no implied selection. |
|
||||||
|
| Hover | Small surface or border change. Must not resemble selected or focused. |
|
||||||
|
| Selected / active | Persistent fill, border, pressed state, or explicit status. Keep the label unchanged when possible. |
|
||||||
|
| Keyboard focus | High-contrast focus ring or border that is visible even over selected fill. |
|
||||||
|
| Disabled | Control cannot activate, remains readable, and explains why when the reason is not obvious. |
|
||||||
|
| Dragging / live preview | Immediate object feedback; indicate the active handle or preview and make cancellation discoverable. |
|
||||||
|
| Error / invalid | Explain the problem near the affected task and offer a concrete next step. Do not rely on red alone. |
|
||||||
|
| Saved / dirty | Show a concise, persistent status such as **Saved** / **Unsaved changes** where it helps prevent lost work. |
|
||||||
|
|
||||||
|
For drawing handles, keep a contrasting backing/halo where the canvas can contain arbitrary colors. The Prop canvas's two-tone selection halo and the Stage gizmo backing are useful precedents. Selection should be readable at all zoom levels; keep handle sizes in screen space when scaling the canvas.
|
||||||
|
|
||||||
|
## 4. Layout and reuse rules
|
||||||
|
|
||||||
|
### 4.1 Use the right pattern for the job
|
||||||
|
|
||||||
|
- **In-stage selected-object inspector:** follow StageInspectorPanel, choice groups, and toggle rows in the [Stage UI spec](../specs/stage_panel_ui_system.md). Its title/Close header, fixed header with scrolling body, and safe-rect fitting are specific to a floating selection card.
|
||||||
|
- **Editor inspector:** favor a persistent column beside the canvas, with labeled groups and vertical scrolling. The Prop Editor already uses a split container and scrollable inspector. Separate sections with rounded cards, readable headings, colorful section icons, and concise helper copy; keep related values grouped in compact tiles. Reuse Stage's hierarchy and state clarity, but do not force the floating Stage card structure into this column.
|
||||||
|
- **Temporary targeting step:** the Stage capture bar is for a short-lived click-on-stage task. An editor can borrow the prompt-plus-visible-Cancel contract when it has a similar temporary canvas capture step, but should not copy the bar just because it is visible in Stage.
|
||||||
|
- **Modal dialog:** follow [Dialog Design Principles](../specs/dialog_design_principles.md). Keep a labeled **Cancel**, **Done**, or **Back** action; use one visually clear primary action; preserve the clicked control's anchor when content grows; cap height and scroll content on short viewports.
|
||||||
|
- **Canvas tools:** keep mode and document controls outside the draw area where possible. Put contextual handles and temporary prompts near the object only if they do not obscure the task.
|
||||||
|
|
||||||
|
### 4.2 Reuse through tokens and small components
|
||||||
|
|
||||||
|
1. Put repeated colors, font sizes, minimum sizes, spacing, radii, border widths, and state colors in a validated theme source.
|
||||||
|
2. Use typed accessors with sensible fallbacks. Avoid repeating raw values across script-built controls and .tscn files.
|
||||||
|
3. Apply per-control StyleBox overrides. Do not mutate a cached/shared Theme to style one editor or one accent variant.
|
||||||
|
4. Extract a reusable control only after the behavior or presentation appears in at least two appropriate contexts. Keep domain rules and document mutations in the editor controller.
|
||||||
|
5. Prefer native Button, ButtonGroup, CheckBox, OptionButton, and LineEdit semantics. Custom-drawn controls should separate visual drawing from input and expose a clear typed API.
|
||||||
|
6. Keep canvas-specific rendering tokens separate from chrome tokens. Grid, artwork, shape fill, selection halo, vertex handles, and preview ghosts have different contrast jobs.
|
||||||
|
7. Do not serialize UI state unless it is part of the authored document. Tool preference, document content, preview-only state, and persistence should be deliberately separated.
|
||||||
|
|
||||||
|
Both standalone editors build their chrome Theme from `EditorVisualTheme`; Prop also uses `PropTheme` for prop-specific art and material defaults. Stage remains the reference for fitting, dialog hierarchy, and child-facing language, but its in-stage inspector layout is not a default editor-inspector component.
|
||||||
|
|
||||||
|
### 4.3 Responsive workspace behavior
|
||||||
|
|
||||||
|
- Preserve canvas priority. When space is constrained, inspector content scrolls before drawing tools or essential canvas controls are clipped.
|
||||||
|
- Keep splitters draggable and give both sides a useful minimum. Prop Editor has the outer canvas/inspector split and an Attach preview split; these are good workspace patterns to retain.
|
||||||
|
- Use container-driven rows (FlowContainer, HBoxContainer, VBoxContainer) instead of fixed offsets for control groups that can wrap or grow with theme fonts.
|
||||||
|
- Keep top actions and mode controls reachable at smaller window sizes. Wrap or stack controls only when necessary, and preserve their order and mode meaning.
|
||||||
|
- Keep toolbars, status bars, and dialogs inside safe margins. A dynamic dialog should grow from a stable anchor and scroll rather than jump or recenter after each choice.
|
||||||
|
- Stickman Overview uses two equal, flexible columns of five body-part drawing panels beside the Whole Stickman preview. Let both columns fill the Overview pane and share extra width; let the five rows share extra height while preserving a 230 × 160 px minimum panel size. Scroll the Overview when the viewport is shorter than its minimum content size.
|
||||||
|
- Keep the Overview/preview splitter draggable. On narrow windows, use the existing Focus view to give one body-part panel the available space; keep Whole Stickman access available through its dedicated control. Switching views must preserve each panel instance, authored geometry, selection, and view transform. In Focus, use the title inside the active panel as the only canvas heading; do not repeat it above the panel.
|
||||||
|
- Validate the layout at wide and narrow windows, with raised fonts and resized panels, before claiming responsive behavior.
|
||||||
|
|
||||||
|
### 4.4 Shared confirmation dialog style
|
||||||
|
|
||||||
|
Use the Sandbox Stage confirmation card for child-facing confirmation, recovery, and short action dialogs in both standalone editors. The shared implementation is `StageDialogStyler`, with color, sizing, and spacing values from `SandboxTheme.popups.confirmation`.
|
||||||
|
|
||||||
|
- Draw a dim backdrop and a centered, rounded dark card. Use the themed card width (560 px target), 22 px inset, 18 px corners, and 2 px border; fit the card inside the safe viewport.
|
||||||
|
- Put a contextual icon badge and a left-aligned 28 px title in the header. Put the message in a left-aligned 20 px body. Wrap longer copy and scroll the body if the viewport is short.
|
||||||
|
- Order actions as primary, destructive, then **Cancel**. Use Stage blue for Save, Restore, and other forward actions; use the danger treatment for Discard, Start Over, and other destructive choices; use the neutral surface for Cancel. Make action labels at least 52 px high and 112 px wide, with 12 px gaps.
|
||||||
|
- Keep the visible **Cancel** action even when Escape is supported. Do not show a native title-bar **×**. Focus the primary action when present; for a destructive-only prompt, focus Cancel.
|
||||||
|
- Keep the native `AcceptDialog` or `ConfirmationDialog` as the signal source and open its shared overlay through `StageDialogStyler`. This keeps confirmation, custom-action, cancellation, keyboard, and safe-viewport behavior consistent.
|
||||||
|
- Apply this pattern to Prop recovery, unsaved changes before New/Close/Stage return, off-origin save choices, anchor rename, and error acknowledgement; apply it to Stickman Start Over, unsaved changes, grid setup, and error acknowledgement. File pickers keep their native file-selection interface.
|
||||||
|
|
||||||
|
Treat the card as one shared product pattern. Do not create editor-local confirmation palettes or button-role variants.
|
||||||
|
|
||||||
|
## 5. Editor flows and state maps
|
||||||
|
|
||||||
|
The following maps describe the current interaction architecture at a level useful for future visual work. They are not proposed additions to file formats.
|
||||||
|
|
||||||
|
### 5.1 Prop Editor flow
|
||||||
|
|
||||||
|
Current primary flow, based on [prop_editor.gd](../../scripts/editors/prop/prop_editor.gd) and [prop_canvas.gd](../../scripts/editors/prop/prop_canvas.gd):
|
||||||
|
|
||||||
|
~~~mermaid
|
||||||
|
flowchart TD
|
||||||
|
Open[Open Prop Editor] --> Recovery{Recover autosave?}
|
||||||
|
Recovery -->|Restore| Dirty[Restore document as dirty]
|
||||||
|
Recovery -->|Discard / none| New[New or loaded document]
|
||||||
|
Dirty --> Arrange[Arrange shapes]
|
||||||
|
New --> Arrange
|
||||||
|
Arrange --> Reshape[Reshape vertices]
|
||||||
|
Arrange --> Attach[Attach anchors and preview]
|
||||||
|
Reshape --> Arrange
|
||||||
|
Attach --> Arrange
|
||||||
|
Attach --> Reshape
|
||||||
|
Arrange --> Save[Save / Save As]
|
||||||
|
Reshape --> Save
|
||||||
|
Attach --> Save
|
||||||
|
Save --> Clean[Saved status; autosave cleared]
|
||||||
|
Arrange --> Exit{New / close with changes?}
|
||||||
|
Reshape --> Exit
|
||||||
|
Attach --> Exit
|
||||||
|
Exit -->|Save| Save
|
||||||
|
Exit -->|Discard| Close[Close or start new]
|
||||||
|
Exit -->|Cancel| CurrentMode[Return to current mode]
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Presentation rules for this flow:
|
||||||
|
|
||||||
|
- Arrange, Reshape, and Attach are persistent tool modes. Keep the active mode visually selected and keep mode labels stable.
|
||||||
|
- Mode changes may change available controls and inspector sections. Explain missing controls through a short label or helper only when the reason is not obvious from the mode.
|
||||||
|
- Shape/anchor edits change the document and should visibly change dirty status. Preview motion, visibility toggles, and other session-only controls should not imply the document was edited.
|
||||||
|
- Saving should confirm completion in the status area or title state. Errors should identify the failed file action and retain the child's work.
|
||||||
|
- New and close with unsaved changes offer explicit **Save**, **Discard**, and **Cancel** outcomes. For actions that could replace work, name the consequence in the prompt.
|
||||||
|
- In Attach mode, show the selected anchor and its editable handles clearly on the canvas, and keep the private stickman/prop preview visually separate from the authoring surface.
|
||||||
|
|
||||||
|
### 5.2 Stickman Editor flow
|
||||||
|
|
||||||
|
Current primary flow, based on [stickman_editor.gd](../../scripts/editors/stickman/stickman_editor.gd), [body_part_panel.gd](../../scripts/editors/stickman/body_part_panel.gd), and [whole_stickman_preview.gd](../../scripts/editors/stickman/whole_stickman_preview.gd):
|
||||||
|
|
||||||
|
~~~mermaid
|
||||||
|
flowchart LR
|
||||||
|
LoadOrStart[Load or start stickman] --> Parts[Choose a body-part drawing panel]
|
||||||
|
Parts --> Draw[Draw and color one or more shapes]
|
||||||
|
Draw --> Select[Select shape or vertex]
|
||||||
|
Select --> Preview[Inspect whole-stickman assembly]
|
||||||
|
Preview --> Guide[Toggle pose guide / reset views]
|
||||||
|
Guide --> Parts
|
||||||
|
Parts --> Save[Save stickman]
|
||||||
|
Save --> Continue[Continue editing]
|
||||||
|
LoadOrStart --> Clear[Clear with confirmation]
|
||||||
|
Clear --> Parts
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Presentation rules for this flow:
|
||||||
|
|
||||||
|
- Make the active body-part panel and active shape unambiguous. Selecting a shape in one panel clears selection in the others.
|
||||||
|
- Keep local drawing context and the assembled Whole Stickman preview distinct but visually related. A change in one body part should have a prompt, visible effect in the assembly preview.
|
||||||
|
- Use the pose guide as quiet construction help beneath selection gizmos. Guide colors distinguish left/right limbs; the guide must not capture pointer input or obscure active edits.
|
||||||
|
- Keep color editing and recent colors close to the relevant part, and confirm a color only through the explicit dialog action.
|
||||||
|
- Keep grid/snap and guide visibility available as tool preferences, visually separated from authored shape color and geometry.
|
||||||
|
|
||||||
|
### 5.3 Reusable dialog transaction
|
||||||
|
|
||||||
|
For either editor, the expected child-facing sequence is:
|
||||||
|
|
||||||
|
1. State the action in the title and one short sentence.
|
||||||
|
2. Show the effect or consequence when it may be surprising.
|
||||||
|
3. Present one primary action, any needed destructive action, and a visible **Cancel**. If the only forward-looking choice is destructive (such as Start Over), keep it in the danger role and focus Cancel initially.
|
||||||
|
4. Keep focus in the dialog, preserve the current anchor during resize, and return focus to the invoking control after close where practical.
|
||||||
|
5. On success, close the dialog and update the affected editor state once. On cancel, leave the document unchanged.
|
||||||
|
|
||||||
|
This is a target interaction contract for editor dialogs. Existing controls should be checked individually before being treated as compliant.
|
||||||
|
|
||||||
|
## 6. Interaction, feedback, and child-facing language
|
||||||
|
|
||||||
|
### 6.1 Input and feedback
|
||||||
|
|
||||||
|
- Support mouse and touch-size targets; target at least **44 × 44 logical pixels** for child-facing actions. Prefer Stage's **48 px** action target where the layout allows.
|
||||||
|
- Keep keyboard navigation visible and complete: Tab reaches meaningful controls, Enter/Space activates them, Escape cancels temporary tasks, and focus does not disappear behind a selected style.
|
||||||
|
- Keep right-click and keyboard shortcuts as optional accelerators. A visible menu item or button should remain available for important operations.
|
||||||
|
- Keep the top-level File/Edit/View menu titles plain and use familiar emoji beside readable action labels inside their dropdowns. Match the Sandbox Stage menu scale: 22 px menu-bar labels and 26 px popup labels. Group file actions as New/Open/Recent, then Save/Save As, with separators before a distinct Rename or Start Over action where present. Use the restrained charcoal popup surface for menu-bar dropdowns; reserve the raised Studio List surface for canvas right-click menus.
|
||||||
|
- Style single-choice editor pickers as a dark field with the chosen label and chevron. Popup rows keep readable text and may include a meaningful domain icon, as in Prop material and shape choices; do not show radio/check glyphs or an empty indicator lane. The current choice gets a full teal surface and cyan outline. Keep hover and keyboard focus visible, and give the field and rows at least 44 logical pixels of height. Use the shared `EditorSelectionDropdown` control for Prop and Stickman editor pickers.
|
||||||
|
- Standardize Prop and Stickman canvas context menus on the **Studio List** pattern: a compact vertical list on a raised editor surface, with a subtle border, rounded corners, readable 20 px labels, and 48 px rows. Resolve surface, border, text, hover, EDIT accent, focus, and destructive colors through `EditorVisualTheme` tokens rather than local color literals.
|
||||||
|
- Group related actions with spacing and separators, and keep each menu's actions specific to the current mode and hit target. Hover uses a lighter surface with a slim EDIT-cyan cue; keyboard focus has a distinct, high-contrast focus outline. Open with no focused row so mouse users do not mistake an action for a selection; show the focus outline after Up/Down navigation, and clear it when the pointer takes over. Give destructive actions explicit labels, separate them from regular actions, and place them last when practical; do not use color as their only cue.
|
||||||
|
- Keep labels as the source of meaning and use icons sparingly and consistently. Open the menu near the pointer, keep the clicked point or shape edge visible, and clamp the menu within the viewport.
|
||||||
|
- For dragging, preview changes continuously, show which handle is active, commit on release, and provide a clear cancel path when the operation is consequential or multi-step.
|
||||||
|
- Avoid hit-test surprises: decorative labels/icons ignore mouse input; a large row toggle has one activation target; a canvas guide is visual only.
|
||||||
|
- Preserve the world point under the cursor during zoom. Keep selected gizmos and handles visible above artwork and grid.
|
||||||
|
- Use both visual and textual feedback for snap, mode, selected state, disabled reason, save state, and errors.
|
||||||
|
|
||||||
|
The Stage panel's full-row toggle and stable help layout are useful behaviors when a row switch is appropriate. In an editor inspector, apply the same principles without copying the Stage card's floating shell.
|
||||||
|
|
||||||
|
### 6.2 Copy and labels
|
||||||
|
|
||||||
|
Prefer concise, concrete verbs and familiar terms:
|
||||||
|
|
||||||
|
| Prefer | Avoid |
|
||||||
|
| --- | --- |
|
||||||
|
| **Arrange**, **Reshape**, **Attach** | Technical labels such as “Transform mode” or “Edit points mode” as the main child-facing label |
|
||||||
|
| **Add Shape**, **Erase Shape**, **Create Point**, **Remove Point** | Ambiguous labels such as “Apply” when the result can be named |
|
||||||
|
| **Save**, **Save As…**, **Load**, **Cancel** | Icon-only actions with no tooltip or accessible label |
|
||||||
|
| “This shape was reshaped.” | A disabled Radius field with no explanation |
|
||||||
|
| “Choose where this prop attaches.” | Internal terms such as “anchor payload” |
|
||||||
|
| “Unsaved changes. Save before starting a new prop?” | Generic “Are you sure?” without naming the consequence |
|
||||||
|
|
||||||
|
Emoji can help distinguish common actions, as used by both editor menus, but should be an optional cue rather than the only meaning. Keep the text label understandable if an emoji font is unavailable. Use the same wording for the same action in menu, dialog, toolbar, tooltip, and user guide.
|
||||||
|
|
||||||
|
### 6.3 Icons and emoji
|
||||||
|
|
||||||
|
- Use one icon or emoji to help scan a familiar action; do not put emoji on every label by default.
|
||||||
|
- Keep the text label as the source of meaning. Provide a tooltip or accessible name for icon-only canvas tools.
|
||||||
|
- Use icons consistently for the same action across menus, toolbars, empty states, and dialogs. Avoid changing icon meaning between editors.
|
||||||
|
- Avoid using an emoji's color as the only state cue; fonts and platforms may render it differently.
|
||||||
|
- Both editors use icons beside readable menu action labels, with separators between distinct groups. Keep top-level File/Edit/View titles plain; place icons on actions and use the same symbol for the same operation across editors.
|
||||||
|
- In the Prop inspector, use colorful, flat icons for section headings, material choices, 2D shape choices, and artwork tags. Keep text labels authoritative. Show tag selection with the selected surface and border; do not add a separate check badge. Shape icons must depict planar 2D geometry.
|
||||||
|
|
||||||
|
### 6.4 Empty, loading, and error states
|
||||||
|
|
||||||
|
The current editors have different levels of state guidance. The following table separates known behavior from recommended presentation:
|
||||||
|
|
||||||
|
| State | Current reference | Recommended presentation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| New/empty Prop canvas | Arrange has an add-shape context menu and an Arrange-only empty-canvas hint; Reshape does not offer adding a shape. | Keep the canvas mostly clear. Show a short Arrange hint only when it helps discover the next action; hide it as soon as artwork exists. Do not show a dead-end hint in Reshape or Attach. |
|
||||||
|
| Empty Prop inspector selection | Inspector sections depend on mode and selected shape/anchor. | State what to select or create when the empty inspector would otherwise look broken. Keep the message short and do not add a second competing empty-canvas prompt. |
|
||||||
|
| Prop load/recovery | Load failures use an error dialog; autosave recovery offers Restore or Discard. | Name the file/action in the message, preserve the current artwork on failure, and keep Restore/Discard outcomes distinct. Loading feedback should not imply that the document is already saved. |
|
||||||
|
| Stickman panel before drawing | The ten body-part panels are the main workspace; the panel script owns drawing and selection behavior. | Give each blank panel a concise, low-contrast drawing cue if discovery testing shows it is needed. Remove it on first shape so it does not compete with artwork. |
|
||||||
|
| Stickman load/clear | Load and save use file dialogs; Clear has a confirmation dialog; error dialogs report failures. | Keep the current document intact on a failed load. State the consequence in Clear confirmation and offer a visible cancel path. |
|
||||||
|
| Save / dirty state | Prop Editor has status/title and autosave behavior; Stickman Editor's status bar reports cursor coordinates, snap state, and save state. | Keep save status easy to find in both editors. Do not treat autosave as a user-visible successful save unless the file was actually saved to the chosen destination. |
|
||||||
|
|
||||||
|
Do not add spinners or blocking overlays for synchronous work without evidence that the operation takes long enough to need them. If an operation becomes asynchronous, keep progress near the task and preserve access to cancellation where safe.
|
||||||
|
|
||||||
|
### 6.5 Motion and reduced motion
|
||||||
|
|
||||||
|
- Use motion to show cause and effect: a brief selection flash, a panel entering, or a successful action cue. Keep it short and do not delay the next input.
|
||||||
|
- Do not animate every hover or value change. Motion should help the child understand an edit, not decorate idle controls.
|
||||||
|
- Keep persistent mode and selection state visible after any transition ends.
|
||||||
|
- Respect a reduced-motion preference for nonessential transitions and flashes. Provide an equally clear static state.
|
||||||
|
- The current code has short visual effects such as the Prop canvas's new-point flash and Stage completion effects. A unified reduced-motion preference is not established by the referenced theme/specs; treat it as a future product-level decision.
|
||||||
|
|
||||||
|
## 7. Theme and implementation checklist
|
||||||
|
|
||||||
|
Before adding or revising an editor control:
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
- [ ] Is the control's job clear and is it in the correct workspace area?
|
||||||
|
- [ ] Does it follow the correct pattern: persistent inspector, modal dialog, canvas tool, or temporary capture?
|
||||||
|
- [ ] Are neutral, hover, selected, focused, disabled, dragging, and error states defined?
|
||||||
|
- [ ] Does selected state remain distinct from hover and focus?
|
||||||
|
- [ ] Are colors backed by labels, icons, position, or shape cues where appropriate?
|
||||||
|
- [ ] Are labels child-readable and consistent with the existing action vocabulary?
|
||||||
|
- [ ] Can the layout handle smaller windows, longer names, and larger font presets?
|
||||||
|
|
||||||
|
### Theme
|
||||||
|
|
||||||
|
- [ ] Are colors, type sizes, spacing, radii, borders, and target sizes tokens rather than repeated literals?
|
||||||
|
- [ ] Does the token use a semantic name rather than a control-specific unexplained hex?
|
||||||
|
- [ ] Does it have a validated accessor, fallback, and range clamp where applicable?
|
||||||
|
- [ ] Are per-control style changes local and isolated from a shared cached Theme?
|
||||||
|
- [ ] Is the difference from Stage tokens intentional and documented?
|
||||||
|
|
||||||
|
### Interaction
|
||||||
|
|
||||||
|
- [ ] Can pointer, touch, and keyboard users reach and activate the control?
|
||||||
|
- [ ] Does a disabled control explain why, if not self-evident?
|
||||||
|
- [ ] Does temporary work have an obvious Cancel/undo path?
|
||||||
|
- [ ] Does a control's position remain stable while help, validation, or mode content changes?
|
||||||
|
- [ ] Does changing a tool preference avoid marking the document dirty unless that is intentional?
|
||||||
|
- [ ] Does success/failure produce visible feedback without hiding the canvas or losing authored work?
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
- [ ] Does the editor controller own document mutation, persistence, and dirty state?
|
||||||
|
- [ ] Does a reusable control emit a typed intent and display a supplied value?
|
||||||
|
- [ ] Is new reuse actually shared by more than one appropriate UI context?
|
||||||
|
- [ ] Are UI preferences, preview-only state, and document data kept separate?
|
||||||
|
- [ ] Are new theme values represented in both default config and committed JSON?
|
||||||
|
- [ ] Are existing user guides/specs updated only after the behavior is verified?
|
||||||
|
|
||||||
|
### Implementation recipe
|
||||||
|
|
||||||
|
For a new editor UI element, use this sequence:
|
||||||
|
|
||||||
|
1. Name its job and state owner. Decide whether the editor controller, canvas, or reusable view owns the value and mutation.
|
||||||
|
2. Identify the closest reference pattern: Stage inspector, Stage dialog, Prop Editor mode control, Prop canvas selection, or Stickman body-part panel. Record what should carry over and what should not.
|
||||||
|
3. Define the semantic tokens and states before styling. Reuse existing token names where their meaning matches; add new names to the theme defaults and JSON with validation/fallback behavior.
|
||||||
|
4. Build with Godot containers and native controls first. Add a custom control only when it provides repeated behavior or custom canvas drawing.
|
||||||
|
5. Wire typed intent signals to the owning controller. Refresh controls from document state without emitting a second mutation.
|
||||||
|
6. Check the smallest supported window, larger type, long labels, all modes, selected/disabled/focused states, and save/error/cancel paths.
|
||||||
|
7. Capture before/after screenshots or a short manual checklist. Update the user guide after the behavior is accepted.
|
||||||
|
|
||||||
|
This is recommended implementation order, not a claim that every current editor control follows it.
|
||||||
|
|
||||||
|
### Recommended harmonization order
|
||||||
|
|
||||||
|
1. **Maintain token ownership.** `EditorVisualTheme` owns shared standalone-editor chrome; `PropTheme` owns prop-domain and canvas defaults; `SandboxTheme` owns Stage-specific UI. Document any intentional override at the token or component that owns it.
|
||||||
|
2. **Audit remaining overrides.** Check for editor chrome values that still bypass `EditorVisualTheme`; preserve the Prop canvas surface/material palette and Stickman canvas drawing tokens where they serve artwork contrast.
|
||||||
|
3. **Document responsive limits.** Record minimum usable widths, toolbar wrapping, how the two Stickman Overview columns share space beside the preview, and when Focus or scrolling takes over.
|
||||||
|
4. **Audit remaining state gaps.** Verify focus, disabled reasons, save/error status, destructive confirmation, and cancel behavior. Both editors use the shared `StageDialogStyler` card for confirmation, recovery, and short action dialogs.
|
||||||
|
5. **Validate visually at agreed sizes.** Compare the editor screenshots with the Stage reference and adjust intentional differences explicitly.
|
||||||
|
|
||||||
|
This order is a maintenance checklist for the current shared chrome and editor-specific drawing patterns; it does not propose replacing the established theme architecture.
|
||||||
|
|
||||||
|
### Visual QA checklist
|
||||||
|
|
||||||
|
- [ ] Compare Prop and Stickman editor startup screens side by side with the Stage's dark surface and EDIT accent references.
|
||||||
|
- [ ] Check every mode with no selection, with a selected object, during drag, and after release/cancel.
|
||||||
|
- [ ] Check primary action, destructive action, disabled control, hover, keyboard focus, and persistent selected states.
|
||||||
|
- [ ] Check prop autosave recovery, load failure, unsaved New/Close, and successful save feedback.
|
||||||
|
- [ ] Check Stickman Clear confirmation, load failure, snap status, pose guide, and cross-panel selection.
|
||||||
|
- [ ] Resize to the smallest supported viewport and increase font size. Confirm canvas access, scroll access, and nonoverlapping controls.
|
||||||
|
- [ ] Verify visible keyboard order and Return/Space/Escape behavior; confirm focus returns to the invoking control where appropriate.
|
||||||
|
- [ ] Check grid, artwork, selection halo, vertex handles, pose guide, anchor markers, and preview ghosts against light and dark artwork.
|
||||||
|
- [ ] Check text contrast and state distinction with a contrast measurement and color-vision simulation; the current specs do not provide measured results.
|
||||||
|
- [ ] Repeat the checks with emoji rendering unavailable or replaced; action meaning must remain clear from text.
|
||||||
|
|
||||||
|
## 8. Recommended additional coverage
|
||||||
|
|
||||||
|
To make this guide a stronger long-term design reference, add these pieces as the editor UI evolves:
|
||||||
|
|
||||||
|
1. **Annotated screenshots at canonical window sizes.** Include each editor's initial screen, every mode, selection/reshape states, Attach preview, and one modal. Mark which visuals are current and which are target.
|
||||||
|
2. **A theme ownership map.** Document how `SandboxTheme`, `EditorVisualTheme`, and `PropTheme` divide Stage UI, shared editor chrome, and Prop domain/canvas values. Note intentional overrides and identify any remaining editor chrome that bypasses its shared token.
|
||||||
|
3. **Responsive breakpoints and minimum workspace sizes.** Record minimum usable canvas/inspector widths, when controls wrap, how the Attach preview stacks, and how the two Stickman body-panel columns share space with the Whole Stickman preview.
|
||||||
|
4. **Accessibility and contrast acceptance checks.** Specify contrast targets, keyboard tab order, focus visibility, touch target checks, color-vision-safe cues, and high-font-size behavior. The current references provide useful states and target sizes but do not establish a measured accessibility audit.
|
||||||
|
5. **Motion and sound rules.** Define when transitions, selection flashes, save feedback, and error feedback animate or use sound; keep them brief, informative, and respectful of reduced-motion preferences.
|
||||||
|
6. **Content and localization rules.** Include label length budgets, wrapping/truncation behavior, tooltip standards, pluralization, and translation expansion checks.
|
||||||
|
7. **Editor-specific control patterns.** Document mode bars, drawing canvases, color pickers, anchor lists, selection gizmos, empty states, and destructive file flows as reusable examples with their owning state and signals.
|
||||||
|
8. **Verification matrix.** Link each visual contract to a scene/test/manual check so future changes can distinguish an implemented pattern from a recommendation.
|
||||||
|
|
||||||
|
## 9. Reference files
|
||||||
|
|
||||||
|
- [AGENTS.md](../../AGENTS.md) — repository layout, editor architecture, shared Stage UI, and child-facing dialog guardrails.
|
||||||
|
- [Stage panel UI system](../specs/stage_panel_ui_system.md) — Stage inspector card, transient capture bar, component contracts, fitting, and acceptance criteria.
|
||||||
|
- [Dialog Design Principles](../specs/dialog_design_principles.md) — child-facing modal shell, action hierarchy, and stable resize behavior.
|
||||||
|
- [SandboxTheme implementation](../../scripts/stage/sandbox_theme.gd) and [SandboxTheme config](../../sandbox_theme.json).
|
||||||
|
- [PropTheme implementation](../../scripts/editors/prop/prop_theme.gd) and [PropTheme config](../../prop_theme.json).
|
||||||
|
- [Prop Editor controller](../../scripts/editors/prop/prop_editor.gd), [Prop Editor scene](../../scenes/editors/prop/prop_editor.tscn), and [Prop Editor guide](prop_editor_user_guide.md).
|
||||||
|
- [Prop canvas](../../scripts/editors/prop/prop_canvas.gd) — mode behavior, canvas gestures, selection handles, and context menus.
|
||||||
|
- [Stickman Editor controller](../../scripts/editors/stickman/stickman_editor.gd), [Stickman Editor scene](../../scenes/editors/stickman/stickman_editor.tscn), [BodyPartPanel](../../scripts/editors/stickman/body_part_panel.gd), and [Whole Stickman preview](../../scripts/editors/stickman/whole_stickman_preview.gd).
|
||||||
|
- [Stage dialog style helper](../../scripts/shared/stage_ui/stage_dialog_styler.gd) and [Stage capture bar](../../scripts/editors/stage/stage_use_target_capture_bar.gd).
|
||||||
|
- [Prop Editor UX specification](../specs/phase_9c_prop_editor_ux.md) — current approved Prop Editor mode, Attach preview, marker, and layout behavior.
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,191 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1800" height="1260" viewBox="0 0 1800 1260" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Editor visual harmonization: three design directions</title>
|
||||||
|
<desc id="desc">A comparison board showing Familiar Studio, Adaptive Studio, and Focused Maker concepts for the Prop and Stickman Editors.</desc>
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.bg { fill: #171b24; }
|
||||||
|
.card { fill: #202633; stroke: #465064; stroke-width: 2; }
|
||||||
|
.card-rec { fill: #202b39; stroke: #22c6ff; stroke-width: 3; }
|
||||||
|
.surface { fill: #252b38; stroke: #596477; stroke-width: 1.5; }
|
||||||
|
.work { fill: #171d28; stroke: #657184; stroke-width: 1.5; }
|
||||||
|
.canvas { fill: #20232b; stroke: #7f8ca0; stroke-width: 1.5; }
|
||||||
|
.cyan { fill: #22c6ff; }
|
||||||
|
.cyan-stroke { stroke: #22c6ff; }
|
||||||
|
.blue { fill: #087dba; }
|
||||||
|
.muted { fill: #aab2c0; }
|
||||||
|
.text { fill: #f5f7fa; font-family: Arial, sans-serif; }
|
||||||
|
.small { font-size: 17px; }
|
||||||
|
.body { font-size: 20px; }
|
||||||
|
.label { font-size: 18px; font-weight: 700; }
|
||||||
|
.h2 { font-size: 30px; font-weight: 700; }
|
||||||
|
.title { font-size: 40px; font-weight: 700; }
|
||||||
|
.chip { fill: #2d3748; stroke: #596477; stroke-width: 1; }
|
||||||
|
.active-chip { fill: #103b4c; stroke: #22c6ff; stroke-width: 2; }
|
||||||
|
.line { stroke: #667286; stroke-width: 1.5; }
|
||||||
|
.accent-line { stroke: #22c6ff; stroke-width: 3; }
|
||||||
|
.selected { fill: none; stroke: #22c6ff; stroke-width: 3; }
|
||||||
|
.outline { fill: none; stroke: #f5f7fa; stroke-width: 2; }
|
||||||
|
.figure { fill: none; stroke: #aab2c0; stroke-width: 4; stroke-linecap: round; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<rect class="bg" width="1800" height="1260"/>
|
||||||
|
<text class="text title" x="48" y="62">Two editors, one visual language</text>
|
||||||
|
<text class="text body muted" x="48" y="98">Phase 0a · Three layout directions · Shared tokens and accessible states in every option</text>
|
||||||
|
|
||||||
|
<!-- Concept A -->
|
||||||
|
<g transform="translate(42,132)">
|
||||||
|
<rect class="card" width="556" height="1080" rx="20"/>
|
||||||
|
<text class="text h2" x="26" y="46">A. Familiar Studio</text>
|
||||||
|
<text class="text small muted" x="26" y="76">Keep today’s workspace arrangement</text>
|
||||||
|
<rect class="chip" x="26" y="94" width="176" height="34" rx="17"/>
|
||||||
|
<text class="text label small" x="42" y="117">LOWEST CHANGE</text>
|
||||||
|
|
||||||
|
<text class="text label" x="26" y="166">PROP EDITOR · WIDE</text>
|
||||||
|
<rect class="surface" x="26" y="180" width="504" height="260" rx="12"/>
|
||||||
|
<rect class="surface" x="36" y="190" width="484" height="38" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="215">File Save <tspan fill="#aab2c0">Unsaved changes</tspan></text>
|
||||||
|
<rect class="active-chip" x="319" y="197" width="82" height="26" rx="13"/>
|
||||||
|
<text class="text small" x="330" y="216">Arrange</text>
|
||||||
|
<rect class="chip" x="405" y="197" width="68" height="26" rx="13"/>
|
||||||
|
<text class="text small" x="415" y="216">Reshape</text>
|
||||||
|
<rect class="chip" x="477" y="197" width="36" height="26" rx="13"/>
|
||||||
|
<text class="text small" x="484" y="216">…</text>
|
||||||
|
<rect class="canvas" x="36" y="237" width="322" height="192" rx="8"/>
|
||||||
|
<path class="figure" d="M196 278 L196 363 M196 297 L151 337 M196 297 L242 337 M196 363 L163 406 M196 363 L230 406"/>
|
||||||
|
<circle class="selected" cx="196" cy="278" r="16"/>
|
||||||
|
<rect class="surface" x="368" y="237" width="152" height="192" rx="8"/>
|
||||||
|
<text class="text small" x="381" y="263">Inspector</text>
|
||||||
|
<rect class="chip" x="380" y="275" width="128" height="34" rx="7"/>
|
||||||
|
<text class="text small" x="393" y="298">Fill color</text>
|
||||||
|
<rect class="chip" x="380" y="317" width="128" height="34" rx="7"/>
|
||||||
|
<text class="text small" x="393" y="340">Outline</text>
|
||||||
|
<rect class="blue" x="380" y="365" width="128" height="42" rx="9"/>
|
||||||
|
<text class="text small" x="403" y="392">Add Shape</text>
|
||||||
|
|
||||||
|
<text class="text label" x="26" y="484">STICKMAN EDITOR · WIDE</text>
|
||||||
|
<rect class="surface" x="26" y="498" width="504" height="330" rx="12"/>
|
||||||
|
<rect class="surface" x="36" y="508" width="484" height="35" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="532">File Save View <tspan fill="#aab2c0">Saved</tspan></text>
|
||||||
|
<rect class="work" x="36" y="552" width="148" height="264" rx="8"/>
|
||||||
|
<rect class="work" x="194" y="552" width="148" height="264" rx="8"/>
|
||||||
|
<rect class="work" x="352" y="552" width="158" height="264" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="577">Head · Torso</text>
|
||||||
|
<text class="text small" x="206" y="577">Arms · Legs</text>
|
||||||
|
<text class="text small" x="364" y="577">Whole Stickman</text>
|
||||||
|
<g fill="#252b38" stroke="#586477">
|
||||||
|
<rect x="46" y="591" width="58" height="62" rx="6"/><rect x="113" y="591" width="58" height="62" rx="6"/>
|
||||||
|
<rect x="46" y="662" width="58" height="62" rx="6"/><rect x="113" y="662" width="58" height="62" rx="6"/>
|
||||||
|
<rect x="46" y="733" width="58" height="62" rx="6"/><rect x="113" y="733" width="58" height="62" rx="6"/>
|
||||||
|
<rect x="204" y="591" width="58" height="62" rx="6"/><rect x="271" y="591" width="58" height="62" rx="6"/>
|
||||||
|
<rect x="204" y="662" width="58" height="62" rx="6"/><rect x="271" y="662" width="58" height="62" rx="6"/>
|
||||||
|
</g>
|
||||||
|
<rect class="selected" x="45" y="590" width="60" height="64" rx="7"/>
|
||||||
|
<path class="figure" d="M431 603 L431 708 M431 623 L405 651 M431 623 L457 651 M431 708 L411 760 M431 708 L451 760"/>
|
||||||
|
<text class="text small muted" x="26" y="858">Update styling, targets, focus, and dialogs;</text>
|
||||||
|
<text class="text small muted" x="26" y="882">keep all ten drawing panels visible.</text>
|
||||||
|
|
||||||
|
<line class="line" x1="26" y1="918" x2="530" y2="918"/>
|
||||||
|
<text class="text label" x="26" y="955">Tradeoff</text>
|
||||||
|
<text class="text body" x="26" y="986">Least workflow change. The ten small canvases</text>
|
||||||
|
<text class="text body" x="26" y="1014">stay dense; narrow windows may hide the preview.</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Concept B -->
|
||||||
|
<g transform="translate(622,132)">
|
||||||
|
<rect class="card-rec" width="556" height="1080" rx="20"/>
|
||||||
|
<text class="text h2" x="26" y="46">B. Adaptive Studio</text>
|
||||||
|
<text class="text small muted" x="26" y="76">Keep overview; add a workspace that can grow</text>
|
||||||
|
<rect class="active-chip" x="26" y="94" width="198" height="34" rx="17"/>
|
||||||
|
<text class="text label small" x="42" y="117">RECOMMENDED</text>
|
||||||
|
|
||||||
|
<text class="text label" x="26" y="166">PROP EDITOR · WIDE</text>
|
||||||
|
<rect class="surface" x="26" y="180" width="504" height="220" rx="12"/>
|
||||||
|
<rect class="surface" x="36" y="190" width="484" height="38" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="215">File Save <tspan fill="#aab2c0">Saved</tspan></text>
|
||||||
|
<rect class="active-chip" x="312" y="197" width="82" height="26" rx="13"/>
|
||||||
|
<text class="text small" x="323" y="216">Arrange</text>
|
||||||
|
<rect class="chip" x="398" y="197" width="68" height="26" rx="13"/>
|
||||||
|
<text class="text small" x="408" y="216">Reshape</text>
|
||||||
|
<rect class="chip" x="470" y="197" width="42" height="26" rx="13"/>
|
||||||
|
<text class="text small" x="478" y="216">Attach</text>
|
||||||
|
<rect class="canvas" x="36" y="237" width="318" height="152" rx="8"/>
|
||||||
|
<path class="figure" d="M192 265 L192 337 M192 281 L154 312 M192 281 L230 312 M192 337 L164 375 M192 337 L221 375"/>
|
||||||
|
<circle class="selected" cx="192" cy="265" r="14"/>
|
||||||
|
<rect class="surface" x="364" y="237" width="156" height="152" rx="8"/>
|
||||||
|
<text class="text small" x="376" y="263">Scroll settings</text>
|
||||||
|
<rect class="chip" x="376" y="276" width="132" height="30" rx="7"/>
|
||||||
|
<rect class="chip" x="376" y="314" width="132" height="30" rx="7"/>
|
||||||
|
<rect class="blue" x="376" y="352" width="132" height="28" rx="7"/>
|
||||||
|
<text class="text small" x="399" y="372">Add Shape</text>
|
||||||
|
|
||||||
|
<text class="text label" x="26" y="446">STICKMAN · FOCUS ON PART</text>
|
||||||
|
<rect class="surface" x="26" y="460" width="504" height="354" rx="12"/>
|
||||||
|
<rect class="surface" x="36" y="470" width="484" height="36" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="495">File Save <tspan fill="#aab2c0">Unsaved changes</tspan></text>
|
||||||
|
<rect class="chip" x="36" y="514" width="224" height="34" rx="8"/>
|
||||||
|
<text class="text small" x="95" y="537">Overview · 10 parts</text>
|
||||||
|
<rect class="active-chip" x="268" y="514" width="252" height="34" rx="8"/>
|
||||||
|
<text class="text small" x="330" y="537">Focus on Part</text>
|
||||||
|
<rect class="active-chip" x="36" y="556" width="484" height="38" rx="8"/>
|
||||||
|
<text class="text label" x="51" y="581">Body part: Head ▾</text>
|
||||||
|
<rect class="canvas" x="36" y="602" width="294" height="170" rx="8"/>
|
||||||
|
<text class="text small muted" x="50" y="625">Head drawing canvas</text>
|
||||||
|
<circle class="outline" cx="181" cy="688" r="43"/>
|
||||||
|
<circle class="selected" cx="181" cy="688" r="48"/>
|
||||||
|
<rect class="surface" x="340" y="602" width="180" height="170" rx="8"/>
|
||||||
|
<text class="text small" x="354" y="627">Whole Stickman</text>
|
||||||
|
<path class="figure" d="M430 643 L430 707 M430 656 L410 674 M430 656 L450 674 M430 707 L414 750 M430 707 L446 750"/>
|
||||||
|
<rect class="surface" x="36" y="780" width="484" height="26" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="800">Add Shape Arrange Reshape Color + Future tools</text>
|
||||||
|
|
||||||
|
<text class="text small muted" x="26" y="848">Overview keeps all ten panels available; Focus makes</text>
|
||||||
|
<text class="text small muted" x="26" y="874">room for new tools and is useful at any window size.</text>
|
||||||
|
<line class="line" x1="26" y1="918" x2="530" y2="918"/>
|
||||||
|
<text class="text label" x="26" y="955">Tradeoff</text>
|
||||||
|
<text class="text body" x="26" y="986">More workspace and state work; switching views</text>
|
||||||
|
<text class="text body" x="26" y="1014">must preserve selection and drawing context.</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Concept C -->
|
||||||
|
<g transform="translate(1202,132)">
|
||||||
|
<rect class="card" width="556" height="1080" rx="20"/>
|
||||||
|
<text class="text h2" x="26" y="46">C. Focused Maker</text>
|
||||||
|
<text class="text small muted" x="26" y="76">Make one task and canvas the center of both editors</text>
|
||||||
|
<rect class="chip" x="26" y="94" width="205" height="34" rx="17"/>
|
||||||
|
<text class="text label small" x="42" y="117">LARGEST REDESIGN</text>
|
||||||
|
|
||||||
|
<text class="text label" x="26" y="166">ONE SHARED WORKSPACE PATTERN</text>
|
||||||
|
<rect class="surface" x="26" y="180" width="504" height="552" rx="12"/>
|
||||||
|
<rect class="surface" x="36" y="190" width="484" height="38" rx="8"/>
|
||||||
|
<text class="text small" x="48" y="215">File Save <tspan fill="#aab2c0">Saved</tspan></text>
|
||||||
|
<rect class="surface" x="36" y="238" width="484" height="50" rx="8"/>
|
||||||
|
<text class="text label" x="52" y="270">Choose task or body part ▾</text>
|
||||||
|
<rect class="canvas" x="36" y="298" width="318" height="338" rx="8"/>
|
||||||
|
<text class="text small muted" x="51" y="324">Large active canvas</text>
|
||||||
|
<path class="figure" d="M195 382 L195 500 M195 405 L151 444 M195 405 L239 444 M195 500 L160 570 M195 500 L230 570"/>
|
||||||
|
<circle class="selected" cx="195" cy="382" r="26"/>
|
||||||
|
<rect class="surface" x="364" y="298" width="156" height="338" rx="8"/>
|
||||||
|
<text class="text small" x="377" y="325">For this task</text>
|
||||||
|
<rect class="chip" x="376" y="340" width="132" height="39" rx="7"/>
|
||||||
|
<text class="text small" x="389" y="366">Color</text>
|
||||||
|
<rect class="chip" x="376" y="388" width="132" height="39" rx="7"/>
|
||||||
|
<text class="text small" x="389" y="414">Shape</text>
|
||||||
|
<rect class="blue" x="376" y="440" width="132" height="42" rx="8"/>
|
||||||
|
<text class="text small" x="392" y="467">Add Shape</text>
|
||||||
|
<rect class="chip" x="376" y="492" width="132" height="39" rx="7"/>
|
||||||
|
<text class="text small" x="389" y="518">Erase Shape</text>
|
||||||
|
<rect class="active-chip" x="36" y="646" width="484" height="72" rx="9"/>
|
||||||
|
<text class="text label" x="53" y="675">Preview drawer</text>
|
||||||
|
<text class="text small" x="53" y="702">Whole Stickman · Attach preview · Open</text>
|
||||||
|
|
||||||
|
<text class="text small muted" x="26" y="770">The same task-first frame in Prop and Stickman.</text>
|
||||||
|
<text class="text small muted" x="26" y="796">The preview opens only when needed.</text>
|
||||||
|
<line class="line" x1="26" y1="918" x2="530" y2="918"/>
|
||||||
|
<text class="text label" x="26" y="955">Tradeoff</text>
|
||||||
|
<text class="text body" x="26" y="986">Strongest focus and consistency, but most users</text>
|
||||||
|
<text class="text body" x="26" y="1014">must relearn navigation; all ten parts are no</text>
|
||||||
|
<text class="text body" x="26" y="1042">longer visible at once.</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text class="text small muted" x="48" y="1240">For discussion only · Palette and labels follow the Editor Visual Style Guide; this is not final UI styling.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -21,6 +21,8 @@ Use this guide when creating or revising child-facing dialogs, popups, and short
|
|||||||
## Theme and implementation
|
## Theme and implementation
|
||||||
|
|
||||||
- Put reusable colors, sizes, spacing, radii, and border widths in `SandboxTheme` or `PropTheme` tokens. Apply local stylebox overrides instead of mutating a shared theme resource.
|
- Put reusable colors, sizes, spacing, radii, and border widths in `SandboxTheme` or `PropTheme` tokens. Apply local stylebox overrides instead of mutating a shared theme resource.
|
||||||
|
- Use `StageDialogStyler` in `scripts/shared/stage_ui/stage_dialog_styler.gd` for Sandbox Stage and standalone Prop/Stickman confirmation, recovery, and short action dialogs. It owns the shared shell, typography, and primary, secondary, or destructive button roles through `SandboxTheme.popups.confirmation`. Custom Control-based dialogs can reuse its `panel_style()` and `style_button()` helpers.
|
||||||
|
- Render the shared modal surface as a Control in the active scene's UI CanvasLayer. Keep the native `AcceptDialog` or `ConfirmationDialog` hidden as the signal source; a full-screen native Window can replace the active scene with a blank surface on some render paths. Native `FileDialog` contents remain native.
|
||||||
- Keep larger text from clipping: use containers that can expand or wrap, and size the dialog after its visible content has been laid out.
|
- Keep larger text from clipping: use containers that can expand or wrap, and size the dialog after its visible content has been laid out.
|
||||||
- Preserve clear hover, pressed, selected, focused, and disabled states. Do not use color as the only indication of a state.
|
- Preserve clear hover, pressed, selected, focused, and disabled states. Do not use color as the only indication of a state.
|
||||||
- Adapt the arrangement to each workflow while keeping this shared visual language. The Choose Scenery dialog in `scripts/shared/ui/asset_selector.gd` and its `popups.asset_selector` theme tokens are the current implementation reference.
|
- Adapt the arrangement to each workflow while keeping this shared visual language. The Choose Scenery dialog in `scripts/shared/ui/asset_selector.gd` and its `popups.asset_selector` theme tokens are the current implementation reference.
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Editor visual harmonization — architecture and UI specification
|
||||||
|
|
||||||
|
> **Status:** Proposed for final user sign-off (2026-10-23). **Branch:** `feature/editor-visual-harmonization`. **Scope:** Prop Editor and Stickman Editor presentation, responsive workspaces, control feedback, dialogs, and save status. This specification does not change `.prop` or `.stk` formats or Stage inspector behavior.
|
||||||
|
|
||||||
|
## 1. Outcome and approved direction
|
||||||
|
|
||||||
|
The two editors should read as parts of one Stickman Studio product while keeping their distinct authoring tasks. The canvas remains the main work surface. The Prop Editor retains its Arrange, Reshape, and Attach modes, split canvas, Attach preview, and scrolling inspector. The Stickman Editor gains **Overview**, with all ten body-part panels available, and **Focus on Part**, with one larger live panel, a body-part picker, and the Whole Stickman preview. Focus can be chosen at any window size. It is the automatic default only when the minimum widths needed by Overview do not fit. Once a child chooses a view, that choice remains in effect for the session through window resizing.
|
||||||
|
|
||||||
|
The [Editor Visual Style Guide](../guides/editor_visual_style_guide.md) supplies the visual direction, and [Dialog Design Principles](dialog_design_principles.md) supplies modal behavior. [Stage panel UI system](stage_panel_ui_system.md) remains scoped to in-stage selected-object cards. Neither `StageInspectorPanel` nor its choice/toggle controls are transplanted into the persistent editor workspaces.
|
||||||
|
|
||||||
|
The implementation proceeds in staged slices under this one contract. Each slice should be reviewable without changing the saved document schema. Final handoff waits for the behavior and rendered visual checks in §10.
|
||||||
|
|
||||||
|
## 2. Current architecture and constraints
|
||||||
|
|
||||||
|
| Area | Current fact | Consequence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Prop theme | `PropTheme` reads `prop_theme.json` with defaults, clamping, and typed accessors. `PropEditor` builds a local `Theme`; it also hardcodes some stylebox colors and gives mode buttons `FOCUS_NONE`. | Share chrome roles through one editor token source and restore a distinct keyboard focus state. Keep prop material, primitive, and canvas values under `PropTheme`. |
|
||||||
|
| Prop layout | `prop_editor.tscn` has fixed top-row offsets, outer `HSplitContainer`, scrolling inspector, and an inner Attach split. | Replace brittle top-row positioning with container sizing; keep both splitters and the inspector scroll. |
|
||||||
|
| Stickman state | `StickmanEditor` instantiates ten `BodyPartPanel` scenes, stores them by part name, and reads their live shapes for preview and save. Each panel also owns shape selection, zoom, pan, and popup state. | A view switch must move the same nodes. Calling `set_shape_data()` would reset selection, zoom, and pan. |
|
||||||
|
| Stickman layout | The scene has two five-panel `VBoxContainer` columns and one `WholeStickmanPreview` in a fixed three-column `HBoxContainer`. Each body panel has a 230 × 160 minimum. | Overview needs scroll access; Focus needs a separate host and width-based initial choice. |
|
||||||
|
| Stickman document feedback | Save and load serialize directly; no current path or dirty status is tracked. `WholeStickmanPreview.part_moved` reports translation, while rotation and scale have no commit signal. | Dirty tracking and unsaved-work flows need an explicit controller boundary covering every authored transform. |
|
||||||
|
| Attach drawing order | Anchor `z_index_offset` is an integer clamped to −5…5. `PropAttachPreview` assigns it to the preview prop's `z_index`; held runtime props apply it relative to the rig's index. | Keep the signed value and preview result. Zero means normal ordering, not a universal “front” preset. |
|
||||||
|
|
||||||
|
## 3. Theme ownership and visual contract
|
||||||
|
|
||||||
|
### 3.1 One shared editor chrome source
|
||||||
|
|
||||||
|
Add `scripts/shared/editor_ui/editor_visual_theme.gd` as `class_name EditorVisualTheme extends RefCounted` and `res://editor_visual_theme.json`. It owns **shared editor chrome**: background and raised surfaces, primary and muted text, EDIT accent, keyboard focus, primary/destructive actions, borders, typography, spacing, radii, and minimum control targets. Its loader follows the validated `PropTheme`/`SandboxTheme` pattern: immutable defaults, deep merge of the committed JSON, type validation, clamping, a warning once per invalid key, and a safe default when a file or value is missing. Do not read `SandboxTheme` at runtime to style either editor; its values are a product reference and its popup/inspector semantics belong to the Stage.
|
||||||
|
|
||||||
|
Both editor controllers hold an `EditorVisualTheme` instance and build/apply their own local Godot `Theme` or per-control style overrides. They never mutate `ThemeDB`, the project-wide GUI theme, or a cached shared `Theme` for one selected control. `PropEditor` retains `PropTheme` for material tints, primitive defaults, outline widths, and **prop canvas** colors. Stickman canvas grid, silhouette guide, and selection colors live in a separate `stickman_canvas` namespace with typed accessors; artwork colors are never overridden by chrome styling. Existing `PropTheme.ui` getters may forward during the staged migration, but the final chrome value has one authoritative source in `EditorVisualTheme`. Remove duplicate configuration values when callers have migrated.
|
||||||
|
|
||||||
|
Suggested typed contract (precise names may change in implementation while preserving ownership):
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name EditorVisualTheme
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const DEFAULT_PATH: String = "res://editor_visual_theme.json"
|
||||||
|
|
||||||
|
func load_from(path: String = DEFAULT_PATH) -> bool: ...
|
||||||
|
func chrome_color(role: StringName, editor: StringName = &"") -> Color: ...
|
||||||
|
func chrome_px(role: StringName, editor: StringName = &"") -> int: ...
|
||||||
|
func stickman_canvas_color(role: StringName) -> Color: ...
|
||||||
|
func control_theme(editor: StringName) -> Theme: ... # return an editor-local instance
|
||||||
|
```
|
||||||
|
|
||||||
|
The JSON payload is organized by semantic chrome and drawing purpose, for example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"chrome": {
|
||||||
|
"colors": {"background": "#171B24F2", "surface": "#252B38FA", "focus": "#8BD9FF"},
|
||||||
|
"targets": {"button_min_height": 44, "mode_button_min_height": 56}
|
||||||
|
},
|
||||||
|
"editors": {"prop": {}, "stickman": {}},
|
||||||
|
"stickman_canvas": {"grid_color": "#FFFFFF26", "selection_core": "#FFFFFF", "selection_halo": "#000000D9"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The complete committed config and script defaults must contain all roles in §3.2, not only these example keys. Resolution order is script defaults, committed shared JSON, then an explicit editor override for the requested editor. Known roles use named accessors or validated role IDs, never arbitrary dotted strings at call sites. An optional `editors.prop` / `editors.stickman` override is allowed only where density or composition has a documented need; shared meanings stay the same. Document any legacy `PropTheme.ui` accessor forwarding and removal in the implementation slice.
|
||||||
|
|
||||||
|
### 3.2 Baseline tokens and states
|
||||||
|
|
||||||
|
| Role | Starting value or rule | Use |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Main background / raised surface | Stage references `#171B24F2` / `#252B38FA` | Editor chrome; keep drawing canvases visually distinct. |
|
||||||
|
| Primary / muted text | `#F5F7FA` / `#AAB2C0` | Titles, labels, helper copy, disabled reasons. |
|
||||||
|
| EDIT accent | `#22C6FF` | Active editor mode, part/view choice, and selection accent. |
|
||||||
|
| Keyboard focus | `#8BD9FF`, at least a distinct 2 px outline | Visible over normal, hover, and pressed styles; native Tab/Space/Enter remain usable. |
|
||||||
|
| Primary / destructive action | `#087DBA` / `#E66B75` | One forward dialog action and explicit erase/discard actions. Destructive controls retain words and a cancel path. |
|
||||||
|
| Targets | At least 44 × 44 logical pixels for child-facing interactive controls; mode buttons may stay 56 px high | Increase Prop's current 36 px button/row minima; enlarge click areas without enlarging canvas handles visually. |
|
||||||
|
| Density | Body text about 18–20 px, section headers about 20–24 px; compact Overview part title may be about 16–18 px | Confirm fit at minimum supported viewport and larger type. Focus may use a larger part title. |
|
||||||
|
| Canvas selection | Bright core plus dark backing; handle size and hit area are screen-space | Preserve Prop's existing two-tone halo. Extend the same contrast principle to Stickman panel and preview gizmos. |
|
||||||
|
|
||||||
|
The final theme must distinguish neutral, hover, pressed/selected, keyboard focus, disabled, dragging/preview, error, saved, and dirty states. Focus and selection may coexist; focus must not paint over or hide the active mode, as the current Prop button style does. Icons or emoji may add cues but each action remains understandable from text alone. The grid, pose guide, authored shape colors, and material colors remain canvas/domain tokens.
|
||||||
|
|
||||||
|
## 4. Stickman adaptive workspace
|
||||||
|
|
||||||
|
### 4.1 Node hierarchy and state owners
|
||||||
|
|
||||||
|
```text
|
||||||
|
StickmanEditor : Control (document, save/load, dirty, active part, view choice)
|
||||||
|
└─ ChromeRoot : VBoxContainer
|
||||||
|
├─ MenuBar / name and view controls
|
||||||
|
├─ WorkspaceRow : HSplitContainer
|
||||||
|
│ ├─ BodyPartWorkspace : Control
|
||||||
|
│ │ ├─ OverviewScroll : ScrollContainer
|
||||||
|
│ │ │ └─ OverviewColumns : HBoxContainer
|
||||||
|
│ │ │ ├─ LeftColumn : VBoxContainer (five original panels)
|
||||||
|
│ │ │ └─ CenterColumn : VBoxContainer (five original panels)
|
||||||
|
│ │ └─ FocusHost : VBoxContainer
|
||||||
|
│ │ ├─ PartPicker : native choices, scroll/wrap as needed
|
||||||
|
│ │ └─ FocusPanelHost : Control (one original BodyPartPanel)
|
||||||
|
│ └─ WholeStickmanPreview : PanelContainer (script: WholeStickmanPreview; permanent sibling)
|
||||||
|
└─ StatusBar
|
||||||
|
```
|
||||||
|
|
||||||
|
There is exactly one `BodyPartPanel` instance per part and one `WholeStickmanPreview` instance. The preview remains a permanent sibling of the body-part workspace in `WorkspaceRow`; it is not duplicated or serialized for a view switch. The controller records each panel's original Overview parent and index. Entering Focus reparents the active panel into `FocusPanelHost`; leaving Focus restores it at the original index. Changing focused part restores the outgoing panel before moving the incoming panel. The controller's existing panel dictionary and signal connections remain stable.
|
||||||
|
|
||||||
|
`StickmanEditor` owns `enum WorkspaceView { OVERVIEW, FOCUS }`, `_active_part_name: String`, `_view_choice_was_manual: bool`, and the pending view/part request. `BodyPartPanel` owns its shapes, selected shape, zoom, and pan; the preview owns its assembly transform and zoom/pan. The part picker changes `_active_part_name` without calling `BodyPartPanel.select()`, which currently selects the topmost shape and would overwrite the child's shape selection. Clicking a panel updates the active part through the existing selection signal. The active part gets its title inside the panel and a visible border cue in Overview and Focus; Focus does not add a duplicate heading above the canvas.
|
||||||
|
|
||||||
|
Suggested typed additions:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# BodyPartPanel
|
||||||
|
signal presentation_safe
|
||||||
|
func can_switch_presentation() -> bool: ... # false during drag, pan, or popup
|
||||||
|
func add_shape(kind: StringName, at_world: Vector2) -> bool: ...
|
||||||
|
func open_selected_color_picker() -> void: ...
|
||||||
|
|
||||||
|
# StickmanEditor
|
||||||
|
func request_workspace_view(view: WorkspaceView, user_chosen: bool = true) -> void: ...
|
||||||
|
func request_active_part(part_name: String) -> void: ...
|
||||||
|
func _apply_pending_presentation() -> void: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The picker and view buttons emit typed intent (`view_requested(view: int)`, `part_requested(part_name: String)` if extracted as a small reusable control). The controller validates the part ID against `BODY_PART_NAMES`. No signal bus or saved UI resource is needed for these synchronous presentation decisions.
|
||||||
|
|
||||||
|
Keep one visible action row tied to the active part in either view. **Add Shape** opens the existing Line/Rectangle/Circle choices and places the new shape at the center of that panel's visible drawing area; **Color** opens the selected shape's existing picker and is disabled with a clear reason until a shape is selected. `BodyPartPanel` exposes small typed methods for those actions and remains the owner of shape mutation. The right-click menu stays available for point editing, ordering, copy/paste, mirroring, and erase; use readable wording and a short on-canvas hint so children can discover those secondary tools. This shared row avoids repeating full 44 px toolbars inside all ten Overview panels and shrinking their drawing areas.
|
||||||
|
|
||||||
|
### 4.2 Responsive rules and safe switching
|
||||||
|
|
||||||
|
The automatic initial view is calculated after the first layout pass from available workspace width and the **Overview** content minimum: both panel columns at their useful minimum widths, container gaps, and the preview's useful minimum width. Use the same stable reference widths when the window resizes; a hidden/expanded view must not alter the threshold. If Overview fits, the default is Overview; otherwise Focus. Automatic reevaluation applies only until a child chooses either view. A manual choice remains for the session, including across later resizing and document load/clear. Do not put it in `.stk` or `user://settings.json` in this phase.
|
||||||
|
|
||||||
|
Overview keeps all ten panels reachable, with vertical and, when needed, horizontal scrolling rather than shrinking drawing areas below their usable minimums. A manual Overview choice remains available at a narrow width. Focus is available at any width. Wide Focus shows a persistent, resizable Whole Stickman preview in the split beside the enlarged selected panel. Narrow Focus starts with the preview collapsed so the drawing panel keeps its minimum width; a visible **Whole Stickman** button opens and closes it. If both pane minima cannot fit at once, opening the preview gives it the available workspace and offers a visible **Back to Part** action; no control or panel may be clipped. Keep the preview node alive so its zoom, pan, guide, selection, and part transforms survive collapse and return.
|
||||||
|
|
||||||
|
Do not reparent a panel during vertex/shape drag, pan, context menu, or color popup. Store the requested view/part and apply it when the panel reports a safe point after the gesture or popup closes. This avoids a partial geometry commit and avoids discarding a color choice. A view switch must not call `set_shape_data()`, `clear_shape()`, `reset_view()`, or `select()`; those methods change selection or view context. In particular, `set_shape_data()` currently resets zoom/pan/selection. A presentation change changes no document data, emits no shape-changed signal, and does not mark the document dirty.
|
||||||
|
|
||||||
|
### 4.3 Data and event flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
View[Overview / Focus request] --> Guard{Panel gesture or popup active?}
|
||||||
|
Guard -->|yes| Pending[Keep request pending]
|
||||||
|
Guard -->|no| Move[Reparent one live panel]
|
||||||
|
Pending -->|presentation_safe| Move
|
||||||
|
Move --> Refresh[Update picker, active cue, preview visibility]
|
||||||
|
Refresh --> Status[Document dirty state unchanged]
|
||||||
|
Edit[Draw or transform a part] --> Owner[Live BodyPartPanel / WholeStickmanPreview]
|
||||||
|
Owner --> Controller[StickmanEditor observes commit]
|
||||||
|
Controller --> Assembly[Refresh assembled preview]
|
||||||
|
Controller --> Dirty[Mark Unsaved changes]
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, when the child focuses the left lower arm, its original panel moves from `LeftColumn` to `FocusPanelHost`. Its current `shapes`, selected shape index, `_zoom`, and `_pan_offset` remain in that node. Editing still triggers the controller's existing shape-changed handler; the preview reads from the same ten-node dictionary. Returning to Overview restores that panel to its original slot with the same drawing context.
|
||||||
|
|
||||||
|
## 5. Prop Editor workspace and controls
|
||||||
|
|
||||||
|
Keep the existing `PropCanvas`, scrolling inspector, outer canvas/inspector `HSplitContainer`, and inner Attach preview split. Convert fixed `TopRow`/`TopBar` offsets into container-driven chrome so Grid/Snap, the name field, and mode controls remain visible at larger fonts and narrow sizes. Tool rows may wrap through `FlowContainer`; keep action order stable. Maintain a useful canvas minimum and allow the inspector to scroll rather than clip. Preserve the clicked control's position when a mode reveals a section or Attach preview.
|
||||||
|
|
||||||
|
Restore keyboard focus on Arrange, Reshape, and Attach mode buttons. Give the local Button theme a focus outline that composes with the pressed/selected appearance; keep selected state persistent and text stable. Programmatic refresh uses `set_pressed_no_signal()` as today. Disabled controls explain nonobvious reasons near the control or via a clear tooltip. Arrange-only and Attach-only controls retain their existing mode gating, document mutation, and preview-only behavior.
|
||||||
|
|
||||||
|
Use visible labels and 44 px targets for the primary shape/anchor operations currently hidden in context menus where layout permits. Context menus remain useful shortcuts; the common child path for adding a shape, reshaping a point, and choosing a body part should have an obvious on-screen entry. Preserve Prop's existing two-tone halo and keep its screen-space handles and hit areas distinct. Anchor and preview markers need equivalent contrast against light and dark props. Canvas artwork and material tint values remain unchanged by chrome token migration.
|
||||||
|
|
||||||
|
## 6. Copy, dialogs, and save feedback
|
||||||
|
|
||||||
|
### 6.1 Child-facing wording
|
||||||
|
|
||||||
|
| Current wording | Target copy / rule | Behavioral reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Prop `Friction: 0.xx` | **Surface grip: 0.xx**, with optional helper “Higher values make sliding harder.” | Keep the exact `PhysicsMaterial.friction` value and its surface-contact meaning; do not imply hand gripping or change physics presets. |
|
||||||
|
| Attach `Z Offset` | **Held prop layer** with nearby “Negative: behind · 0: normal order · Positive: farther in front.” Keep the −5…5 integer control and show its visual effect in the preview. | `z_index_offset` is a signed render-layer offset in both preview and held runtime behavior. Zero may draw in front in the preview due tree order, so do not relabel zero as a fixed front/behind category. |
|
||||||
|
| Destructive commands | Prefer **Erase Shape**, **Start Over**, and explicit **Discard Changes** in dialogs. For Start Over, state “Start over with a blank stickman?” and keep a separate visible **Cancel** action. | Say which authored work is affected; pair with a visible Cancel path. |
|
||||||
|
| Technical axis labels | Keep X/Y when they are editable coordinates; use plain helper text where the operation is unclear. | Avoid silently changing geometry or transform semantics while updating words. |
|
||||||
|
|
||||||
|
The Prop Editor's existing dirty title/status and autosave/recovery flow remain authoritative. Improve status wording to clear **Saved** / **Unsaved changes** states; a failed save/load keeps the current document and shows an actionable reason. Preview-only choices, Show Stickman Preview, marker visibility, and mode/view changes do not mark dirty.
|
||||||
|
|
||||||
|
The Stickman Editor adds controller-owned `_current_path: String` and `_dirty: bool`. Name changes, shape edits, shape order, color edits, and committed Whole Stickman position/rotation/scale changes mark dirty; grid/snap, guide, zoom/pan, selection, part picker, and Overview/Focus do not. Add one transform-commit signal or equivalent controller-observable path to `WholeStickmanPreview` that covers translation, rotation, and scale, because its current `part_moved` signal covers only translation. Only a successful save clears dirty; a failed save preserves it. Successful load/clear initializes the expected clean state, and save/load continue to use the existing `.stk` schema and `ContentPaths.canonicalize()` behavior.
|
||||||
|
|
||||||
|
Before Stickman **Load**, **Clear**, or window close replaces unsaved work, show the child-facing **Save / Discard Changes / Cancel** choice. Save with no usable path opens the existing Save dialog and resumes the pending action only after successful save. Cancel or a failed save leaves the document and active view untouched. Clear continues to name that it removes the current stickman. Keep Prop's current New/Close/recovery transactions intact while restyling them. Both editors' dialogs follow the dialog guide: rounded shell, readable type, blue primary action, distinct danger action, 44 px targets, visible labeled dismissal, safe viewport fit, and stable anchor when contents change. Adapt `StageDialogStyler` visual rules; its Stage overlay host is not part of either editor.
|
||||||
|
|
||||||
|
## 7. Static contracts and document boundaries
|
||||||
|
|
||||||
|
| Target | Contract | Owner / boundary |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `EditorVisualTheme : RefCounted` | `load_from(path: String) -> bool`; typed semantic color/size accessors; local `Theme` construction or copies | Validated shared chrome only; no Stage runtime dependency. |
|
||||||
|
| `PropTheme : RefCounted` | Existing material, primitive, outline, and canvas accessors; temporary UI forwards only while callers migrate | Prop-specific presentation and authoring defaults; never physics material mass/friction/bounce values. |
|
||||||
|
| `PropEditor : Control` | Existing mode, file, autosave, inspector signals and handlers; consume shared chrome tokens | Prop document mutation, Save/Load, dirty, mode control, local Theme. |
|
||||||
|
| `PropCanvas : Control` | Existing typed mode and shape/anchor signals; selection drawing uses canvas tokens | Authoring input and visual feedback. |
|
||||||
|
| `StickmanEditor : Control` | Add `class_name StickmanEditor`; `WorkspaceView`, `request_workspace_view`, `request_active_part`; `_dirty`/pending document action | One live panel dictionary, responsive presentation, `.stk` save/load, dirty state. |
|
||||||
|
| `BodyPartPanel : PanelContainer` | Existing `shape_changed(Array)` and `shape_selected()`; `can_switch_presentation() -> bool`, `presentation_safe`, `add_shape(kind: StringName, at_world: Vector2) -> bool`, `open_selected_color_picker() -> void` | One part's shapes, selection, zoom/pan, gesture/popup safety, and shape operations. |
|
||||||
|
| `WholeStickmanPreview : PanelContainer` | Existing `part_moved(String, Vector2)`; add typed `part_transform_committed(part_name: String, position: Vector2, rotation_deg: float, scale: Vector2)` or equivalent | One assembled preview and document transform commit boundary. |
|
||||||
|
|
||||||
|
No new global bus, serialized `Resource`, duplicate canvas instance, or `.prop`/`.stk` version bump is needed. UI preferences remain session state unless a later feature specifically approves persistence. Saving gathers from the ten live panels and the one live preview, independent of which view is visible.
|
||||||
|
|
||||||
|
## 8. Staged implementation slices
|
||||||
|
|
||||||
|
1. **Shared foundation.** Add the shared editor chrome theme/config and typed accessors. Set semantic roles, focus/selection/disabled states, minimum targets, and scoped theme application. Keep Prop-specific canvas/material tokens. Confirm both config and defaults match.
|
||||||
|
2. **Prop Editor pass.** Migrate Prop chrome to shared roles, fix mode-button keyboard focus, raise touch targets, make top controls and inspector fit, refine labels/help, and style existing dialogs. Preserve its file and autosave behavior.
|
||||||
|
3. **Stickman workspace pass.** Apply the theme to Stickman chrome, introduce Overview/Focus and the body-part picker, move live panel instances safely, add scrolling and narrow preview collapse, preserve drawing context, and expose active-part feedback.
|
||||||
|
4. **Shared state and recovery pass.** Align canvas contrast and action discovery, add complete Stickman dirty/unsaved transactions, and finish dialog and status consistency. Resolve remaining measured layout and contrast issues from rendered review.
|
||||||
|
|
||||||
|
Keep one spec so theme roles, view state, and save behavior are reviewed together. Each slice should leave its editor usable and should report which acceptance outcomes below it satisfies. User-guide changes follow the spec-pipeline documentation phase after the user verifies the feature in the editor.
|
||||||
|
|
||||||
|
## 9. Architectural trade-offs and risks
|
||||||
|
|
||||||
|
| Decision | Benefit | Cost / mitigation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Shared semantic editor source with local editor Themes | Consistent chrome and focus states without coupling to Stage internals | Migrate Prop's old `ui` values deliberately; prevent two active authorities for one chrome role. |
|
||||||
|
| Reparent one existing BodyPartPanel into Focus | Preserves shapes, selection, zoom/pan, signals, and current save architecture | Node movement during a gesture/popup is unsafe; defer until `presentation_safe`. Keep original parent/index map. |
|
||||||
|
| Permanent WholeStickmanPreview sibling | One source of assembly transform and preview view state | Narrow layout needs collapse/return controls and a minimum-width fallback. |
|
||||||
|
| Session-only manual view choice | A child's chosen workspace does not jump on resize | Automatic layout helps only until the first manual choice; narrow Overview requires scroll access. |
|
||||||
|
| One umbrella specification, staged slices | Prevents token, mode, and state contracts from drifting across separate documents | Each slice needs explicit acceptance accounting and user review. |
|
||||||
|
| Stickman dirty state added with transform commit | Protects edits during Load/Clear/Close | All authored mutation paths must be audited; translation-only signal is insufficient. |
|
||||||
|
|
||||||
|
Known dependencies: the untracked style guide and design-direction board are source references in this checkout; unrelated dirty Stage work must remain untouched. Theme and layout work can be implemented independently of that Stage work. Prop's in-Stage authoring mode also instantiates `PropEditor`, so chrome changes must preserve its host and hide/disable behavior. Long labels, emoji fallback, larger fonts, and small viewports may expose new minimum-size pressure; resolve by wrapping/scrolling and measured fitting, not by silently shrinking controls.
|
||||||
|
|
||||||
|
## 10. Observable acceptance and verification routes
|
||||||
|
|
||||||
|
| User path | Observable outcome | Verification route |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Open each editor at a wide window; Tab through menu, primary controls, mode/view buttons, inspector, and dialogs; activate with Space/Enter. | All focusable controls show a distinct focus ring, active mode/view remains visible under focus, and controls meet the agreed target size. | Rendered GUI capture plus real keyboard navigation at ordinary and larger font settings. |
|
||||||
|
| Open Stickman wide, select a part, use Add Shape and Color, draw in a second part, choose Focus on Part, change the focused part, then return to Overview. | The visible actions affect only the active part; Overview has all ten panels; Focus works at wide width; each part retains shapes, selected shape, zoom, and pan; preview updates from edits; no view switch marks dirty. | Interact through production controls; compare panel and preview state before/after switches and inspect rendered captures. |
|
||||||
|
| Open Stickman where Overview's measured minimum does not fit; choose Overview manually, resize wider and narrower, then choose Focus. | Focus is the initial automatic view; Overview remains reachable through scrolling; manual choice remains through resize; Focus preview collapses with visible Whole Stickman access and returns without resetting preview view state. | Rendered narrow/wide GUI session with actual resize and control input. Record viewport dimensions and pane sizes. |
|
||||||
|
| Begin a panel drag or open its color popup, request another view/part, then finish or dismiss that interaction. | The switch waits until the interaction is safe; geometry/color is neither partially committed nor lost; selected shape and pan/zoom remain. | Real input path through the panel and popup; inspect shapes and resulting selection after release/dismissal. |
|
||||||
|
| In Prop, switch Arrange/Reshape/Attach, resize, edit a shape and anchor, and open its preview. | Mode state, inspector sections, top Grid/Snap, canvas, and Attach preview remain reachable without clipped labels or jumping controls; existing document and preview-only dirty rules hold. | Rendered GUI captures of all modes at wide/narrow sizes; check actual edits and status. |
|
||||||
|
| In Prop Attach, set Held prop layer to −1, 0, and +1 and view the preview; save and reopen. | Negative places the held prop behind the rig, zero follows normal ordering, positive places it farther in front; numeric −5…5 values round-trip unchanged. | Preview capture for each value plus a small repeatable save/reopen scenario using an anchor and visible overlapping prop. Confirm runtime held ordering in a Stage scenario where a rig holds the same prop. |
|
||||||
|
| In Prop, choose a material preset and read Surface grip, then switch presets. | The displayed value equals `PhysicsMaterial.friction`; wording describes sliding resistance without changing physics values. | Inspector readback against preset values and rendered text check. |
|
||||||
|
| Edit a Stickman shape, move/rotate/scale a preview part, then Save; edit again and choose Load, Clear, or close. | Every authored mutation sets Unsaved changes; successful Save clears it; Save/Discard Changes/Cancel protects the current document and resumes only the chosen action. | Small repeatable `.stk` scenario: draw and transform two parts, save, reload, compare shape/transform data, then exercise each unsaved branch. Measure status and file content before/after. |
|
||||||
|
| Open save/load/recovery/clear/error dialogs; enlarge type or shorten the viewport. | Primary/destructive actions, visible Cancel/Back/Close, message, and focused control remain accessible; content scrolls or fits without abrupt recentering after a choice. | Rendered dialog states and real pointer/keyboard interactions; capture normal, error, and destructive branches. |
|
||||||
|
| Select artwork with light and dark fills in each editor, drag handles, and use the pose guide/Attach markers. | Selection, active handle, guide, and markers remain legible over both fills and at working zoom; grid stays quieter than the artwork. | Rendered pixel inspection at named scenes/states, with a contrast/color-vision check for text and UI state. |
|
||||||
|
|
||||||
|
Headless parsing or a successful scene launch alone does not establish these visual or interaction outcomes. During implementation, run targeted Godot import/parse and relevant existing regressions, then use a render-capable editor session and real controls for the visual paths. Record scene/state, viewport size, capture location, observed result, and any outcome that could not be verified. Stop for user verification after agent evidence is complete; update user guides only after the user confirms the feature works.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"chrome": {
|
||||||
|
"colors": {
|
||||||
|
"background": "#171B24F2",
|
||||||
|
"surface": "#252B38FA",
|
||||||
|
"surface_hover": "#30394A",
|
||||||
|
"selected_surface": "#103B4D",
|
||||||
|
"text": "#F5F7FA",
|
||||||
|
"text_muted": "#AAB2C0",
|
||||||
|
"edit": "#22C6FF",
|
||||||
|
"preview": "#22C6FF66",
|
||||||
|
"focus": "#8BD9FF",
|
||||||
|
"action_primary": "#087DBA",
|
||||||
|
"destructive": "#E66B75",
|
||||||
|
"border": "#FFFFFF33",
|
||||||
|
"disabled": "#77808F",
|
||||||
|
"saved": "#8FD6A4",
|
||||||
|
"dirty": "#FFD166",
|
||||||
|
"error": "#FF8A8A"
|
||||||
|
},
|
||||||
|
"fonts": {
|
||||||
|
"body": 18,
|
||||||
|
"helper": 16,
|
||||||
|
"section": 22,
|
||||||
|
"title": 26,
|
||||||
|
"menu": 20,
|
||||||
|
"menu_bar": 22,
|
||||||
|
"menu_popup": 26,
|
||||||
|
"status": 16,
|
||||||
|
"mode": 24,
|
||||||
|
"prop_name": 20,
|
||||||
|
"context_menu": 20
|
||||||
|
},
|
||||||
|
"spacing": { "small": 8, "medium": 12, "large": 16 },
|
||||||
|
"radii": { "control": 8, "panel": 10 },
|
||||||
|
"borders": { "normal": 1, "focus": 2 },
|
||||||
|
"targets": {
|
||||||
|
"button_min_height": 44,
|
||||||
|
"row_min_height": 44,
|
||||||
|
"mode_button_min_height": 56,
|
||||||
|
"context_menu_row_height": 48,
|
||||||
|
"context_menu_min_width": 208
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editors": { "prop": {}, "stickman": {} },
|
||||||
|
"stickman_canvas": {
|
||||||
|
"grid_color": "#FFFFFF26",
|
||||||
|
"selection_core": "#FFFFFF",
|
||||||
|
"selection_halo": "#000000D9"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,29 +20,5 @@
|
|||||||
"grid_size": 15,
|
"grid_size": 15,
|
||||||
"background_color": "#1F1F1F",
|
"background_color": "#1F1F1F",
|
||||||
"grid_color": "#FFFFFF26"
|
"grid_color": "#FFFFFF26"
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"fonts": {
|
|
||||||
"body": 18,
|
|
||||||
"header": 24,
|
|
||||||
"menu": 22,
|
|
||||||
"menu_bar": 22,
|
|
||||||
"mode": 26,
|
|
||||||
"status": 18,
|
|
||||||
"prop_name": 20,
|
|
||||||
"context_menu": 22
|
|
||||||
},
|
|
||||||
"hit_targets": {
|
|
||||||
"button_min_height": 36,
|
|
||||||
"row_min_height": 36,
|
|
||||||
"mode_button_min_height": 56
|
|
||||||
},
|
|
||||||
"colors": {
|
|
||||||
"text": "#F2F2F2",
|
|
||||||
"text_muted": "#9A9A9A",
|
|
||||||
"border": "#FFFFFF33",
|
|
||||||
"accent": "#4FA3FF",
|
|
||||||
"inspector_background": "#262C39"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,9 +101,9 @@
|
|||||||
"queue_size": [680, 520],
|
"queue_size": [680, 520],
|
||||||
"rule_size": [720, 560],
|
"rule_size": [720, 560],
|
||||||
"action_size": [560, 390],
|
"action_size": [560, 390],
|
||||||
"confirm_size": [620, 300],
|
"confirm_size": [560, 300],
|
||||||
"font_size": 22,
|
"font_size": 22,
|
||||||
"message_font_size": 26
|
"message_font_size": 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"popups": {
|
"popups": {
|
||||||
@@ -122,27 +122,59 @@
|
|||||||
},
|
},
|
||||||
"confirmation": {
|
"confirmation": {
|
||||||
"scenery_erase_size": [520, 208],
|
"scenery_erase_size": [520, 208],
|
||||||
"padding": 18,
|
"padding": 22,
|
||||||
"body_gap": 10,
|
"body_gap": 12,
|
||||||
"footer_gap": 12,
|
"footer_gap": 12,
|
||||||
"corner_radius": 16,
|
"corner_radius": 18,
|
||||||
"border_width": 2,
|
"border_width": 2,
|
||||||
"title_font_size": 26,
|
"title_font_size": 28,
|
||||||
"message_font_size": 20,
|
"message_font_size": 20,
|
||||||
"button_font_size": 20,
|
"button_font_size": 20,
|
||||||
"button_height": 48,
|
"button_height": 52,
|
||||||
|
"button_min_width": 112,
|
||||||
|
"card_width": 560,
|
||||||
"button_radius": 12,
|
"button_radius": 12,
|
||||||
"button_border_width": 2,
|
"button_border_width": 2,
|
||||||
"remove_button_width": 170,
|
"remove_button_width": 170,
|
||||||
"keep_button_width": 132,
|
"keep_button_width": 160,
|
||||||
"surface_color": "#202633FA",
|
"surface_color": "#202633FA",
|
||||||
"border_color": "#22C6FF",
|
"border_color": "#667386",
|
||||||
"accent_color": "#22C6FF",
|
"secondary_surface": "#293242",
|
||||||
|
"secondary_border": "#718096",
|
||||||
|
"focus_color": "#8BD9FF",
|
||||||
|
"backdrop_color": "#080D15B8",
|
||||||
|
"accent_color": "#8BD9FF",
|
||||||
"action_color": "#087DBA",
|
"action_color": "#087DBA",
|
||||||
"danger_color": "#FF626B",
|
"danger_color": "#E66B75",
|
||||||
|
"danger_surface": "#44262D",
|
||||||
"danger_hover_surface": "#3B252C",
|
"danger_hover_surface": "#3B252C",
|
||||||
"danger_pressed_surface": "#4A252D"
|
"danger_pressed_surface": "#4A252D"
|
||||||
},
|
},
|
||||||
|
"ground_finish_chooser": {
|
||||||
|
"card_width": 880,
|
||||||
|
"max_height_ratio": 0.86,
|
||||||
|
"screen_margin": 24,
|
||||||
|
"choice_height": 96,
|
||||||
|
"choice_preview_height": 48,
|
||||||
|
"choice_font_size": 18,
|
||||||
|
"choice_min_width": 104,
|
||||||
|
"choice_corner_radius": 12,
|
||||||
|
"compact_choice_height": 50,
|
||||||
|
"compact_choice_preview_height": 22,
|
||||||
|
"compact_choice_font_size": 16,
|
||||||
|
"compact_grid_gap": 6,
|
||||||
|
"compact_layout_max_width": 600,
|
||||||
|
"compact_layout_max_height": 460,
|
||||||
|
"compact_height_ratio": 0.89,
|
||||||
|
"preview_height": 128,
|
||||||
|
"compact_preview_height": 88,
|
||||||
|
"preview_min_width": 224,
|
||||||
|
"preview_corner_radius": 12,
|
||||||
|
"preview_surface": "#151C28",
|
||||||
|
"preview_border": "#39465B",
|
||||||
|
"grid_gap": 8,
|
||||||
|
"wide_layout_min_width": 760
|
||||||
|
},
|
||||||
"asset_selector": {
|
"asset_selector": {
|
||||||
"backdrop_alpha": 0.55,
|
"backdrop_alpha": 0.55,
|
||||||
"cell_size": [190, 220],
|
"cell_size": [190, 220],
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
[gd_scene load_steps=4 format=3]
|
[gd_scene load_steps=3 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/editors/prop/prop_editor.gd" id="1_editor_script"]
|
[ext_resource type="Script" path="res://scripts/editors/prop/prop_editor.gd" id="1_editor_script"]
|
||||||
[ext_resource type="Script" path="res://scripts/editors/prop/prop_canvas.gd" id="2_canvas_script"]
|
[ext_resource type="Script" path="res://scripts/editors/prop/prop_canvas.gd" id="2_canvas_script"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_canvas"]
|
|
||||||
bg_color = Color(0.121569, 0.121569, 0.121569, 1)
|
|
||||||
border_width_left = 1
|
|
||||||
border_width_top = 1
|
|
||||||
border_width_right = 1
|
|
||||||
border_width_bottom = 1
|
|
||||||
border_color = Color(0.3, 0.3, 0.3, 1)
|
|
||||||
corner_radius_top_left = 4
|
|
||||||
corner_radius_top_right = 4
|
|
||||||
corner_radius_bottom_right = 4
|
|
||||||
corner_radius_bottom_left = 4
|
|
||||||
|
|
||||||
[node name="PropEditor" type="Control"]
|
[node name="PropEditor" type="Control"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
@@ -24,91 +12,89 @@ grow_horizontal = 2
|
|||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("1_editor_script")
|
script = ExtResource("1_editor_script")
|
||||||
|
|
||||||
[node name="TopRow" type="HBoxContainer" parent="."]
|
[node name="ChromeRoot" type="VBoxContainer" parent="."]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 0
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
offset_bottom = 44.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
[node name="MenuBar" type="MenuBar" parent="TopRow"]
|
[node name="TopRow" type="FlowContainer" parent="ChromeRoot"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 44)
|
||||||
|
theme_override_constants/h_separation = 8
|
||||||
|
theme_override_constants/v_separation = 4
|
||||||
|
|
||||||
|
[node name="MenuBar" type="MenuBar" parent="ChromeRoot/TopRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TopRightControls" type="FlowContainer" parent="ChromeRoot/TopRow"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/h_separation = 8
|
||||||
|
theme_override_constants/v_separation = 4
|
||||||
|
|
||||||
[node name="TopRightControls" type="HBoxContainer" parent="TopRow"]
|
[node name="TopBar" type="HBoxContainer" parent="ChromeRoot"]
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 8
|
custom_minimum_size = Vector2(0, 44)
|
||||||
|
|
||||||
[node name="TopBar" type="HBoxContainer" parent="."]
|
[node name="NameLabel" type="Label" parent="ChromeRoot/TopBar"]
|
||||||
layout_mode = 1
|
|
||||||
anchors_preset = 0
|
|
||||||
anchor_right = 1.0
|
|
||||||
offset_top = 44.0
|
|
||||||
offset_bottom = 88.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
|
|
||||||
[node name="NameLabel" type="Label" parent="TopBar"]
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.85, 0.85, 0.85, 1)
|
|
||||||
text = "Prop Name:"
|
text = "Prop Name:"
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
[node name="PropNameEdit" type="LineEdit" parent="TopBar"]
|
[node name="PropNameEdit" type="LineEdit" parent="ChromeRoot/TopBar"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
placeholder_text = "Unnamed Prop"
|
placeholder_text = "Unnamed Prop"
|
||||||
|
|
||||||
[node name="MainLayout" type="HSplitContainer" parent="."]
|
[node name="MainLayout" type="HSplitContainer" parent="ChromeRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 1
|
layout_mode = 2
|
||||||
anchors_preset = 15
|
size_flags_vertical = 3
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
offset_top = 92.0
|
|
||||||
offset_bottom = -28.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
[node name="CanvasPanel" type="PanelContainer" parent="MainLayout"]
|
[node name="CanvasPanel" type="PanelContainer" parent="ChromeRoot/MainLayout"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
size_flags_stretch_ratio = 2.0
|
size_flags_stretch_ratio = 2.0
|
||||||
custom_minimum_size = Vector2(260, 0)
|
custom_minimum_size = Vector2(260, 0)
|
||||||
theme_override_styles/panel = SubResource("StyleBox_canvas")
|
|
||||||
|
|
||||||
[node name="CanvasArea" type="VBoxContainer" parent="MainLayout/CanvasPanel"]
|
[node name="CanvasArea" type="VBoxContainer" parent="ChromeRoot/MainLayout/CanvasPanel"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 4
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
[node name="ModeBar" type="HBoxContainer" parent="MainLayout/CanvasPanel/CanvasArea"]
|
[node name="ModeBar" type="FlowContainer" parent="ChromeRoot/MainLayout/CanvasPanel/CanvasArea"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/h_separation = 8
|
||||||
|
theme_override_constants/v_separation = 4
|
||||||
|
|
||||||
|
[node name="SizeTools" type="FlowContainer" parent="ChromeRoot/MainLayout/CanvasPanel/CanvasArea"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 8
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
[node name="SizeTools" type="FlowContainer" parent="MainLayout/CanvasPanel/CanvasArea"]
|
[node name="AttachTools" type="FlowContainer" parent="ChromeRoot/MainLayout/CanvasPanel/CanvasArea"]
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_constants/separation = 8
|
|
||||||
|
|
||||||
[node name="AttachTools" type="FlowContainer" parent="MainLayout/CanvasPanel/CanvasArea"]
|
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 12
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
[node name="CanvasWorkspace" type="HSplitContainer" parent="MainLayout/CanvasPanel/CanvasArea"]
|
[node name="CanvasWorkspace" type="HSplitContainer" parent="ChromeRoot/MainLayout/CanvasPanel/CanvasArea"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
split_offset = 520
|
split_offset = 520
|
||||||
|
|
||||||
[node name="PropCanvas" type="Control" parent="MainLayout/CanvasPanel/CanvasArea/CanvasWorkspace"]
|
[node name="PropCanvas" type="Control" parent="ChromeRoot/MainLayout/CanvasPanel/CanvasArea/CanvasWorkspace"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
@@ -117,18 +103,18 @@ mouse_filter = 0
|
|||||||
clip_contents = true
|
clip_contents = true
|
||||||
script = ExtResource("2_canvas_script")
|
script = ExtResource("2_canvas_script")
|
||||||
|
|
||||||
[node name="InspectorColumn" type="PanelContainer" parent="MainLayout"]
|
[node name="InspectorColumn" type="PanelContainer" parent="ChromeRoot/MainLayout"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
custom_minimum_size = Vector2(300, 0)
|
custom_minimum_size = Vector2(300, 0)
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
[node name="InspectorScroll" type="ScrollContainer" parent="MainLayout/InspectorColumn"]
|
[node name="InspectorScroll" type="ScrollContainer" parent="ChromeRoot/MainLayout/InspectorColumn"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
|
|
||||||
[node name="InspectorList" type="VBoxContainer" parent="MainLayout/InspectorColumn/InspectorScroll"]
|
[node name="InspectorList" type="VBoxContainer" parent="ChromeRoot/MainLayout/InspectorColumn/InspectorScroll"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
@@ -154,18 +140,12 @@ filters = PackedStringArray("*.prop ; Prop Files")
|
|||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "Error"
|
title = "Error"
|
||||||
|
|
||||||
[node name="StatusBar" type="HBoxContainer" parent="."]
|
[node name="StatusBar" type="HBoxContainer" parent="ChromeRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 1
|
layout_mode = 2
|
||||||
anchor_left = 0.0
|
custom_minimum_size = Vector2(0, 28)
|
||||||
anchor_top = 1.0
|
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
offset_top = -28.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 0
|
|
||||||
|
|
||||||
[node name="SaveStatus" type="Label" parent="StatusBar"]
|
[node name="SaveStatus" type="Label" parent="ChromeRoot/StatusBar"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
@@ -173,9 +153,12 @@ text = ""
|
|||||||
horizontal_alignment = 0
|
horizontal_alignment = 0
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
[node name="CursorCoords" type="Label" parent="StatusBar"]
|
[node name="CursorCoords" type="Label" parent="ChromeRoot/StatusBar"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = ""
|
text = ""
|
||||||
horizontal_alignment = 2
|
horizontal_alignment = 2
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="StageDialogLayer" type="CanvasLayer" parent="."]
|
||||||
|
layer = 100
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
[gd_scene load_steps=4 format=3]
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/editors/stickman/body_part_panel.gd" id="1_panel_script"]
|
[ext_resource type="Script" path="res://scripts/editors/stickman/body_part_panel.gd" id="1_panel_script"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/shared/editor_ui/editor_context_menu.gd" id="2_context_menu_script"]
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_panel"]
|
|
||||||
bg_color = Color(0.15, 0.15, 0.15, 1)
|
|
||||||
border_width_left = 1
|
|
||||||
border_width_top = 1
|
|
||||||
border_width_right = 1
|
|
||||||
border_width_bottom = 1
|
|
||||||
border_color = Color(0.3, 0.3, 0.3, 1)
|
|
||||||
corner_radius_top_left = 4
|
|
||||||
corner_radius_top_right = 4
|
|
||||||
corner_radius_bottom_right = 4
|
|
||||||
corner_radius_bottom_left = 4
|
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_drawing"]
|
[sub_resource type="StyleBoxFlat" id="StyleBox_drawing"]
|
||||||
bg_color = Color(0.12, 0.12, 0.12, 1)
|
bg_color = Color(0.12, 0.12, 0.12, 1)
|
||||||
@@ -26,8 +15,8 @@ border_color = Color(0.25, 0.25, 0.25, 1)
|
|||||||
custom_minimum_size = Vector2(230, 160)
|
custom_minimum_size = Vector2(230, 160)
|
||||||
offset_right = 230.0
|
offset_right = 230.0
|
||||||
offset_bottom = 160.0
|
offset_bottom = 160.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
theme_override_styles/panel = SubResource("StyleBox_panel")
|
|
||||||
script = ExtResource("1_panel_script")
|
script = ExtResource("1_panel_script")
|
||||||
|
|
||||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
@@ -42,8 +31,6 @@ unique_name_in_owner = true
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "Body Part"
|
text = "Body Part"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
theme_override_colors/font_color = Color(0.85, 0.85, 0.85, 1)
|
|
||||||
theme_override_font_sizes/font_size = 13
|
|
||||||
|
|
||||||
[node name="DrawingArea" type="Control" parent="VBoxContainer"]
|
[node name="DrawingArea" type="Control" parent="VBoxContainer"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
@@ -53,9 +40,9 @@ mouse_filter = 1
|
|||||||
clip_contents = true
|
clip_contents = true
|
||||||
theme_override_styles/panel = SubResource("StyleBox_drawing")
|
theme_override_styles/panel = SubResource("StyleBox_drawing")
|
||||||
|
|
||||||
[node name="ContextMenu" type="PopupMenu" parent="."]
|
[node name="ContextMenu" type="PopupPanel" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
item_count = 0
|
script = ExtResource("2_context_menu_script")
|
||||||
|
|
||||||
[node name="ColorPickerPopup" type="PopupPanel" parent="."]
|
[node name="ColorPickerPopup" type="PopupPanel" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
@@ -70,6 +57,7 @@ layout_mode = 2
|
|||||||
custom_minimum_size = Vector2(300, 300)
|
custom_minimum_size = Vector2(300, 300)
|
||||||
|
|
||||||
[node name="ButtonRow" type="HBoxContainer" parent="ColorPickerPopup/VBoxContainer"]
|
[node name="ButtonRow" type="HBoxContainer" parent="ColorPickerPopup/VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
alignment = 2
|
alignment = 2
|
||||||
|
|
||||||
@@ -81,4 +69,4 @@ text = "Cancel"
|
|||||||
[node name="OKButton" type="Button" parent="ColorPickerPopup/VBoxContainer/ButtonRow"]
|
[node name="OKButton" type="Button" parent="ColorPickerPopup/VBoxContainer/ButtonRow"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "OK"
|
text = "Use Color"
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://dlacmxk257ro" path="res://scripts/editors/stickman/stickman_editor.gd" id="1_editor_script"]
|
[ext_resource type="Script" uid="uid://dlacmxk257ro" path="res://scripts/editors/stickman/stickman_editor.gd" id="1_editor_script"]
|
||||||
[ext_resource type="Script" uid="uid://codv3g6qw1tef" path="res://scripts/editors/stickman/whole_stickman_preview.gd" id="2_preview_script"]
|
[ext_resource type="Script" uid="uid://codv3g6qw1tef" path="res://scripts/editors/stickman/whole_stickman_preview.gd" id="2_preview_script"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/shared/editor_ui/editor_selection_dropdown.gd" id="3_selection_dropdown_script"]
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_preview"]
|
|
||||||
bg_color = Color(0.12, 0.12, 0.12, 1)
|
|
||||||
border_width_left = 1
|
|
||||||
border_width_top = 1
|
|
||||||
border_width_right = 1
|
|
||||||
border_width_bottom = 1
|
|
||||||
border_color = Color(0.3, 0.3, 0.3, 1)
|
|
||||||
corner_radius_top_left = 4
|
|
||||||
corner_radius_top_right = 4
|
|
||||||
corner_radius_bottom_right = 4
|
|
||||||
corner_radius_bottom_left = 4
|
|
||||||
|
|
||||||
[node name="StickmanEditor" type="Control"]
|
[node name="StickmanEditor" type="Control"]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
@@ -24,85 +13,196 @@ grow_horizontal = 2
|
|||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("1_editor_script")
|
script = ExtResource("1_editor_script")
|
||||||
|
|
||||||
[node name="MenuBar" type="MenuBar" parent="."]
|
[node name="ChromeRoot" type="VBoxContainer" parent="."]
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 1
|
|
||||||
anchors_preset = 0
|
|
||||||
anchor_right = 1.0
|
|
||||||
offset_bottom = 32.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
|
|
||||||
[node name="TopBar" type="HBoxContainer" parent="."]
|
|
||||||
layout_mode = 1
|
|
||||||
anchors_preset = 0
|
|
||||||
anchor_right = 1.0
|
|
||||||
offset_top = 32.0
|
|
||||||
offset_bottom = 64.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
|
|
||||||
[node name="NameLabel" type="Label" parent="TopBar"]
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_colors/font_color = Color(0.85, 0.85, 0.85, 1)
|
|
||||||
text = "Stickman Name:"
|
|
||||||
vertical_alignment = 1
|
|
||||||
|
|
||||||
[node name="StickmanNameEdit" type="LineEdit" parent="TopBar"]
|
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 2
|
|
||||||
size_flags_horizontal = 3
|
|
||||||
placeholder_text = "Unnamed Stickman"
|
|
||||||
|
|
||||||
[node name="MainLayout" type="HBoxContainer" parent="."]
|
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
offset_top = 68.0
|
|
||||||
offset_bottom = -28.0
|
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
[node name="LeftColumn" type="VBoxContainer" parent="MainLayout"]
|
[node name="MenuBar" type="MenuBar" parent="ChromeRoot"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 44)
|
||||||
|
|
||||||
|
[node name="TopBar" type="HBoxContainer" parent="ChromeRoot"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="NameLabel" type="Label" parent="ChromeRoot/TopBar"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Stickman Name:"
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="StickmanNameEdit" type="LineEdit" parent="ChromeRoot/TopBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
custom_minimum_size = Vector2(180, 44)
|
||||||
|
placeholder_text = "Unnamed Stickman"
|
||||||
|
|
||||||
|
[node name="WorkspaceToolbar" type="FlowContainer" parent="ChromeRoot"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/h_separation = 8
|
||||||
|
theme_override_constants/v_separation = 4
|
||||||
|
|
||||||
|
[node name="OverviewButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Overview"
|
||||||
|
toggle_mode = true
|
||||||
|
|
||||||
|
[node name="FocusButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Focus on Part"
|
||||||
|
toggle_mode = true
|
||||||
|
|
||||||
|
[node name="PartPicker" type="MenuButton" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
script = ExtResource("3_selection_dropdown_script")
|
||||||
|
|
||||||
|
[node name="AddShapeButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Add Shape"
|
||||||
|
|
||||||
|
[node name="ColorButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Color"
|
||||||
|
disabled = true
|
||||||
|
|
||||||
|
[node name="WholeStickmanButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Whole Stickman"
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BackToPartButton" type="Button" parent="ChromeRoot/WorkspaceToolbar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Back to Part"
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="WorkspaceRow" type="HSplitContainer" parent="ChromeRoot"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="BodyPartWorkspace" type="Control" parent="ChromeRoot/WorkspaceRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
custom_minimum_size = Vector2(350, 260)
|
||||||
|
|
||||||
|
[node name="OverviewScroll" type="ScrollContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
horizontal_scroll_mode = 1
|
||||||
|
vertical_scroll_mode = 1
|
||||||
|
|
||||||
|
[node name="OverviewColumns" type="HBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/OverviewScroll"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="LeftColumn" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/OverviewScroll/OverviewColumns"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
[node name="CenterColumn" type="VBoxContainer" parent="MainLayout"]
|
[node name="CenterColumn" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/OverviewScroll/OverviewColumns"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
[node name="WholeStickmanPreview" type="PanelContainer" parent="MainLayout"]
|
[node name="FocusHost" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
visible = false
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="FocusPanelHost" type="Control" parent="ChromeRoot/WorkspaceRow/BodyPartWorkspace/FocusHost"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
clip_contents = true
|
||||||
|
|
||||||
|
[node name="WholeStickmanPreview" type="PanelContainer" parent="ChromeRoot/WorkspaceRow"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
size_flags_stretch_ratio = 2.0
|
custom_minimum_size = Vector2(320, 260)
|
||||||
theme_override_styles/panel = SubResource("StyleBox_preview")
|
|
||||||
script = ExtResource("2_preview_script")
|
script = ExtResource("2_preview_script")
|
||||||
|
|
||||||
[node name="VBoxContainer" type="VBoxContainer" parent="MainLayout/WholeStickmanPreview"]
|
[node name="PreviewColumn" type="VBoxContainer" parent="ChromeRoot/WorkspaceRow/WholeStickmanPreview"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="PreviewTitle" type="Label" parent="MainLayout/WholeStickmanPreview/VBoxContainer"]
|
[node name="PreviewTitle" type="Label" parent="ChromeRoot/WorkspaceRow/WholeStickmanPreview/PreviewColumn"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.85, 0.85, 0.85, 1)
|
|
||||||
theme_override_font_sizes/font_size = 14
|
|
||||||
text = "Whole Stickman"
|
text = "Whole Stickman"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="PreviewArea" type="Control" parent="MainLayout/WholeStickmanPreview/VBoxContainer"]
|
[node name="PreviewArea" type="Control" parent="ChromeRoot/WorkspaceRow/WholeStickmanPreview/PreviewColumn"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
mouse_filter = 1
|
mouse_filter = 1
|
||||||
clip_contents = true
|
clip_contents = true
|
||||||
|
|
||||||
|
[node name="StatusBar" type="HBoxContainer" parent="ChromeRoot"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 32)
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="CursorCoords" type="Label" parent="ChromeRoot/StatusBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = ""
|
||||||
|
horizontal_alignment = 0
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="SaveStatus" type="Label" parent="ChromeRoot/StatusBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "New stickman"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="SnapStatus" type="Label" parent="ChromeRoot/StatusBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "SNAP: OFF"
|
||||||
|
horizontal_alignment = 2
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
[node name="SaveDialog" type="FileDialog" parent="."]
|
[node name="SaveDialog" type="FileDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "Save Stickman"
|
title = "Save Stickman"
|
||||||
@@ -112,7 +212,7 @@ filters = PackedStringArray("*.stk ; Stickman Files")
|
|||||||
|
|
||||||
[node name="LoadDialog" type="FileDialog" parent="."]
|
[node name="LoadDialog" type="FileDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "Open a File"
|
title = "Open a Stickman"
|
||||||
size = Vector2i(600, 450)
|
size = Vector2i(600, 450)
|
||||||
ok_button_text = "Open"
|
ok_button_text = "Open"
|
||||||
file_mode = 0
|
file_mode = 0
|
||||||
@@ -121,19 +221,24 @@ filters = PackedStringArray("*.stk ; Stickman Files")
|
|||||||
|
|
||||||
[node name="ClearConfirmDialog" type="ConfirmationDialog" parent="."]
|
[node name="ClearConfirmDialog" type="ConfirmationDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "Clear Stickman"
|
title = "Start Over?"
|
||||||
ok_button_text = "Clear"
|
ok_button_text = "Start Over"
|
||||||
dialog_text = "Are you sure you want to clear the current stickman?
|
dialog_text = "This will clear your current stickman. Start over?"
|
||||||
This action cannot be undone."
|
|
||||||
|
[node name="UnsavedChangesDialog" type="ConfirmationDialog" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
title = "Unsaved Changes"
|
||||||
|
ok_button_text = "Save"
|
||||||
|
dialog_text = "Save changes before continuing?"
|
||||||
|
|
||||||
[node name="ErrorDialog" type="AcceptDialog" parent="."]
|
[node name="ErrorDialog" type="AcceptDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "Error"
|
title = "Could Not Complete"
|
||||||
|
|
||||||
[node name="GridConfigDialog" type="ConfirmationDialog" parent="."]
|
[node name="GridConfigDialog" type="ConfirmationDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "Configure Grid"
|
title = "Configure Grid"
|
||||||
ok_button_text = "OK"
|
ok_button_text = "Done"
|
||||||
|
|
||||||
[node name="VBoxContainer" type="VBoxContainer" parent="GridConfigDialog"]
|
[node name="VBoxContainer" type="VBoxContainer" parent="GridConfigDialog"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
@@ -145,34 +250,12 @@ text = "Grid Size (pixels):"
|
|||||||
[node name="GridSizeSpinBox" type="SpinBox" parent="GridConfigDialog/VBoxContainer"]
|
[node name="GridSizeSpinBox" type="SpinBox" parent="GridConfigDialog/VBoxContainer"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 44)
|
||||||
min_value = 1.0
|
min_value = 1.0
|
||||||
max_value = 100.0
|
max_value = 100.0
|
||||||
value = 15.0
|
value = 15.0
|
||||||
step = 1.0
|
step = 1.0
|
||||||
rounded = true
|
rounded = true
|
||||||
|
|
||||||
[node name="StatusBar" type="HBoxContainer" parent="."]
|
[node name="StageDialogLayer" type="CanvasLayer" parent="."]
|
||||||
unique_name_in_owner = true
|
layer = 100
|
||||||
layout_mode = 1
|
|
||||||
anchor_left = 0.0
|
|
||||||
anchor_top = 1.0
|
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
offset_top = -28.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 0
|
|
||||||
|
|
||||||
[node name="CursorCoords" type="Label" parent="StatusBar"]
|
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 2
|
|
||||||
size_flags_horizontal = 3
|
|
||||||
text = ""
|
|
||||||
horizontal_alignment = 0
|
|
||||||
vertical_alignment = 1
|
|
||||||
|
|
||||||
[node name="SnapStatus" type="Label" parent="StatusBar"]
|
|
||||||
unique_name_in_owner = true
|
|
||||||
layout_mode = 2
|
|
||||||
text = "SNAP: OFF"
|
|
||||||
horizontal_alignment = 2
|
|
||||||
vertical_alignment = 1
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
|||||||
const PropAnchorOverlayScript := preload("res://scripts/props/prop_anchor_overlay.gd")
|
const PropAnchorOverlayScript := preload("res://scripts/props/prop_anchor_overlay.gd")
|
||||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||||
const WholeStickmanPreviewScript := preload("res://scripts/editors/stickman/whole_stickman_preview.gd")
|
const WholeStickmanPreviewScript := preload("res://scripts/editors/stickman/whole_stickman_preview.gd")
|
||||||
|
const EditorContextMenuScript := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||||
|
|
||||||
const SELECTION_CORE_COLOR := Color.WHITE
|
const SELECTION_CORE_COLOR := Color.WHITE
|
||||||
const SELECTION_HALO_COLOR := Color(0.0, 0.0, 0.0, 0.85)
|
const SELECTION_HALO_COLOR := Color(0.0, 0.0, 0.0, 0.85)
|
||||||
@@ -121,7 +122,8 @@ var _pan_offset: Vector2 = Vector2.ZERO
|
|||||||
var _grid_size: int = 15
|
var _grid_size: int = 15
|
||||||
var _snap_enabled: bool = false
|
var _snap_enabled: bool = false
|
||||||
var _has_clipboard: bool = false
|
var _has_clipboard: bool = false
|
||||||
var _context_menu: PopupMenu
|
var _create_point_armed: bool = false
|
||||||
|
var _context_menu: EditorContextMenu
|
||||||
var _edit_mode: EditMode = EditMode.TRANSFORM
|
var _edit_mode: EditMode = EditMode.TRANSFORM
|
||||||
var _anchor_overlay: RefCounted
|
var _anchor_overlay: RefCounted
|
||||||
var _origin_marker_color: Color = ORIGIN_MARKER_DEFAULT_COLOR
|
var _origin_marker_color: Color = ORIGIN_MARKER_DEFAULT_COLOR
|
||||||
@@ -188,7 +190,7 @@ func _ready() -> void:
|
|||||||
_anchor_overlay = PropAnchorOverlayScript.new()
|
_anchor_overlay = PropAnchorOverlayScript.new()
|
||||||
_anchor_overlay.anchors_changed.connect(func(data: Dictionary) -> void: anchors_changed.emit(data); queue_redraw())
|
_anchor_overlay.anchors_changed.connect(func(data: Dictionary) -> void: anchors_changed.emit(data); queue_redraw())
|
||||||
_anchor_overlay.anchor_selected.connect(func(anchor_id: String) -> void: anchor_selected.emit(anchor_id); queue_redraw())
|
_anchor_overlay.anchor_selected.connect(func(anchor_id: String) -> void: anchor_selected.emit(anchor_id); queue_redraw())
|
||||||
_context_menu = PopupMenu.new()
|
_context_menu = EditorContextMenuScript.new() as EditorContextMenu
|
||||||
_context_menu.name = "ContextMenu"
|
_context_menu.name = "ContextMenu"
|
||||||
add_child(_context_menu)
|
add_child(_context_menu)
|
||||||
if not _context_menu.id_pressed.is_connected(_on_context_menu_id_pressed):
|
if not _context_menu.id_pressed.is_connected(_on_context_menu_id_pressed):
|
||||||
@@ -214,6 +216,8 @@ func set_edit_mode(mode: int) -> void:
|
|||||||
if _stage_size_preview_active:
|
if _stage_size_preview_active:
|
||||||
cancel_stage_size_preview()
|
cancel_stage_size_preview()
|
||||||
_edit_mode = m
|
_edit_mode = m
|
||||||
|
if _edit_mode != EditMode.POINTS:
|
||||||
|
_create_point_armed = false
|
||||||
if _edit_mode != EditMode.TRANSFORM:
|
if _edit_mode != EditMode.TRANSFORM:
|
||||||
set_whole_prop_resize_enabled(false)
|
set_whole_prop_resize_enabled(false)
|
||||||
_drag_mode = DragMode.NONE
|
_drag_mode = DragMode.NONE
|
||||||
@@ -225,6 +229,21 @@ func set_edit_mode(mode: int) -> void:
|
|||||||
func get_edit_mode() -> int:
|
func get_edit_mode() -> int:
|
||||||
return _edit_mode
|
return _edit_mode
|
||||||
|
|
||||||
|
|
||||||
|
func arm_create_point() -> void:
|
||||||
|
set_edit_mode(EditMode.POINTS)
|
||||||
|
_create_point_armed = true
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func cancel_create_point() -> void:
|
||||||
|
_create_point_armed = false
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func is_create_point_armed() -> bool:
|
||||||
|
return _create_point_armed
|
||||||
|
|
||||||
func set_anchor_data(data: Variant) -> void:
|
func set_anchor_data(data: Variant) -> void:
|
||||||
if _anchor_overlay == null: _anchor_overlay = PropAnchorOverlayScript.new()
|
if _anchor_overlay == null: _anchor_overlay = PropAnchorOverlayScript.new()
|
||||||
_anchor_overlay.set_anchor_data(data)
|
_anchor_overlay.set_anchor_data(data)
|
||||||
@@ -965,7 +984,12 @@ func set_has_clipboard(state: bool) -> void:
|
|||||||
## Sets the font size of the right-click context menu (Round 4, theme-driven).
|
## Sets the font size of the right-click context menu (Round 4, theme-driven).
|
||||||
func set_context_menu_font_size(size: int) -> void:
|
func set_context_menu_font_size(size: int) -> void:
|
||||||
if _context_menu != null:
|
if _context_menu != null:
|
||||||
_context_menu.add_theme_font_size_override("font_size", size)
|
_context_menu.set_font_size(size)
|
||||||
|
|
||||||
|
|
||||||
|
func set_context_menu_visual_theme(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||||
|
if _context_menu != null:
|
||||||
|
_context_menu.configure(visual_theme, editor)
|
||||||
|
|
||||||
|
|
||||||
func is_cursor_over_drawing(global_pos: Vector2) -> bool:
|
func is_cursor_over_drawing(global_pos: Vector2) -> bool:
|
||||||
@@ -1254,7 +1278,10 @@ func _on_canvas_gui_input(event: InputEvent) -> void:
|
|||||||
return
|
return
|
||||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||||
if mb.pressed:
|
if mb.pressed:
|
||||||
_handle_left_press(world_pos)
|
if _create_point_armed and _edit_mode == EditMode.POINTS:
|
||||||
|
_create_point_from_toolbar(world_pos)
|
||||||
|
else:
|
||||||
|
_handle_left_press(world_pos)
|
||||||
else:
|
else:
|
||||||
_handle_left_release()
|
_handle_left_release()
|
||||||
|
|
||||||
@@ -1314,8 +1341,8 @@ func _handle_points_right_click(mb: InputEventMouseButton, world_pos: Vector2) -
|
|||||||
if context_shape >= 0 and context_vertex >= 0:
|
if context_shape >= 0 and context_vertex >= 0:
|
||||||
_context_menu.set_meta("context_shape", context_shape)
|
_context_menu.set_meta("context_shape", context_shape)
|
||||||
_context_menu.set_meta("context_vertex", context_vertex)
|
_context_menu.set_meta("context_vertex", context_vertex)
|
||||||
_context_menu.add_item("Remove Point", MENU_REMOVE_POINT)
|
_context_menu.add_destructive_item("Remove Point", MENU_REMOVE_POINT)
|
||||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
_context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Segment proximity -> Create Point (selects the shape first).
|
# Segment proximity -> Create Point (selects the shape first).
|
||||||
@@ -1327,7 +1354,7 @@ func _handle_points_right_click(mb: InputEventMouseButton, world_pos: Vector2) -
|
|||||||
queue_redraw()
|
queue_redraw()
|
||||||
shape_selected.emit(seg_shape)
|
shape_selected.emit(seg_shape)
|
||||||
_context_menu.add_item("Create Point", MENU_CREATE_POINT)
|
_context_menu.add_item("Create Point", MENU_CREATE_POINT)
|
||||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
_context_menu.popup_at(mb.global_position, _global_shape_target_rect(seg_shape))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Interior body: select only, no menu.
|
# Interior body: select only, no menu.
|
||||||
@@ -1360,17 +1387,18 @@ func _handle_transform_right_click(mb: InputEventMouseButton, world_pos: Vector2
|
|||||||
_context_menu.set_meta("click_position", world_pos)
|
_context_menu.set_meta("click_position", world_pos)
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
shape_selected.emit(hit_shape)
|
shape_selected.emit(hit_shape)
|
||||||
_context_menu.add_item("Copy", MENU_COPY)
|
_context_menu.add_item("Copy Shape", MENU_COPY)
|
||||||
if _has_clipboard:
|
if _has_clipboard:
|
||||||
_context_menu.add_item("Paste", MENU_PASTE)
|
_context_menu.add_item("Paste Shape", MENU_PASTE)
|
||||||
_context_menu.add_item("Erase", MENU_DELETE)
|
|
||||||
_context_menu.add_separator()
|
_context_menu.add_separator()
|
||||||
_context_menu.add_item("Flip Left-Right", MENU_MIRROR_X)
|
_context_menu.add_item("Flip Left-Right", MENU_MIRROR_X)
|
||||||
_context_menu.add_item("Flip Up-Down", MENU_MIRROR_Y)
|
_context_menu.add_item("Flip Up-Down", MENU_MIRROR_Y)
|
||||||
_context_menu.add_separator()
|
_context_menu.add_separator()
|
||||||
_context_menu.add_item("Bring Forward", MENU_BRING_FORWARD)
|
_context_menu.add_item("Bring Forward", MENU_BRING_FORWARD)
|
||||||
_context_menu.add_item("Send Back", MENU_SEND_BACK)
|
_context_menu.add_item("Send Back", MENU_SEND_BACK)
|
||||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
_context_menu.add_separator()
|
||||||
|
_context_menu.add_destructive_item("Erase Shape", MENU_DELETE)
|
||||||
|
_context_menu.popup_at(mb.global_position, _global_shape_target_rect(hit_shape))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Empty canvas -> add primitives.
|
# Empty canvas -> add primitives.
|
||||||
@@ -1382,8 +1410,8 @@ func _handle_transform_right_click(mb: InputEventMouseButton, world_pos: Vector2
|
|||||||
_context_menu.add_item("Add Circle", MENU_ADD_CIRCLE)
|
_context_menu.add_item("Add Circle", MENU_ADD_CIRCLE)
|
||||||
if _has_clipboard:
|
if _has_clipboard:
|
||||||
_context_menu.add_separator()
|
_context_menu.add_separator()
|
||||||
_context_menu.add_item("Paste", MENU_PASTE)
|
_context_menu.add_item("Paste Shape", MENU_PASTE)
|
||||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
_context_menu.popup_at(mb.global_position)
|
||||||
|
|
||||||
|
|
||||||
func _handle_left_press(world_pos: Vector2) -> void:
|
func _handle_left_press(world_pos: Vector2) -> void:
|
||||||
@@ -1464,6 +1492,21 @@ func _handle_left_press(world_pos: Vector2) -> void:
|
|||||||
shape_selected.emit(-1)
|
shape_selected.emit(-1)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_point_from_toolbar(world_pos: Vector2) -> void:
|
||||||
|
var shape_idx: int = hit_test_segment(world_pos)
|
||||||
|
if shape_idx < 0:
|
||||||
|
return
|
||||||
|
_selected_shape_idx = shape_idx
|
||||||
|
shape_selected.emit(shape_idx)
|
||||||
|
var new_vertex_index: int = create_point_on_edge(world_pos)
|
||||||
|
if new_vertex_index < 0:
|
||||||
|
return
|
||||||
|
_create_point_armed = false
|
||||||
|
_drag_mode = DragMode.NONE
|
||||||
|
_emit_changed()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
func _handle_drag_motion(mm: InputEventMouseMotion, world_pos: Vector2, ctrl_pressed: bool) -> void:
|
func _handle_drag_motion(mm: InputEventMouseMotion, world_pos: Vector2, ctrl_pressed: bool) -> void:
|
||||||
match _drag_mode:
|
match _drag_mode:
|
||||||
DragMode.VERTEX:
|
DragMode.VERTEX:
|
||||||
@@ -1821,6 +1864,19 @@ func _screen_to_world(pos: Vector2) -> Vector2:
|
|||||||
return (pos - _pan_offset) / _zoom
|
return (pos - _pan_offset) / _zoom
|
||||||
|
|
||||||
|
|
||||||
|
func _global_point_target_rect(world_pos: Vector2) -> Rect2:
|
||||||
|
var screen_pos := global_position + _pan_offset + world_pos * _zoom
|
||||||
|
return Rect2(screen_pos - Vector2(6.0, 6.0), Vector2(12.0, 12.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _global_shape_target_rect(shape_idx: int) -> Rect2:
|
||||||
|
if shape_idx < 0 or shape_idx >= shapes.size():
|
||||||
|
return Rect2()
|
||||||
|
var world_bounds: Rect2 = _shape_bbox(shapes[shape_idx])
|
||||||
|
var screen_position := global_position + _pan_offset + world_bounds.position * _zoom
|
||||||
|
return Rect2(screen_position, world_bounds.size * _zoom).grow(4.0)
|
||||||
|
|
||||||
|
|
||||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
func _snap_to_grid(pos: Vector2) -> Vector2:
|
||||||
var gs := float(_grid_size)
|
var gs := float(_grid_size)
|
||||||
if gs <= 0.0:
|
if gs <= 0.0:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ extends Control
|
|||||||
|
|
||||||
const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
||||||
const PropThemeScript := preload("res://scripts/editors/prop/prop_theme.gd")
|
const PropThemeScript := preload("res://scripts/editors/prop/prop_theme.gd")
|
||||||
|
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||||
|
const SandboxThemeScript := preload("res://scripts/stage/sandbox_theme.gd")
|
||||||
|
const StageDialogStylerScript := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||||
const PropCanvasScript := preload("res://scripts/editors/prop/prop_canvas.gd")
|
const PropCanvasScript := preload("res://scripts/editors/prop/prop_canvas.gd")
|
||||||
const PropThumbnailScript := preload("res://scripts/shared/thumbnails/prop_thumbnail.gd")
|
const PropThumbnailScript := preload("res://scripts/shared/thumbnails/prop_thumbnail.gd")
|
||||||
@@ -19,6 +22,7 @@ const ThumbnailCacheScript := preload("res://scripts/shared/thumbnails/thumbnail
|
|||||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||||
const PropAttachPreviewScene := preload("res://scenes/editors/prop/prop_attach_preview.tscn")
|
const PropAttachPreviewScene := preload("res://scenes/editors/prop/prop_attach_preview.tscn")
|
||||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
||||||
|
const PropInspectorIconsScript := preload("res://scripts/editors/prop/prop_inspector_icons.gd")
|
||||||
|
|
||||||
const PRESET_LABELS: PackedStringArray = ["None", "Wood", "Rubber", "Cardboard", "Metal"]
|
const PRESET_LABELS: PackedStringArray = ["None", "Wood", "Rubber", "Cardboard", "Metal"]
|
||||||
const PRIMITIVE_IDS: PackedStringArray = ["box", "plank", "triangle", "line", "circle"]
|
const PRIMITIVE_IDS: PackedStringArray = ["box", "plank", "triangle", "line", "circle"]
|
||||||
@@ -92,10 +96,10 @@ static func should_warn_off_origin(has_geometry: bool, bounds: Rect2) -> bool:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@onready var _menu_bar: MenuBar = %MenuBar
|
@onready var _menu_bar: MenuBar = %MenuBar
|
||||||
@onready var _top_right: HBoxContainer = %TopRightControls
|
@onready var _top_right: FlowContainer = %TopRightControls
|
||||||
@onready var _prop_name_edit: LineEdit = %PropNameEdit
|
@onready var _prop_name_edit: LineEdit = %PropNameEdit
|
||||||
@onready var _canvas: PropCanvas = %PropCanvas
|
@onready var _canvas: PropCanvas = %PropCanvas
|
||||||
@onready var _mode_bar: HBoxContainer = %ModeBar
|
@onready var _mode_bar: FlowContainer = %ModeBar
|
||||||
@onready var _size_tools: FlowContainer = %SizeTools
|
@onready var _size_tools: FlowContainer = %SizeTools
|
||||||
@onready var _attach_tools: FlowContainer = %AttachTools
|
@onready var _attach_tools: FlowContainer = %AttachTools
|
||||||
@onready var _canvas_workspace: HSplitContainer = %CanvasWorkspace
|
@onready var _canvas_workspace: HSplitContainer = %CanvasWorkspace
|
||||||
@@ -114,6 +118,8 @@ static func should_warn_off_origin(has_geometry: bool, bounds: Rect2) -> bool:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var _theme: PropTheme
|
var _theme: PropTheme
|
||||||
|
var _editor_visual_theme: EditorVisualTheme
|
||||||
|
var _dialog_theme: SandboxTheme
|
||||||
var _material_preset: int = PropBlockScript.MaterialPreset.NONE
|
var _material_preset: int = PropBlockScript.MaterialPreset.NONE
|
||||||
var _current_path: String = ""
|
var _current_path: String = ""
|
||||||
var _dirty: bool = false
|
var _dirty: bool = false
|
||||||
@@ -126,11 +132,14 @@ var _thumb_cache: ThumbnailCache
|
|||||||
var _ui_theme: Theme
|
var _ui_theme: Theme
|
||||||
|
|
||||||
# Inspector controls
|
# Inspector controls
|
||||||
var _preset_option: OptionButton
|
var _preset_option: PropInspectorDropdown
|
||||||
var _material_section: VBoxContainer
|
var _material_section: PanelContainer
|
||||||
var _mass_label: Label
|
var _mass_label: Label
|
||||||
|
var _mass_value: Label
|
||||||
var _friction_label: Label
|
var _friction_label: Label
|
||||||
|
var _friction_value: Label
|
||||||
var _bounce_label: Label
|
var _bounce_label: Label
|
||||||
|
var _bounce_value: Label
|
||||||
var _fill_button: ColorPickerButton
|
var _fill_button: ColorPickerButton
|
||||||
var _outline_button: ColorPickerButton
|
var _outline_button: ColorPickerButton
|
||||||
var _width_spin: SpinBox
|
var _width_spin: SpinBox
|
||||||
@@ -139,14 +148,16 @@ var _radius_row: Control
|
|||||||
var _reshaped_note: Label
|
var _reshaped_note: Label
|
||||||
var _vertex_label: Label
|
var _vertex_label: Label
|
||||||
var _delete_button: Button
|
var _delete_button: Button
|
||||||
var _shape_section: VBoxContainer
|
var _create_point_button: Button
|
||||||
var _primitive_option: OptionButton
|
var _create_point_hint: Label
|
||||||
|
var _shape_section: PanelContainer
|
||||||
|
var _primitive_option: PropInspectorDropdown
|
||||||
var _params_box: VBoxContainer
|
var _params_box: VBoxContainer
|
||||||
var _param_spins: Dictionary = {}
|
var _param_spins: Dictionary = {}
|
||||||
var _add_button: Button
|
var _add_button: Button
|
||||||
var _updating_inspector: bool = false
|
var _updating_inspector: bool = false
|
||||||
var _tag_checks: Dictionary = {}
|
var _tag_checks: Dictionary = {}
|
||||||
var _tag_section: VBoxContainer
|
var _tag_section: PanelContainer
|
||||||
var _stage_authoring: bool = false
|
var _stage_authoring: bool = false
|
||||||
var _stage_return_pending: bool = false
|
var _stage_return_pending: bool = false
|
||||||
var _stage_place_button: Button
|
var _stage_place_button: Button
|
||||||
@@ -173,7 +184,7 @@ var _points_button: Button
|
|||||||
var _attach_button: Button
|
var _attach_button: Button
|
||||||
|
|
||||||
# Add-shape inspector section (hidden in Reshape mode, Round 3)
|
# Add-shape inspector section (hidden in Reshape mode, Round 3)
|
||||||
var _add_section: VBoxContainer
|
var _add_section: PanelContainer
|
||||||
var _anchor_section: VBoxContainer
|
var _anchor_section: VBoxContainer
|
||||||
var _anchor_list: ItemList
|
var _anchor_list: ItemList
|
||||||
var _anchor_role: OptionButton
|
var _anchor_role: OptionButton
|
||||||
@@ -212,6 +223,10 @@ var _recent_props: Array[String] = []
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if not _stage_authoring:
|
if not _stage_authoring:
|
||||||
get_tree().auto_accept_quit = false
|
get_tree().auto_accept_quit = false
|
||||||
|
_editor_visual_theme = EditorVisualThemeScript.new()
|
||||||
|
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||||
|
_dialog_theme = SandboxThemeScript.new()
|
||||||
|
_dialog_theme.load_from(SandboxThemeScript.DEFAULT_PATH)
|
||||||
_theme = PropThemeScript.new()
|
_theme = PropThemeScript.new()
|
||||||
_theme.load_from(PropThemeScript.DEFAULT_PATH)
|
_theme.load_from(PropThemeScript.DEFAULT_PATH)
|
||||||
_thumb_cache = ThumbnailCacheScript.new()
|
_thumb_cache = ThumbnailCacheScript.new()
|
||||||
@@ -248,6 +263,7 @@ func _ready() -> void:
|
|||||||
_canvas.set_grid_size(_theme.grid_size())
|
_canvas.set_grid_size(_theme.grid_size())
|
||||||
_canvas.set_snap_enabled(false)
|
_canvas.set_snap_enabled(false)
|
||||||
_canvas.set_has_clipboard(false)
|
_canvas.set_has_clipboard(false)
|
||||||
|
_canvas.set_context_menu_visual_theme(_editor_visual_theme, &"prop")
|
||||||
_canvas.set_context_menu_font_size(_theme.font_context_menu())
|
_canvas.set_context_menu_font_size(_theme.font_context_menu())
|
||||||
_canvas.set_anchor_data(_anchors)
|
_canvas.set_anchor_data(_anchors)
|
||||||
var origin_marker_color: Color = _theme.text_muted_color()
|
var origin_marker_color: Color = _theme.text_muted_color()
|
||||||
@@ -260,7 +276,7 @@ func _ready() -> void:
|
|||||||
_cursor_coords.add_theme_font_size_override("font_size", _theme.font_status())
|
_cursor_coords.add_theme_font_size_override("font_size", _theme.font_status())
|
||||||
_save_status.add_theme_font_size_override("font_size", _theme.font_status())
|
_save_status.add_theme_font_size_override("font_size", _theme.font_status())
|
||||||
|
|
||||||
_mode_bar.alignment = BoxContainer.ALIGNMENT_CENTER
|
_mode_bar.alignment = FlowContainer.ALIGNMENT_CENTER
|
||||||
|
|
||||||
_refresh_inspector_from_selection()
|
_refresh_inspector_from_selection()
|
||||||
_sync_mode_buttons(_canvas.get_edit_mode())
|
_sync_mode_buttons(_canvas.get_edit_mode())
|
||||||
@@ -311,7 +327,11 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
return
|
return
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null
|
var focus_owner: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null
|
||||||
if not (focus_owner is LineEdit or focus_owner is TextEdit):
|
if not (focus_owner is LineEdit or focus_owner is TextEdit):
|
||||||
if _canvas.is_whole_prop_resize_dragging():
|
if _canvas.is_create_point_armed():
|
||||||
|
_canvas.cancel_create_point()
|
||||||
|
_update_create_point_feedback()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif _canvas.is_whole_prop_resize_dragging():
|
||||||
_canvas.cancel_whole_prop_resize()
|
_canvas.cancel_whole_prop_resize()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
elif _canvas.is_whole_prop_resize_enabled():
|
elif _canvas.is_whole_prop_resize_enabled():
|
||||||
@@ -337,8 +357,11 @@ func _update_save_status() -> void:
|
|||||||
var name: String = _prop_name_edit.text if _prop_name_edit != null else ""
|
var name: String = _prop_name_edit.text if _prop_name_edit != null else ""
|
||||||
if name.is_empty():
|
if name.is_empty():
|
||||||
name = "Unnamed Prop"
|
name = "Unnamed Prop"
|
||||||
var suffix: String = " • Modified" if _dirty else ""
|
var has_saved_path: bool = not _current_path.is_empty()
|
||||||
_save_status.text = "%s — %s%s" % [name, PRESET_LABELS[_material_preset], suffix]
|
var state: String = "Unsaved changes" if _dirty else ("Saved" if has_saved_path else "Not saved yet")
|
||||||
|
_save_status.text = "%s — %s — %s" % [name, PRESET_LABELS[_material_preset], state]
|
||||||
|
var status_color: Color = _editor_visual_theme.chrome_color(&"dirty", &"prop") if _dirty else (_editor_visual_theme.chrome_color(&"saved", &"prop") if has_saved_path else _editor_visual_theme.chrome_color(&"text_muted", &"prop"))
|
||||||
|
_save_status.add_theme_color_override("font_color", status_color)
|
||||||
_update_window_title()
|
_update_window_title()
|
||||||
|
|
||||||
|
|
||||||
@@ -372,10 +395,7 @@ func _make_mode_button(mode: int) -> Button:
|
|||||||
button.button_group = _mode_group
|
button.button_group = _mode_group
|
||||||
button.custom_minimum_size.y = float(_theme.mode_button_min_height())
|
button.custom_minimum_size.y = float(_theme.mode_button_min_height())
|
||||||
button.add_theme_font_size_override("font_size", _theme.font_mode())
|
button.add_theme_font_size_override("font_size", _theme.font_mode())
|
||||||
# No keyboard focus: the focus stylebox otherwise draws over the pressed
|
button.focus_mode = Control.FOCUS_ALL
|
||||||
# stylebox, hiding the selected (accent-bordered) state until the cursor
|
|
||||||
# moves away (Round 4 fix).
|
|
||||||
button.focus_mode = Control.FOCUS_NONE
|
|
||||||
_mode_bar.add_child(button)
|
_mode_bar.add_child(button)
|
||||||
return button
|
return button
|
||||||
|
|
||||||
@@ -399,6 +419,8 @@ func _on_canvas_mode_changed(mode: int) -> void:
|
|||||||
return
|
return
|
||||||
if mode != PropCanvasScript.EditMode.TRANSFORM:
|
if mode != PropCanvasScript.EditMode.TRANSFORM:
|
||||||
_set_whole_prop_resize_enabled(false)
|
_set_whole_prop_resize_enabled(false)
|
||||||
|
if mode != PropCanvasScript.EditMode.POINTS and _canvas.is_create_point_armed():
|
||||||
|
_canvas.cancel_create_point()
|
||||||
var entering_attach: bool = mode == PropCanvasScript.EditMode.ATTACH and _last_canvas_mode != PropCanvasScript.EditMode.ATTACH
|
var entering_attach: bool = mode == PropCanvasScript.EditMode.ATTACH and _last_canvas_mode != PropCanvasScript.EditMode.ATTACH
|
||||||
if entering_attach and _show_preview_check != null:
|
if entering_attach and _show_preview_check != null:
|
||||||
_show_preview_check.set_pressed_no_signal(true)
|
_show_preview_check.set_pressed_no_signal(true)
|
||||||
@@ -430,6 +452,11 @@ func _sync_mode_controls(mode: int) -> void:
|
|||||||
_add_section.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
_add_section.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||||
if _delete_button != null:
|
if _delete_button != null:
|
||||||
_delete_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
_delete_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||||
|
if _create_point_button != null:
|
||||||
|
_create_point_button.visible = mode == PropCanvasScript.EditMode.POINTS and _canvas.get_selected_index() >= 0
|
||||||
|
if _create_point_hint != null:
|
||||||
|
_create_point_hint.visible = mode == PropCanvasScript.EditMode.POINTS and _canvas.get_selected_index() >= 0
|
||||||
|
_update_create_point_feedback()
|
||||||
if _shape_section != null:
|
if _shape_section != null:
|
||||||
_shape_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _canvas.is_whole_prop_resize_enabled() and _canvas.get_selected_index() >= 0
|
_shape_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _canvas.is_whole_prop_resize_enabled() and _canvas.get_selected_index() >= 0
|
||||||
if _material_section != null:
|
if _material_section != null:
|
||||||
@@ -461,73 +488,15 @@ func _build_attach_tools() -> void:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _build_control_theme() -> void:
|
func _build_control_theme() -> void:
|
||||||
_ui_theme = Theme.new()
|
_ui_theme = _editor_visual_theme.control_theme(&"prop")
|
||||||
var bg: Color = _theme.canvas_background_color()
|
theme = _ui_theme
|
||||||
var panel := StyleBoxFlat.new()
|
var inspector_surface: StyleBoxFlat = _ui_theme.get_stylebox("panel", "PanelContainer").duplicate() as StyleBoxFlat
|
||||||
panel.bg_color = Color(0.12, 0.12, 0.12, 1.0)
|
|
||||||
panel.set_corner_radius_all(6)
|
|
||||||
panel.content_margin_left = 8
|
|
||||||
panel.content_margin_right = 8
|
|
||||||
panel.content_margin_top = 6
|
|
||||||
panel.content_margin_bottom = 6
|
|
||||||
panel.border_color = Color(1.0, 1.0, 1.0, 0.12)
|
|
||||||
panel.set_border_width_all(1)
|
|
||||||
var panel_dark := StyleBoxFlat.new()
|
|
||||||
panel_dark.bg_color = bg.darkened(0.25)
|
|
||||||
panel_dark.set_corner_radius_all(6)
|
|
||||||
panel_dark.content_margin_left = 8
|
|
||||||
panel_dark.content_margin_right = 8
|
|
||||||
panel_dark.content_margin_top = 6
|
|
||||||
panel_dark.content_margin_bottom = 6
|
|
||||||
panel_dark.border_color = Color(1.0, 1.0, 1.0, 0.12)
|
|
||||||
panel_dark.set_border_width_all(1)
|
|
||||||
var panel_pressed := StyleBoxFlat.new()
|
|
||||||
panel_pressed.bg_color = bg.darkened(0.25)
|
|
||||||
panel_pressed.set_corner_radius_all(6)
|
|
||||||
panel_pressed.content_margin_left = 8
|
|
||||||
panel_pressed.content_margin_right = 8
|
|
||||||
panel_pressed.content_margin_top = 6
|
|
||||||
panel_pressed.content_margin_bottom = 6
|
|
||||||
panel_pressed.border_color = _theme.accent_color()
|
|
||||||
panel_pressed.set_border_width_all(2)
|
|
||||||
_ui_theme.set_stylebox("normal", "Button", panel)
|
|
||||||
_ui_theme.set_stylebox("hover", "Button", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("pressed", "Button", panel_pressed)
|
|
||||||
_ui_theme.set_stylebox("focus", "Button", panel)
|
|
||||||
var inspector_surface := StyleBoxFlat.new()
|
|
||||||
inspector_surface.bg_color = _theme.inspector_background_color()
|
|
||||||
inspector_surface.border_color = _theme.border_color()
|
|
||||||
inspector_surface.set_border_width_all(1)
|
|
||||||
inspector_surface.content_margin_left = 8
|
|
||||||
inspector_surface.content_margin_right = 8
|
|
||||||
_inspector_column.add_theme_stylebox_override("panel", inspector_surface)
|
_inspector_column.add_theme_stylebox_override("panel", inspector_surface)
|
||||||
var splitter := StyleBoxFlat.new()
|
var splitter := StyleBoxFlat.new()
|
||||||
splitter.bg_color = _theme.border_color().lightened(0.35)
|
splitter.bg_color = _editor_visual_theme.chrome_color(&"border", &"prop").lightened(0.35)
|
||||||
splitter.content_margin_left = 3
|
splitter.content_margin_left = 3
|
||||||
splitter.content_margin_right = 3
|
splitter.content_margin_right = 3
|
||||||
_canvas_workspace.add_theme_stylebox_override("grabber", splitter)
|
_canvas_workspace.add_theme_stylebox_override("grabber", splitter)
|
||||||
_ui_theme.set_stylebox("normal", "OptionButton", panel)
|
|
||||||
_ui_theme.set_stylebox("hover", "OptionButton", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("pressed", "OptionButton", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("normal", "SpinBox", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("normal", "LineEdit", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("normal", "ColorPickerButton", panel)
|
|
||||||
_ui_theme.set_stylebox("hover", "ColorPickerButton", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("normal", "PopupMenu", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("panel", "MenuBar", panel_dark)
|
|
||||||
_ui_theme.set_stylebox("normal", "CheckBox", panel)
|
|
||||||
_ui_theme.set_stylebox("panel", "PanelContainer", panel_dark)
|
|
||||||
_ui_theme.set_color("font_color", "Label", _theme.text_color())
|
|
||||||
_ui_theme.set_color("font_color", "Button", _theme.text_color())
|
|
||||||
_ui_theme.set_color("font_hover_color", "Button", Color.WHITE)
|
|
||||||
_ui_theme.set_color("font_pressed_color", "Button", _theme.accent_color())
|
|
||||||
_ui_theme.set_font_size("font_size", "Label", _theme.font_body())
|
|
||||||
_ui_theme.set_font_size("font_size", "Button", _theme.font_body())
|
|
||||||
_ui_theme.set_font_size("font_size", "OptionButton", _theme.font_body())
|
|
||||||
_ui_theme.set_font_size("font_size", "SpinBox", _theme.font_body())
|
|
||||||
_ui_theme.set_font_size("font_size", "LineEdit", _theme.font_body())
|
|
||||||
_ui_theme.set_font_size("font_size", "PopupMenu", _theme.font_menu())
|
|
||||||
_ui_theme.set_font_size("font_size", "MenuBar", _theme.font_menu_bar())
|
|
||||||
_menu_bar.theme = _ui_theme
|
_menu_bar.theme = _ui_theme
|
||||||
_top_right.theme = _ui_theme
|
_top_right.theme = _ui_theme
|
||||||
_prop_name_edit.theme = _ui_theme
|
_prop_name_edit.theme = _ui_theme
|
||||||
@@ -544,26 +513,26 @@ func _setup_menu_bar() -> void:
|
|||||||
var file_menu := PopupMenu.new()
|
var file_menu := PopupMenu.new()
|
||||||
file_menu.name = "FileMenu"
|
file_menu.name = "FileMenu"
|
||||||
file_menu.add_item("🆕 New", MENU_NEW)
|
file_menu.add_item("🆕 New", MENU_NEW)
|
||||||
file_menu.add_item("💾 Save", MENU_SAVE)
|
file_menu.add_item("📂 Load…", MENU_LOAD)
|
||||||
file_menu.add_item("📄 Save As...", MENU_SAVE_AS)
|
|
||||||
file_menu.add_item("📂 Load...", MENU_LOAD)
|
|
||||||
file_menu.add_separator()
|
|
||||||
var recent_popup := PopupMenu.new()
|
var recent_popup := PopupMenu.new()
|
||||||
recent_popup.name = "RecentProps"
|
recent_popup.name = "RecentProps"
|
||||||
recent_popup.id_pressed.connect(_on_recent_prop_selected)
|
recent_popup.id_pressed.connect(_on_recent_prop_selected)
|
||||||
file_menu.add_child(recent_popup)
|
file_menu.add_child(recent_popup)
|
||||||
file_menu.add_submenu_item("🕑 Recent", "RecentProps")
|
file_menu.add_submenu_item("🕑 Open Recent", "RecentProps")
|
||||||
|
file_menu.add_separator()
|
||||||
|
file_menu.add_item("💾 Save", MENU_SAVE)
|
||||||
|
file_menu.add_item("📄 Save As…", MENU_SAVE_AS)
|
||||||
file_menu.id_pressed.connect(_on_menu_id_pressed)
|
file_menu.id_pressed.connect(_on_menu_id_pressed)
|
||||||
file_menu.about_to_popup.connect(_update_recent_menu)
|
file_menu.about_to_popup.connect(_update_recent_menu)
|
||||||
_menu_bar.add_child(file_menu)
|
_menu_bar.add_child(file_menu)
|
||||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "📁 File")
|
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
||||||
|
|
||||||
var view_menu := PopupMenu.new()
|
var view_menu := PopupMenu.new()
|
||||||
view_menu.name = "ViewMenu"
|
view_menu.name = "ViewMenu"
|
||||||
view_menu.add_item("🔍 Reset View", MENU_RESET_VIEWS)
|
view_menu.add_item("🔍 Reset Views", MENU_RESET_VIEWS)
|
||||||
view_menu.id_pressed.connect(_on_menu_id_pressed)
|
view_menu.id_pressed.connect(_on_menu_id_pressed)
|
||||||
_menu_bar.add_child(view_menu)
|
_menu_bar.add_child(view_menu)
|
||||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "👁️ View")
|
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "View")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -654,22 +623,35 @@ func _setup_dialogs() -> void:
|
|||||||
_load_dialog.current_dir = content_dir
|
_load_dialog.current_dir = content_dir
|
||||||
_save_dialog.file_selected.connect(_on_save_file_selected)
|
_save_dialog.file_selected.connect(_on_save_file_selected)
|
||||||
_load_dialog.file_selected.connect(_on_load_file_selected)
|
_load_dialog.file_selected.connect(_on_load_file_selected)
|
||||||
|
_editor_visual_theme.style_button_role(_save_dialog.get_ok_button(), &"primary", &"prop")
|
||||||
|
_editor_visual_theme.style_button_role(_save_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||||
|
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"prop")
|
||||||
|
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||||
|
_error_dialog.get_ok_button().text = "Got it"
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_error_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_error_dialog.get_ok_button()
|
||||||
|
)
|
||||||
_build_exit_dialog()
|
_build_exit_dialog()
|
||||||
_rename_anchor_dialog = ConfirmationDialog.new()
|
_rename_anchor_dialog = ConfirmationDialog.new()
|
||||||
_rename_anchor_dialog.title = "Rename Anchor"
|
_rename_anchor_dialog.title = "Rename Anchor"
|
||||||
_rename_anchor_dialog.ok_button_text = "Rename"
|
_rename_anchor_dialog.ok_button_text = "Rename"
|
||||||
_rename_anchor_dialog.theme = _ui_theme
|
_rename_anchor_dialog.set_meta(StageDialogStylerScript.ICON_META, "📝")
|
||||||
_rename_anchor_edit = LineEdit.new()
|
_rename_anchor_edit = LineEdit.new()
|
||||||
_rename_anchor_edit.placeholder_text = "Anchor name"
|
_rename_anchor_edit.placeholder_text = "Anchor name"
|
||||||
_rename_anchor_dialog.add_child(_rename_anchor_edit)
|
_rename_anchor_dialog.add_child(_rename_anchor_edit)
|
||||||
add_child(_rename_anchor_dialog)
|
add_child(_rename_anchor_dialog)
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_rename_anchor_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_rename_anchor_dialog.get_ok_button()
|
||||||
|
)
|
||||||
_rename_anchor_edit.text_changed.connect(_update_rename_anchor_confirmation)
|
_rename_anchor_edit.text_changed.connect(_update_rename_anchor_confirmation)
|
||||||
_rename_anchor_edit.text_submitted.connect(_on_rename_anchor_text_submitted)
|
_rename_anchor_edit.text_submitted.connect(_on_rename_anchor_text_submitted)
|
||||||
_rename_anchor_dialog.confirmed.connect(_commit_anchor_rename)
|
_rename_anchor_dialog.confirmed.connect(_commit_anchor_rename)
|
||||||
_rename_anchor_dialog.canceled.connect(_clear_pending_anchor_rename)
|
_rename_anchor_dialog.canceled.connect(_clear_pending_anchor_rename)
|
||||||
|
|
||||||
func _on_rename_anchor_text_submitted(_text: String) -> void:
|
func _on_rename_anchor_text_submitted(_text: String) -> void:
|
||||||
if _rename_anchor_dialog == null or not _rename_anchor_dialog.visible:
|
if _rename_anchor_dialog == null or not StageDialogStylerScript.is_open(_rename_anchor_dialog):
|
||||||
return
|
return
|
||||||
var ok_button := _rename_anchor_dialog.get_ok_button()
|
var ok_button := _rename_anchor_dialog.get_ok_button()
|
||||||
if not ok_button.disabled:
|
if not ok_button.disabled:
|
||||||
@@ -708,8 +690,8 @@ func _build_top_right_controls() -> void:
|
|||||||
_top_right.add_child(_snap_check)
|
_top_right.add_child(_snap_check)
|
||||||
func _build_size_tools() -> void:
|
func _build_size_tools() -> void:
|
||||||
_whole_prop_resize_button = Button.new()
|
_whole_prop_resize_button = Button.new()
|
||||||
_whole_prop_resize_button.text = "✥ Transform Whole Prop"
|
_whole_prop_resize_button.text = "Move, Turn, or Resize Prop"
|
||||||
_whole_prop_resize_button.tooltip_text = "Move, rotate, or resize the whole prop including grip markers."
|
_whole_prop_resize_button.tooltip_text = "Move, turn, or resize the whole prop, including grip markers."
|
||||||
_whole_prop_resize_button.toggle_mode = true
|
_whole_prop_resize_button.toggle_mode = true
|
||||||
_whole_prop_resize_button.custom_minimum_size.y = float(_theme.button_min_height())
|
_whole_prop_resize_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
_whole_prop_resize_button.toggled.connect(_set_whole_prop_resize_enabled)
|
_whole_prop_resize_button.toggled.connect(_set_whole_prop_resize_enabled)
|
||||||
@@ -920,24 +902,31 @@ func _on_whole_prop_rotate_requested(angle_rad: float) -> void:
|
|||||||
|
|
||||||
func _build_exit_dialog() -> void:
|
func _build_exit_dialog() -> void:
|
||||||
_exit_dialog = ConfirmationDialog.new()
|
_exit_dialog = ConfirmationDialog.new()
|
||||||
_exit_dialog.title = "⚠️ Unsaved Changes"
|
_exit_dialog.title = "Unsaved Changes"
|
||||||
|
_exit_dialog.set_meta(StageDialogStylerScript.ICON_META, "💾")
|
||||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
||||||
_exit_dialog.ok_button_text = "💾 Save"
|
_exit_dialog.ok_button_text = "Save"
|
||||||
_exit_dialog.add_button("🗑️ Discard", false, "discard")
|
var discard_button: Button = _exit_dialog.add_button("Discard", false, "discard")
|
||||||
_exit_dialog.get_cancel_button().text = "❌ Cancel"
|
_exit_dialog.get_cancel_button().text = "Cancel"
|
||||||
_exit_dialog.theme = _ui_theme
|
|
||||||
add_child(_exit_dialog)
|
add_child(_exit_dialog)
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_exit_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_exit_dialog.get_ok_button(), [discard_button]
|
||||||
|
)
|
||||||
_exit_dialog.confirmed.connect(_on_exit_save)
|
_exit_dialog.confirmed.connect(_on_exit_save)
|
||||||
_exit_dialog.custom_action.connect(_on_exit_custom_action)
|
_exit_dialog.custom_action.connect(_on_exit_custom_action)
|
||||||
_exit_dialog.canceled.connect(_on_exit_cancel)
|
_exit_dialog.canceled.connect(_on_exit_cancel)
|
||||||
_off_origin_save_dialog = ConfirmationDialog.new()
|
_off_origin_save_dialog = ConfirmationDialog.new()
|
||||||
_off_origin_save_dialog.title = "🎯 Center Prop?"
|
_off_origin_save_dialog.title = "🎯 Center Prop?"
|
||||||
_off_origin_save_dialog.dialog_text = "This prop is far from (0,0). Centering it keeps its shapes and anchors together.\n\nWhat would you like to do?"
|
_off_origin_save_dialog.dialog_text = "This prop is far from (0,0). Centering it keeps its shapes and anchors together.\n\nWhat would you like to do?"
|
||||||
_off_origin_save_dialog.ok_button_text = "🎯 Center & Save"
|
_off_origin_save_dialog.ok_button_text = "Center & Save"
|
||||||
_off_origin_save_dialog.add_button("💾 Save Anyway", false, "save_anyway")
|
var save_anyway_button: Button = _off_origin_save_dialog.add_button("Save Anyway", false, "save_anyway")
|
||||||
_off_origin_save_dialog.get_cancel_button().text = "❌ Cancel"
|
_off_origin_save_dialog.get_cancel_button().text = "Cancel"
|
||||||
_off_origin_save_dialog.theme = _ui_theme
|
|
||||||
add_child(_off_origin_save_dialog)
|
add_child(_off_origin_save_dialog)
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_off_origin_save_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_off_origin_save_dialog.get_ok_button(), [], [save_anyway_button]
|
||||||
|
)
|
||||||
_off_origin_save_dialog.confirmed.connect(func() -> void: await _execute_pending_save(true))
|
_off_origin_save_dialog.confirmed.connect(func() -> void: await _execute_pending_save(true))
|
||||||
_off_origin_save_dialog.custom_action.connect(func(action: StringName) -> void:
|
_off_origin_save_dialog.custom_action.connect(func(action: StringName) -> void:
|
||||||
if action == &"save_anyway": await _execute_pending_save(false))
|
if action == &"save_anyway": await _execute_pending_save(false))
|
||||||
@@ -959,7 +948,7 @@ func _request_close() -> void:
|
|||||||
_pending_quit = true
|
_pending_quit = true
|
||||||
_pending_new = false
|
_pending_new = false
|
||||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
||||||
_exit_dialog.popup_centered()
|
StageDialogStylerScript.popup(_exit_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _request_new() -> void:
|
func _request_new() -> void:
|
||||||
@@ -970,7 +959,7 @@ func _request_new() -> void:
|
|||||||
_pending_new = true
|
_pending_new = true
|
||||||
_pending_quit = false
|
_pending_quit = false
|
||||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before starting a new prop?"
|
_exit_dialog.dialog_text = "You have unsaved changes. Save them before starting a new prop?"
|
||||||
_exit_dialog.popup_centered()
|
StageDialogStylerScript.popup(_exit_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _request_stage_return() -> void:
|
func _request_stage_return() -> void:
|
||||||
@@ -986,11 +975,11 @@ func _request_stage_return() -> void:
|
|||||||
_pending_new = false
|
_pending_new = false
|
||||||
_pending_quit = false
|
_pending_quit = false
|
||||||
_exit_dialog.dialog_text = "Save your artwork before returning? Saving places it on the Stage."
|
_exit_dialog.dialog_text = "Save your artwork before returning? Saving places it on the Stage."
|
||||||
_exit_dialog.popup_centered()
|
StageDialogStylerScript.popup(_exit_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _on_exit_save() -> void:
|
func _on_exit_save() -> void:
|
||||||
_exit_dialog.hide()
|
StageDialogStylerScript.hide(_exit_dialog)
|
||||||
if _current_path.is_empty():
|
if _current_path.is_empty():
|
||||||
_save_dialog.popup_centered()
|
_save_dialog.popup_centered()
|
||||||
return
|
return
|
||||||
@@ -999,7 +988,7 @@ func _on_exit_save() -> void:
|
|||||||
|
|
||||||
func _on_exit_custom_action(action: StringName) -> void:
|
func _on_exit_custom_action(action: StringName) -> void:
|
||||||
if action == &"discard":
|
if action == &"discard":
|
||||||
_exit_dialog.hide()
|
StageDialogStylerScript.hide(_exit_dialog)
|
||||||
if not _stage_authoring:
|
if not _stage_authoring:
|
||||||
_clear_autosave()
|
_clear_autosave()
|
||||||
if _stage_return_pending:
|
if _stage_return_pending:
|
||||||
@@ -1074,47 +1063,60 @@ func _on_menu_id_pressed(id: int) -> void:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _build_inspector() -> void:
|
func _build_inspector() -> void:
|
||||||
var header_size: int = _theme.font_header()
|
|
||||||
var row_height: int = _theme.row_min_height()
|
var row_height: int = _theme.row_min_height()
|
||||||
|
_inspector_list.add_theme_constant_override("separation", 10)
|
||||||
|
_inspector_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
# Material preset section
|
# Material preset section
|
||||||
_material_section = VBoxContainer.new()
|
_material_section = _make_inspector_card()
|
||||||
_material_section.add_theme_constant_override("separation", 6)
|
|
||||||
_inspector_list.add_child(_material_section)
|
_inspector_list.add_child(_material_section)
|
||||||
var material_title := Label.new()
|
var material_content: VBoxContainer = _inspector_card_content(_material_section)
|
||||||
material_title.text = "Material"
|
material_content.add_child(_make_inspector_header("Material", PropInspectorIconsScript.section("material")))
|
||||||
material_title.add_theme_font_size_override("font_size", header_size)
|
|
||||||
_material_section.add_child(material_title)
|
|
||||||
|
|
||||||
_preset_option = OptionButton.new()
|
_preset_option = PropInspectorDropdown.new()
|
||||||
for label: String in PRESET_LABELS:
|
for index: int in PRESET_LABELS.size():
|
||||||
_preset_option.add_item(label)
|
_preset_option.add_icon_item(
|
||||||
|
PropInspectorIconsScript.material(index, _theme.material_tint(index, "fill")),
|
||||||
|
PRESET_LABELS[index]
|
||||||
|
)
|
||||||
_preset_option.selected = _material_preset
|
_preset_option.selected = _material_preset
|
||||||
_preset_option.custom_minimum_size.y = float(_theme.button_min_height())
|
_preset_option.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
|
_preset_option.configure(
|
||||||
|
_ui_theme,
|
||||||
|
_theme.inspector_background_color(),
|
||||||
|
_editor_visual_theme.chrome_color(&"selected_surface", &"prop"),
|
||||||
|
_theme.border_color(),
|
||||||
|
_theme.accent_color(),
|
||||||
|
_editor_visual_theme.chrome_color(&"focus", &"prop"),
|
||||||
|
_theme.text_color(),
|
||||||
|
_theme.font_body()
|
||||||
|
)
|
||||||
_preset_option.item_selected.connect(_on_preset_selected)
|
_preset_option.item_selected.connect(_on_preset_selected)
|
||||||
_material_section.add_child(_preset_option)
|
material_content.add_child(_preset_option)
|
||||||
|
|
||||||
_mass_label = Label.new()
|
var stats := HBoxContainer.new()
|
||||||
_friction_label = Label.new()
|
stats.add_theme_constant_override("separation", 6)
|
||||||
_bounce_label = Label.new()
|
material_content.add_child(stats)
|
||||||
_material_section.add_child(_mass_label)
|
var mass_tile: Array[Label] = _make_stat_tile(stats, "Mass")
|
||||||
_material_section.add_child(_friction_label)
|
_mass_label = mass_tile[0]
|
||||||
_material_section.add_child(_bounce_label)
|
_mass_value = mass_tile[1]
|
||||||
|
var grip_tile: Array[Label] = _make_stat_tile(stats, "Surface grip")
|
||||||
|
_friction_label = grip_tile[0]
|
||||||
|
_friction_value = grip_tile[1]
|
||||||
|
var bounce_tile: Array[Label] = _make_stat_tile(stats, "Bounce")
|
||||||
|
_bounce_label = bounce_tile[0]
|
||||||
|
_bounce_value = bounce_tile[1]
|
||||||
_update_physics_labels()
|
_update_physics_labels()
|
||||||
|
|
||||||
# Selected-shape section
|
# Selected-shape section
|
||||||
_shape_section = VBoxContainer.new()
|
_shape_section = _make_inspector_card()
|
||||||
_shape_section.add_theme_constant_override("separation", 6)
|
|
||||||
_inspector_list.add_child(_shape_section)
|
_inspector_list.add_child(_shape_section)
|
||||||
|
var shape_content: VBoxContainer = _inspector_card_content(_shape_section)
|
||||||
var shape_title := Label.new()
|
shape_content.add_child(_make_inspector_header("Selected Shape", PropInspectorIconsScript.section("selected_shape")))
|
||||||
shape_title.text = "Selected Shape"
|
|
||||||
shape_title.add_theme_font_size_override("font_size", header_size)
|
|
||||||
_shape_section.add_child(shape_title)
|
|
||||||
|
|
||||||
var fill_row := HBoxContainer.new()
|
var fill_row := HBoxContainer.new()
|
||||||
fill_row.custom_minimum_size.y = float(row_height)
|
fill_row.custom_minimum_size.y = float(row_height)
|
||||||
_shape_section.add_child(fill_row)
|
shape_content.add_child(fill_row)
|
||||||
var fill_label := Label.new()
|
var fill_label := Label.new()
|
||||||
fill_label.text = "Fill:"
|
fill_label.text = "Fill:"
|
||||||
fill_label.custom_minimum_size = Vector2(90.0, 0.0)
|
fill_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||||
@@ -1127,7 +1129,7 @@ func _build_inspector() -> void:
|
|||||||
|
|
||||||
var outline_row := HBoxContainer.new()
|
var outline_row := HBoxContainer.new()
|
||||||
outline_row.custom_minimum_size.y = float(row_height)
|
outline_row.custom_minimum_size.y = float(row_height)
|
||||||
_shape_section.add_child(outline_row)
|
shape_content.add_child(outline_row)
|
||||||
var outline_label := Label.new()
|
var outline_label := Label.new()
|
||||||
outline_label.text = "Outline:"
|
outline_label.text = "Outline:"
|
||||||
outline_label.custom_minimum_size = Vector2(90.0, 0.0)
|
outline_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||||
@@ -1140,7 +1142,7 @@ func _build_inspector() -> void:
|
|||||||
|
|
||||||
var width_row := HBoxContainer.new()
|
var width_row := HBoxContainer.new()
|
||||||
width_row.custom_minimum_size.y = float(row_height)
|
width_row.custom_minimum_size.y = float(row_height)
|
||||||
_shape_section.add_child(width_row)
|
shape_content.add_child(width_row)
|
||||||
var width_label := Label.new()
|
var width_label := Label.new()
|
||||||
width_label.text = "Width:"
|
width_label.text = "Width:"
|
||||||
width_label.custom_minimum_size = Vector2(90.0, 0.0)
|
width_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||||
@@ -1156,7 +1158,7 @@ func _build_inspector() -> void:
|
|||||||
|
|
||||||
_radius_row = HBoxContainer.new()
|
_radius_row = HBoxContainer.new()
|
||||||
_radius_row.custom_minimum_size.y = float(row_height)
|
_radius_row.custom_minimum_size.y = float(row_height)
|
||||||
_shape_section.add_child(_radius_row)
|
shape_content.add_child(_radius_row)
|
||||||
var radius_label := Label.new()
|
var radius_label := Label.new()
|
||||||
radius_label.text = "Radius:"
|
radius_label.text = "Radius:"
|
||||||
radius_label.custom_minimum_size = Vector2(90.0, 0.0)
|
radius_label.custom_minimum_size = Vector2(90.0, 0.0)
|
||||||
@@ -1173,43 +1175,66 @@ func _build_inspector() -> void:
|
|||||||
_reshaped_note = Label.new()
|
_reshaped_note = Label.new()
|
||||||
_reshaped_note.text = "✏️ This shape was reshaped"
|
_reshaped_note.text = "✏️ This shape was reshaped"
|
||||||
_reshaped_note.add_theme_color_override("font_color", _theme.text_muted_color())
|
_reshaped_note.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||||
_shape_section.add_child(_reshaped_note)
|
shape_content.add_child(_reshaped_note)
|
||||||
|
|
||||||
_vertex_label = Label.new()
|
_vertex_label = Label.new()
|
||||||
_shape_section.add_child(_vertex_label)
|
shape_content.add_child(_vertex_label)
|
||||||
|
|
||||||
|
_create_point_button = Button.new()
|
||||||
|
_create_point_button.text = "📍 Create Point"
|
||||||
|
_create_point_button.tooltip_text = "Add a point to the selected shape's edge."
|
||||||
|
_create_point_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
|
_editor_visual_theme.style_button_role(_create_point_button, &"primary", &"prop")
|
||||||
|
_create_point_button.pressed.connect(_on_create_point_pressed)
|
||||||
|
shape_content.add_child(_create_point_button)
|
||||||
|
_create_point_hint = Label.new()
|
||||||
|
_create_point_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_create_point_hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||||
|
shape_content.add_child(_create_point_hint)
|
||||||
|
|
||||||
_delete_button = Button.new()
|
_delete_button = Button.new()
|
||||||
_delete_button.text = "Erase Shape"
|
_delete_button.text = "Erase Shape"
|
||||||
_delete_button.custom_minimum_size.y = float(_theme.button_min_height())
|
_delete_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
|
_editor_visual_theme.style_button_role(_delete_button, &"destructive", &"prop")
|
||||||
_delete_button.pressed.connect(_on_delete_shape_pressed)
|
_delete_button.pressed.connect(_on_delete_shape_pressed)
|
||||||
_shape_section.add_child(_delete_button)
|
shape_content.add_child(_delete_button)
|
||||||
|
|
||||||
# Add-shape section (hidden in Reshape mode, Round 3)
|
# Add-shape section (hidden in Reshape mode, Round 3)
|
||||||
_add_section = VBoxContainer.new()
|
_add_section = _make_inspector_card()
|
||||||
_add_section.add_theme_constant_override("separation", 8)
|
|
||||||
_inspector_list.add_child(_add_section)
|
_inspector_list.add_child(_add_section)
|
||||||
|
var add_content: VBoxContainer = _inspector_card_content(_add_section)
|
||||||
|
add_content.add_child(_make_inspector_header("Add Shape", PropInspectorIconsScript.section("add_shape")))
|
||||||
|
|
||||||
var add_title := Label.new()
|
_primitive_option = PropInspectorDropdown.new()
|
||||||
add_title.text = "Add Shape"
|
for index: int in PRIMITIVE_LABELS.size():
|
||||||
add_title.add_theme_font_size_override("font_size", header_size)
|
_primitive_option.add_icon_item(
|
||||||
_add_section.add_child(add_title)
|
PropInspectorIconsScript.shape(PRIMITIVE_IDS[index], _primitive_icon_color(PRIMITIVE_IDS[index])),
|
||||||
|
PRIMITIVE_LABELS[index]
|
||||||
_primitive_option = OptionButton.new()
|
)
|
||||||
for label: String in PRIMITIVE_LABELS:
|
|
||||||
_primitive_option.add_item(label)
|
|
||||||
_primitive_option.custom_minimum_size.y = float(_theme.button_min_height())
|
_primitive_option.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
|
_primitive_option.configure(
|
||||||
|
_ui_theme,
|
||||||
|
_theme.inspector_background_color(),
|
||||||
|
_editor_visual_theme.chrome_color(&"selected_surface", &"prop"),
|
||||||
|
_theme.border_color(),
|
||||||
|
_theme.accent_color(),
|
||||||
|
_editor_visual_theme.chrome_color(&"focus", &"prop"),
|
||||||
|
_theme.text_color(),
|
||||||
|
_theme.font_body()
|
||||||
|
)
|
||||||
_primitive_option.item_selected.connect(_on_primitive_selected)
|
_primitive_option.item_selected.connect(_on_primitive_selected)
|
||||||
_add_section.add_child(_primitive_option)
|
add_content.add_child(_primitive_option)
|
||||||
|
|
||||||
_params_box = VBoxContainer.new()
|
_params_box = VBoxContainer.new()
|
||||||
_params_box.add_theme_constant_override("separation", 4)
|
_params_box.add_theme_constant_override("separation", 4)
|
||||||
_add_section.add_child(_params_box)
|
add_content.add_child(_params_box)
|
||||||
|
|
||||||
_add_button = Button.new()
|
_add_button = Button.new()
|
||||||
_add_button.text = "Add a Shape"
|
_add_button.text = "Add a Shape"
|
||||||
_add_button.custom_minimum_size.y = float(_theme.button_min_height())
|
_add_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
|
_editor_visual_theme.style_button_role(_add_button, &"primary", &"prop")
|
||||||
_add_button.pressed.connect(_on_add_pressed)
|
_add_button.pressed.connect(_on_add_pressed)
|
||||||
_add_section.add_child(_add_button)
|
add_content.add_child(_add_button)
|
||||||
|
|
||||||
_apply_control_theme(_inspector_list)
|
_apply_control_theme(_inspector_list)
|
||||||
_rebuild_primitive_params()
|
_rebuild_primitive_params()
|
||||||
@@ -1218,8 +1243,119 @@ func _build_inspector() -> void:
|
|||||||
_refresh_tag_controls()
|
_refresh_tag_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _make_inspector_card() -> PanelContainer:
|
||||||
|
var card := PanelContainer.new()
|
||||||
|
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = _theme.inspector_background_color().darkened(0.07)
|
||||||
|
style.border_color = _theme.border_color()
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(10)
|
||||||
|
style.content_margin_left = 10.0
|
||||||
|
style.content_margin_top = 10.0
|
||||||
|
style.content_margin_right = 10.0
|
||||||
|
style.content_margin_bottom = 10.0
|
||||||
|
card.add_theme_stylebox_override("panel", style)
|
||||||
|
var content := VBoxContainer.new()
|
||||||
|
content.add_theme_constant_override("separation", 8)
|
||||||
|
card.add_child(content)
|
||||||
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
func _inspector_card_content(card: PanelContainer) -> VBoxContainer:
|
||||||
|
return card.get_child(0) as VBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
func _make_inspector_header(title: String, icon: Texture2D) -> HBoxContainer:
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
header.add_theme_constant_override("separation", 8)
|
||||||
|
var icon_view := TextureRect.new()
|
||||||
|
icon_view.texture = icon
|
||||||
|
icon_view.custom_minimum_size = Vector2(30.0, 30.0)
|
||||||
|
icon_view.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
icon_view.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
icon_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
header.add_child(icon_view)
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = title
|
||||||
|
label.add_theme_font_size_override("font_size", _theme.font_header())
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header.add_child(label)
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
func _make_stat_tile(parent: Control, caption_text: String) -> Array[Label]:
|
||||||
|
var tile := PanelContainer.new()
|
||||||
|
tile.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = _editor_visual_theme.chrome_color(&"background", &"prop").darkened(0.02)
|
||||||
|
style.border_color = _theme.border_color()
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 7.0
|
||||||
|
style.content_margin_top = 7.0
|
||||||
|
style.content_margin_right = 7.0
|
||||||
|
style.content_margin_bottom = 7.0
|
||||||
|
tile.add_theme_stylebox_override("panel", style)
|
||||||
|
parent.add_child(tile)
|
||||||
|
var content := VBoxContainer.new()
|
||||||
|
content.add_theme_constant_override("separation", 1)
|
||||||
|
tile.add_child(content)
|
||||||
|
var caption := Label.new()
|
||||||
|
caption.text = caption_text
|
||||||
|
caption.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
caption.add_theme_font_size_override("font_size", 14)
|
||||||
|
caption.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||||
|
content.add_child(caption)
|
||||||
|
var value := Label.new()
|
||||||
|
value.add_theme_font_size_override("font_size", 22)
|
||||||
|
content.add_child(value)
|
||||||
|
return [caption, value]
|
||||||
|
|
||||||
|
|
||||||
|
func _style_tag_button(button: Button) -> void:
|
||||||
|
var text_color: Color = _theme.text_color()
|
||||||
|
var surface: Color = _editor_visual_theme.chrome_color(&"background", &"prop")
|
||||||
|
var border: Color = _theme.border_color()
|
||||||
|
var accent: Color = _theme.accent_color()
|
||||||
|
button.add_theme_stylebox_override("normal", _make_tag_style(surface, border, 1))
|
||||||
|
button.add_theme_stylebox_override("hover", _make_tag_style(surface.lightened(0.12), accent, 1))
|
||||||
|
button.add_theme_stylebox_override("pressed", _make_tag_style(_editor_visual_theme.chrome_color(&"selected_surface", &"prop"), accent, 2))
|
||||||
|
button.add_theme_stylebox_override("focus", _make_tag_style(Color(0.0, 0.0, 0.0, 0.0), _editor_visual_theme.chrome_color(&"focus", &"prop"), 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("icon_normal_color", Color.WHITE)
|
||||||
|
button.add_theme_color_override("icon_hover_color", Color.WHITE)
|
||||||
|
button.add_theme_color_override("icon_pressed_color", Color.WHITE)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_tag_style(fill: Color, border: Color, border_width: int) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = fill
|
||||||
|
style.border_color = border
|
||||||
|
style.set_border_width_all(border_width)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 7.0
|
||||||
|
style.content_margin_top = 5.0
|
||||||
|
style.content_margin_right = 7.0
|
||||||
|
style.content_margin_bottom = 5.0
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _primitive_icon_color(primitive_id: String) -> Color:
|
||||||
|
match primitive_id:
|
||||||
|
"box": return Color("#70C8F0")
|
||||||
|
"plank": return Color("#F3B65D")
|
||||||
|
"triangle": return Color("#66C681")
|
||||||
|
"line": return Color("#DDE7F5")
|
||||||
|
"circle": return Color("#C9A3FF")
|
||||||
|
return _theme.accent_color()
|
||||||
|
|
||||||
|
|
||||||
func _build_stage_authoring_controls() -> void:
|
func _build_stage_authoring_controls() -> void:
|
||||||
var top_bar: HBoxContainer = get_node_or_null(NodePath("TopBar")) as HBoxContainer
|
var top_bar: HBoxContainer = get_node_or_null(NodePath("ChromeRoot/TopBar")) as HBoxContainer
|
||||||
if top_bar == null:
|
if top_bar == null:
|
||||||
return
|
return
|
||||||
var name_label: Label = top_bar.get_node_or_null(NodePath("NameLabel")) as Label
|
var name_label: Label = top_bar.get_node_or_null(NodePath("NameLabel")) as Label
|
||||||
@@ -1232,6 +1368,7 @@ func _build_stage_authoring_controls() -> void:
|
|||||||
_stage_place_button.custom_minimum_size.y = float(_theme.button_min_height())
|
_stage_place_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
_stage_place_button.size_flags_horizontal = Control.SIZE_SHRINK_END
|
_stage_place_button.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||||
_stage_place_button.theme = _ui_theme
|
_stage_place_button.theme = _ui_theme
|
||||||
|
_editor_visual_theme.style_button_role(_stage_place_button, &"primary", &"prop")
|
||||||
_stage_place_button.pressed.connect(_request_stage_return)
|
_stage_place_button.pressed.connect(_request_stage_return)
|
||||||
top_bar.add_child(_stage_place_button)
|
top_bar.add_child(_stage_place_button)
|
||||||
|
|
||||||
@@ -1257,29 +1394,30 @@ func _apply_authoring_context() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _build_tag_inspector() -> void:
|
func _build_tag_inspector() -> void:
|
||||||
_tag_section = VBoxContainer.new()
|
_tag_section = _make_inspector_card()
|
||||||
_tag_section.add_theme_constant_override("separation", 4)
|
|
||||||
_inspector_list.add_child(_tag_section)
|
_inspector_list.add_child(_tag_section)
|
||||||
var title := Label.new()
|
var tag_content: VBoxContainer = _inspector_card_content(_tag_section)
|
||||||
title.text = "Artwork Tags"
|
tag_content.add_child(_make_inspector_header("Artwork Tags", PropInspectorIconsScript.section("tags")))
|
||||||
title.add_theme_font_size_override("font_size", _theme.font_header())
|
|
||||||
_tag_section.add_child(title)
|
|
||||||
var hint := Label.new()
|
var hint := Label.new()
|
||||||
hint.text = "Choose a few so this art is easy to find."
|
hint.text = "Choose a few so this art is easy to find."
|
||||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
_tag_section.add_child(hint)
|
hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||||
|
tag_content.add_child(hint)
|
||||||
var rows := FlowContainer.new()
|
var rows := FlowContainer.new()
|
||||||
rows.add_theme_constant_override("h_separation", 6)
|
rows.add_theme_constant_override("h_separation", 6)
|
||||||
rows.add_theme_constant_override("v_separation", 4)
|
rows.add_theme_constant_override("v_separation", 6)
|
||||||
_tag_section.add_child(rows)
|
tag_content.add_child(rows)
|
||||||
for index: int in ART_TAG_IDS.size():
|
for index: int in ART_TAG_IDS.size():
|
||||||
var tag_id: String = ART_TAG_IDS[index]
|
var tag_id: String = ART_TAG_IDS[index]
|
||||||
var check := CheckBox.new()
|
var tag_button := Button.new()
|
||||||
check.text = ART_TAG_LABELS[index]
|
tag_button.text = ART_TAG_LABELS[index]
|
||||||
check.custom_minimum_size.y = float(_theme.button_min_height())
|
tag_button.toggle_mode = true
|
||||||
check.toggled.connect(_on_art_tag_toggled.bind(tag_id))
|
tag_button.custom_minimum_size = Vector2(122.0, 50.0)
|
||||||
rows.add_child(check)
|
tag_button.icon = PropInspectorIconsScript.tag(tag_id)
|
||||||
_tag_checks[tag_id] = check
|
tag_button.toggled.connect(_on_art_tag_toggled.bind(tag_id))
|
||||||
|
_style_tag_button(tag_button)
|
||||||
|
rows.add_child(tag_button)
|
||||||
|
_tag_checks[tag_id] = tag_button
|
||||||
|
|
||||||
|
|
||||||
func _on_art_tag_toggled(enabled: bool, tag_id: String) -> void:
|
func _on_art_tag_toggled(enabled: bool, tag_id: String) -> void:
|
||||||
@@ -1298,9 +1436,9 @@ func _refresh_tag_controls() -> void:
|
|||||||
return
|
return
|
||||||
_updating_inspector = true
|
_updating_inspector = true
|
||||||
for tag_id: String in ART_TAG_IDS:
|
for tag_id: String in ART_TAG_IDS:
|
||||||
var check: CheckBox = _tag_checks.get(tag_id) as CheckBox
|
var tag_button: Button = _tag_checks.get(tag_id) as Button
|
||||||
if check != null:
|
if tag_button != null:
|
||||||
check.set_pressed_no_signal(_tags.has(tag_id))
|
tag_button.set_pressed_no_signal(_tags.has(tag_id))
|
||||||
_updating_inspector = false
|
_updating_inspector = false
|
||||||
|
|
||||||
func _build_anchor_inspector() -> void:
|
func _build_anchor_inspector() -> void:
|
||||||
@@ -1312,6 +1450,8 @@ func _build_anchor_inspector() -> void:
|
|||||||
var actions := HBoxContainer.new(); _anchor_section.add_child(actions)
|
var actions := HBoxContainer.new(); _anchor_section.add_child(actions)
|
||||||
for label: String in ["Add Anchor", "Rename", "Delete"]:
|
for label: String in ["Add Anchor", "Rename", "Delete"]:
|
||||||
var button := Button.new(); button.text = label; button.custom_minimum_size.y = float(_theme.button_min_height()); actions.add_child(button)
|
var button := Button.new(); button.text = label; button.custom_minimum_size.y = float(_theme.button_min_height()); actions.add_child(button)
|
||||||
|
var role: StringName = &"primary" if label == "Add Anchor" else (&"destructive" if label == "Delete" else &"secondary")
|
||||||
|
_editor_visual_theme.style_button_role(button, role, &"prop")
|
||||||
if label == "Add Anchor": button.pressed.connect(_on_add_anchor_pressed)
|
if label == "Add Anchor": button.pressed.connect(_on_add_anchor_pressed)
|
||||||
elif label == "Rename":
|
elif label == "Rename":
|
||||||
_rename_anchor_button = button
|
_rename_anchor_button = button
|
||||||
@@ -1324,11 +1464,17 @@ func _build_anchor_inspector() -> void:
|
|||||||
_anchor_x = _anchor_spin("X", -100000.0, 100000.0, _on_anchor_x_changed)
|
_anchor_x = _anchor_spin("X", -100000.0, 100000.0, _on_anchor_x_changed)
|
||||||
_anchor_y = _anchor_spin("Y", -100000.0, 100000.0, _on_anchor_y_changed)
|
_anchor_y = _anchor_spin("Y", -100000.0, 100000.0, _on_anchor_y_changed)
|
||||||
_anchor_rotation = _anchor_spin("Rotation", -180.0, 179.0, _on_anchor_rotation_changed)
|
_anchor_rotation = _anchor_spin("Rotation", -180.0, 179.0, _on_anchor_rotation_changed)
|
||||||
_anchor_z = _anchor_spin("Z Offset", -5.0, 5.0, _on_anchor_z_changed)
|
_anchor_z = _anchor_spin("Held prop layer", -5.0, 5.0, _on_anchor_z_changed)
|
||||||
_anchor_z.step = 1.0
|
_anchor_z.step = 1.0
|
||||||
|
var anchor_layer_hint := Label.new()
|
||||||
|
anchor_layer_hint.text = "Negative: behind · 0: normal order · Positive: farther in front."
|
||||||
|
anchor_layer_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
anchor_layer_hint.add_theme_font_size_override("font_size", _theme.font_status())
|
||||||
|
anchor_layer_hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||||
|
_anchor_section.add_child(anchor_layer_hint)
|
||||||
var presets := FlowContainer.new(); _anchor_section.add_child(presets)
|
var presets := FlowContainer.new(); _anchor_section.add_child(presets)
|
||||||
for entry: Dictionary in [{"text":"One-Handed","id":"one_handed"}, {"text":"Two-Handed","id":"two_handed"}, {"text":"Tool / Nozzle","id":"tool_nozzle"}, {"text":"Melee Edge","id":"melee_edge"}]:
|
for entry: Dictionary in [{"text":"One-Handed","id":"one_handed"}, {"text":"Two-Handed","id":"two_handed"}, {"text":"Tool / Nozzle","id":"tool_nozzle"}, {"text":"Melee Edge","id":"melee_edge"}]:
|
||||||
var button := Button.new(); button.text = str(entry["text"]); button.pressed.connect(func() -> void: _apply_anchor_preset(str(entry["id"]))); presets.add_child(button)
|
var button := Button.new(); button.text = str(entry["text"]); button.custom_minimum_size.y = float(_theme.button_min_height()); button.pressed.connect(func() -> void: _apply_anchor_preset(str(entry["id"]))); presets.add_child(button)
|
||||||
_refresh_anchor_inspector()
|
_refresh_anchor_inspector()
|
||||||
|
|
||||||
func _anchor_option(label_text: String, entries: PackedStringArray, callback: Callable) -> OptionButton:
|
func _anchor_option(label_text: String, entries: PackedStringArray, callback: Callable) -> OptionButton:
|
||||||
@@ -1341,6 +1487,7 @@ func _anchor_option(label_text: String, entries: PackedStringArray, callback: Ca
|
|||||||
row.add_child(label)
|
row.add_child(label)
|
||||||
var option := OptionButton.new()
|
var option := OptionButton.new()
|
||||||
option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
option.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
for entry: String in entries:
|
for entry: String in entries:
|
||||||
option.add_item(entry)
|
option.add_item(entry)
|
||||||
option.item_selected.connect(callback)
|
option.item_selected.connect(callback)
|
||||||
@@ -1360,6 +1507,7 @@ func _anchor_spin(label_text: String, minimum: float, maximum: float, callback:
|
|||||||
spin.max_value = maximum
|
spin.max_value = maximum
|
||||||
spin.step = 1.0
|
spin.step = 1.0
|
||||||
spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
spin.custom_minimum_size.y = float(_theme.button_min_height())
|
||||||
spin.value_changed.connect(callback)
|
spin.value_changed.connect(callback)
|
||||||
row.add_child(spin)
|
row.add_child(spin)
|
||||||
return spin
|
return spin
|
||||||
@@ -1400,13 +1548,17 @@ func _on_rename_anchor_pressed() -> void:
|
|||||||
_rename_anchor_source_id = _selected_anchor_id
|
_rename_anchor_source_id = _selected_anchor_id
|
||||||
_rename_anchor_edit.text = _rename_anchor_source_id
|
_rename_anchor_edit.text = _rename_anchor_source_id
|
||||||
_update_rename_anchor_confirmation(_rename_anchor_edit.text)
|
_update_rename_anchor_confirmation(_rename_anchor_edit.text)
|
||||||
_rename_anchor_dialog.popup_centered(Vector2i(420, 150))
|
StageDialogStylerScript.popup(_rename_anchor_dialog)
|
||||||
_rename_anchor_edit.call_deferred("grab_focus")
|
_rename_anchor_edit.call_deferred("grab_focus")
|
||||||
_rename_anchor_edit.call_deferred("select_all")
|
_rename_anchor_edit.call_deferred("select_all")
|
||||||
|
|
||||||
func _update_rename_anchor_confirmation(value: String) -> void:
|
func _update_rename_anchor_confirmation(value: String) -> void:
|
||||||
if _rename_anchor_dialog != null:
|
if _rename_anchor_dialog != null:
|
||||||
_rename_anchor_dialog.get_ok_button().disabled = value.strip_edges().is_empty()
|
var native_button: Button = _rename_anchor_dialog.get_ok_button()
|
||||||
|
native_button.disabled = value.strip_edges().is_empty()
|
||||||
|
var visible_button: Button = StageDialogStylerScript.visual_button(native_button)
|
||||||
|
if visible_button != native_button:
|
||||||
|
visible_button.disabled = native_button.disabled
|
||||||
|
|
||||||
func _commit_anchor_rename() -> void:
|
func _commit_anchor_rename() -> void:
|
||||||
if _rename_anchor_source_id.is_empty() or not _anchors.has(_rename_anchor_source_id):
|
if _rename_anchor_source_id.is_empty() or not _anchors.has(_rename_anchor_source_id):
|
||||||
@@ -1466,15 +1618,16 @@ func _apply_control_theme(node: Node) -> void:
|
|||||||
|
|
||||||
func _update_physics_labels() -> void:
|
func _update_physics_labels() -> void:
|
||||||
var mat: PhysicsMaterial = PropBlockScript.physics_material(_material_preset)
|
var mat: PhysicsMaterial = PropBlockScript.physics_material(_material_preset)
|
||||||
_mass_label.text = "Mass: %.1f" % PropBlockScript.mass_for(_material_preset)
|
_mass_value.text = "%.1f" % PropBlockScript.mass_for(_material_preset)
|
||||||
_friction_label.text = "Friction: %.2f" % mat.friction
|
_friction_value.text = "%.2f" % mat.friction
|
||||||
_bounce_label.text = "Bounce: %.2f" % mat.bounce
|
_friction_label.tooltip_text = "Higher values make sliding harder."
|
||||||
|
_bounce_value.text = "%.2f" % mat.bounce
|
||||||
|
|
||||||
|
|
||||||
func _on_preset_selected(index: int) -> void:
|
func _on_preset_selected(index: int) -> void:
|
||||||
_material_preset = clampi(index, 0, PRESET_LABELS.size() - 1)
|
_material_preset = clampi(index, 0, PRESET_LABELS.size() - 1)
|
||||||
_update_physics_labels()
|
_update_physics_labels()
|
||||||
_update_save_status()
|
_mark_dirty()
|
||||||
|
|
||||||
|
|
||||||
func _rebuild_primitive_params() -> void:
|
func _rebuild_primitive_params() -> void:
|
||||||
@@ -1604,6 +1757,7 @@ func _refresh_inspector_from_selection() -> void:
|
|||||||
func _on_canvas_shape_changed(_shapes: Array[Dictionary]) -> void:
|
func _on_canvas_shape_changed(_shapes: Array[Dictionary]) -> void:
|
||||||
_mark_dirty()
|
_mark_dirty()
|
||||||
_refresh_inspector_from_selection()
|
_refresh_inspector_from_selection()
|
||||||
|
_sync_mode_controls(_canvas.get_edit_mode())
|
||||||
_refresh_anchor_inspector()
|
_refresh_anchor_inspector()
|
||||||
_refresh_center_prop_button()
|
_refresh_center_prop_button()
|
||||||
_sync_attach_preview()
|
_sync_attach_preview()
|
||||||
@@ -1700,6 +1854,25 @@ func _center_prop_on_origin(focus_view: bool = true) -> bool:
|
|||||||
|
|
||||||
func _on_canvas_shape_selected(_idx: int) -> void:
|
func _on_canvas_shape_selected(_idx: int) -> void:
|
||||||
_refresh_inspector_from_selection()
|
_refresh_inspector_from_selection()
|
||||||
|
_sync_mode_controls(_canvas.get_edit_mode())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_create_point_pressed() -> void:
|
||||||
|
if _canvas.get_selected_index() < 0:
|
||||||
|
return
|
||||||
|
_canvas.arm_create_point()
|
||||||
|
_sync_mode_buttons(_canvas.get_edit_mode())
|
||||||
|
_sync_mode_controls(_canvas.get_edit_mode())
|
||||||
|
_update_create_point_feedback()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_create_point_feedback() -> void:
|
||||||
|
if _create_point_button == null or _create_point_hint == null or _canvas == null:
|
||||||
|
return
|
||||||
|
var armed: bool = _canvas.is_create_point_armed()
|
||||||
|
_create_point_button.text = "Click an Edge to Place Point" if armed else "📍 Create Point"
|
||||||
|
_create_point_button.tooltip_text = "Press Esc to cancel." if armed else "Add a point to the selected shape's edge."
|
||||||
|
_create_point_hint.text = "Click near an edge to place the point. Press Esc to cancel." if armed else "Adds a point where you click on the selected edge."
|
||||||
|
|
||||||
|
|
||||||
func _on_canvas_copy_requested(shape: Dictionary) -> void:
|
func _on_canvas_copy_requested(shape: Dictionary) -> void:
|
||||||
@@ -1768,11 +1941,16 @@ func _check_autosave_recovery() -> void:
|
|||||||
|
|
||||||
var dialog := ConfirmationDialog.new()
|
var dialog := ConfirmationDialog.new()
|
||||||
dialog.title = "Restore Prop?"
|
dialog.title = "Restore Prop?"
|
||||||
dialog.dialog_text = "An unsaved prop was recovered. Restore it?"
|
dialog.set_meta(StageDialogStylerScript.ICON_META, "🔄")
|
||||||
dialog.ok_button_text = "🔄 Restore"
|
dialog.dialog_text = "An unsaved prop was recovered. Restore this work?"
|
||||||
dialog.add_button("🗑️ Discard", false, "discard")
|
dialog.ok_button_text = "Restore"
|
||||||
dialog.theme = _ui_theme
|
var discard_button: Button = dialog.add_button("Discard", false, "discard")
|
||||||
|
dialog.get_cancel_button().text = "Cancel"
|
||||||
add_child(dialog)
|
add_child(dialog)
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
dialog.get_ok_button(), [discard_button]
|
||||||
|
)
|
||||||
dialog.confirmed.connect(func() -> void:
|
dialog.confirmed.connect(func() -> void:
|
||||||
_apply_json_data(raw, true)
|
_apply_json_data(raw, true)
|
||||||
_current_path = source_path
|
_current_path = source_path
|
||||||
@@ -1782,7 +1960,8 @@ func _check_autosave_recovery() -> void:
|
|||||||
if action == &"discard":
|
if action == &"discard":
|
||||||
_clear_autosave()
|
_clear_autosave()
|
||||||
dialog.queue_free())
|
dialog.queue_free())
|
||||||
dialog.popup_centered()
|
dialog.canceled.connect(func() -> void: dialog.queue_free())
|
||||||
|
StageDialogStylerScript.popup(dialog)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1858,7 +2037,7 @@ func _begin_save_request(path: String, with_thumbnail: bool) -> void:
|
|||||||
_pending_save_path = path
|
_pending_save_path = path
|
||||||
_pending_save_with_thumbnail = with_thumbnail
|
_pending_save_with_thumbnail = with_thumbnail
|
||||||
if should_warn_off_origin(_canvas.has_shape_geometry(), _canvas.get_shapes_bounds()):
|
if should_warn_off_origin(_canvas.has_shape_geometry(), _canvas.get_shapes_bounds()):
|
||||||
_off_origin_save_dialog.popup_centered()
|
StageDialogStylerScript.popup(_off_origin_save_dialog)
|
||||||
return
|
return
|
||||||
await _execute_pending_save(false)
|
await _execute_pending_save(false)
|
||||||
|
|
||||||
@@ -1923,4 +2102,4 @@ func _on_load_file_selected(path: String) -> void:
|
|||||||
|
|
||||||
func _show_error(message: String) -> void:
|
func _show_error(message: String) -> void:
|
||||||
_error_dialog.dialog_text = message
|
_error_dialog.dialog_text = message
|
||||||
_error_dialog.popup_centered()
|
StageDialogStylerScript.popup(_error_dialog)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class_name PropInspectorDropdown
|
||||||
|
extends EditorSelectionDropdown
|
||||||
|
## Prop-specific icon adapter for the shared editor selection dropdown.
|
||||||
|
|
||||||
|
const PropInspectorIconsScript := preload("res://scripts/editors/prop/prop_inspector_icons.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func add_icon_item(texture: Texture2D, label: String) -> void:
|
||||||
|
add_styled_item(label, texture, PropInspectorIconsScript.popup_item(texture))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bnnujoeli7cwk
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
class_name PropInspectorIcons
|
||||||
|
extends RefCounted
|
||||||
|
## Small flat 2D glyphs for the Prop Editor inspector.
|
||||||
|
## SVG is rasterized at runtime so colors can follow the editor and material tokens.
|
||||||
|
|
||||||
|
const ICON_SIZE := 24
|
||||||
|
|
||||||
|
static func material(preset: int, tint: Color) -> Texture2D:
|
||||||
|
if preset == 0:
|
||||||
|
return _texture(_svg('<circle cx="24" cy="24" r="15" fill="#252B38" stroke="#C4D1E5" stroke-width="4"/>'))
|
||||||
|
return _texture(_svg('<circle cx="24" cy="24" r="15" fill="%s" stroke="#FFFFFF" stroke-opacity="0.35" stroke-width="3"/>' % _hex(tint)))
|
||||||
|
|
||||||
|
|
||||||
|
static func shape(shape_id: String, tint: Color = Color("#22C6FF")) -> Texture2D:
|
||||||
|
var color := _hex(tint)
|
||||||
|
var content := ""
|
||||||
|
match shape_id:
|
||||||
|
"box":
|
||||||
|
content = '<rect x="8" y="8" width="32" height="32" rx="2" fill="%s"/>' % color
|
||||||
|
"plank":
|
||||||
|
content = '<rect x="5" y="16" width="38" height="16" rx="2" fill="%s"/>' % color
|
||||||
|
"triangle":
|
||||||
|
content = '<polygon points="24,6 44,41 4,41" fill="%s"/>' % color
|
||||||
|
"line":
|
||||||
|
content = '<path d="M8 38 L40 10" fill="none" stroke="%s" stroke-width="6" stroke-linecap="round"/>' % color
|
||||||
|
"circle":
|
||||||
|
content = '<circle cx="24" cy="24" r="16" fill="%s"/>' % color
|
||||||
|
_:
|
||||||
|
content = '<rect x="8" y="8" width="32" height="32" rx="2" fill="%s"/>' % color
|
||||||
|
return _texture(_svg(content))
|
||||||
|
|
||||||
|
|
||||||
|
static func section(section_id: String) -> Texture2D:
|
||||||
|
var content := ""
|
||||||
|
match section_id:
|
||||||
|
"material":
|
||||||
|
content = '<rect x="7" y="9" width="14" height="14" rx="3" fill="#F3B65D"/><rect x="27" y="9" width="14" height="14" rx="3" fill="#E76D66"/><rect x="7" y="27" width="14" height="14" rx="3" fill="#66C681"/><rect x="27" y="27" width="14" height="14" rx="3" fill="#70BFE8"/>'
|
||||||
|
"selected_shape":
|
||||||
|
content = '<path d="M9 32 L16 11 L38 15 L35 37 Z" fill="#DDE7F5" stroke="#22C6FF" stroke-width="3" stroke-linejoin="round"/><circle cx="9" cy="32" r="3" fill="#FFFFFF"/><circle cx="16" cy="11" r="3" fill="#FFFFFF"/><circle cx="38" cy="15" r="3" fill="#FFFFFF"/><circle cx="35" cy="37" r="3" fill="#FFFFFF"/>'
|
||||||
|
"add_shape":
|
||||||
|
content = '<rect x="7" y="7" width="34" height="34" fill="none" stroke="#22C6FF" stroke-width="5"/><path d="M24 15 V33 M15 24 H33" stroke="#22C6FF" stroke-width="4" stroke-linecap="round"/>'
|
||||||
|
"tags":
|
||||||
|
content = '<path d="M7 17 L17 7 H34 L42 15 V32 L25 42 L7 24 Z" fill="#FFB84D"/><circle cx="31" cy="16" r="3.5" fill="#252B38"/>'
|
||||||
|
return _texture(_svg(content))
|
||||||
|
|
||||||
|
|
||||||
|
static func tag(tag_id: String) -> Texture2D:
|
||||||
|
var color := "#66C681"
|
||||||
|
var body := ""
|
||||||
|
match tag_id:
|
||||||
|
"nature":
|
||||||
|
color = "#66C681"
|
||||||
|
body = '<path d="M9 35 C9 18 21 8 39 8 C39 26 29 39 13 39 Z" fill="%s"/><path d="M12 38 L33 15" stroke="#E8FFF0" stroke-width="3" stroke-linecap="round"/>' % color
|
||||||
|
"buildings":
|
||||||
|
color = "#F3A653"
|
||||||
|
body = '<path d="M10 40 V10 H29 V18 H38 V40 Z" fill="%s"/><path d="M16 16 H21 M16 23 H21 M16 30 H21 M31 24 H35 M31 31 H35" stroke="#3C2A22" stroke-width="3"/>' % color
|
||||||
|
"walls":
|
||||||
|
color = "#F36B5B"
|
||||||
|
body = '<path d="M5 10 H21 V19 H5 Z M24 10 H42 V19 H24 Z M14 22 H31 V31 H14 Z M4 34 H20 V42 H4 Z M23 34 H42 V42 H23 Z" fill="%s"/><path d="M22 10 V19 M13 22 V31 M31 34 V42" stroke="#733A37" stroke-width="2"/>' % color
|
||||||
|
"roads":
|
||||||
|
color = "#9BAEC9"
|
||||||
|
body = '<path d="M16 7 H32 L42 41 H6 Z" fill="%s"/><path d="M24 12 V19 M24 25 V31 M24 36 V40" stroke="#283247" stroke-width="3" stroke-linecap="round"/>' % color
|
||||||
|
"objects":
|
||||||
|
color = "#E8A96D"
|
||||||
|
body = '<rect x="7" y="11" width="34" height="26" fill="none" stroke="%s" stroke-width="5"/>' % color
|
||||||
|
return _texture(_svg(body))
|
||||||
|
|
||||||
|
|
||||||
|
static func popup_item(texture: Texture2D, size: int = 44) -> Texture2D:
|
||||||
|
if texture == null:
|
||||||
|
return null
|
||||||
|
var source: Image = texture.get_image()
|
||||||
|
var canvas := Image.create(size, size, false, Image.FORMAT_RGBA8)
|
||||||
|
var draw_size: Vector2i = Vector2i(mini(source.get_width(), size), mini(source.get_height(), size))
|
||||||
|
var source_rect := Rect2i(Vector2i.ZERO, draw_size)
|
||||||
|
var destination := Vector2i((size - draw_size.x) / 2, (size - draw_size.y) / 2)
|
||||||
|
canvas.blit_rect(source, source_rect, destination)
|
||||||
|
return ImageTexture.create_from_image(canvas)
|
||||||
|
|
||||||
|
|
||||||
|
static func _svg(content: String) -> String:
|
||||||
|
return '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 48 48">%s</svg>' % [ICON_SIZE, ICON_SIZE, content]
|
||||||
|
|
||||||
|
|
||||||
|
static func _texture(svg: String) -> Texture2D:
|
||||||
|
var image := Image.new()
|
||||||
|
var error: Error = image.load_svg_from_string(svg)
|
||||||
|
if error != OK:
|
||||||
|
push_warning("Prop Inspector icon could not be rasterized (%s)." % error_string(error))
|
||||||
|
return null
|
||||||
|
return ImageTexture.create_from_image(image)
|
||||||
|
|
||||||
|
|
||||||
|
static func _hex(color: Color) -> String:
|
||||||
|
return "#" + color.to_html(false)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://5ac0640i4bj7
|
||||||
@@ -10,6 +10,7 @@ extends RefCounted
|
|||||||
## truth.
|
## truth.
|
||||||
|
|
||||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||||
|
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||||
|
|
||||||
const SCHEMA_VERSION: String = "1.0"
|
const SCHEMA_VERSION: String = "1.0"
|
||||||
const DEFAULT_PATH: String = "res://prop_theme.json"
|
const DEFAULT_PATH: String = "res://prop_theme.json"
|
||||||
@@ -39,19 +40,16 @@ const DEFAULT_CONFIG: Dictionary = {
|
|||||||
"background_color": "#1F1F1F",
|
"background_color": "#1F1F1F",
|
||||||
"grid_color": "#FFFFFF26",
|
"grid_color": "#FFFFFF26",
|
||||||
},
|
},
|
||||||
"ui": {
|
|
||||||
"fonts": {"body": 18, "header": 24, "menu": 22, "menu_bar": 22, "mode": 26, "status": 18, "prop_name": 20, "context_menu": 22},
|
|
||||||
"hit_targets": {"button_min_height": 36, "row_min_height": 36, "mode_button_min_height": 56},
|
|
||||||
"colors": {"text": "#F2F2F2", "text_muted": "#9A9A9A", "border": "#FFFFFF33", "accent": "#4FA3FF", "inspector_background": "#262C39"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _config: Dictionary = DEFAULT_CONFIG.duplicate(true)
|
var _config: Dictionary = DEFAULT_CONFIG.duplicate(true)
|
||||||
var _warned: Dictionary = {}
|
var _warned: Dictionary = {}
|
||||||
|
var _editor_visual_theme: EditorVisualTheme = EditorVisualThemeScript.new()
|
||||||
|
|
||||||
|
|
||||||
func load_from(path: String = DEFAULT_PATH) -> bool:
|
func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||||
invalidate_caches()
|
invalidate_caches()
|
||||||
|
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||||
_config = DEFAULT_CONFIG.duplicate(true)
|
_config = DEFAULT_CONFIG.duplicate(true)
|
||||||
if not FileAccess.file_exists(path):
|
if not FileAccess.file_exists(path):
|
||||||
_warn_once(path, "PropTheme: '%s' was not found; using defaults." % path)
|
_warn_once(path, "PropTheme: '%s' was not found; using defaults." % path)
|
||||||
@@ -155,71 +153,71 @@ func canvas_grid_color() -> Color:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Kid-friendly UI tokens (Round 2)
|
# Compatibility accessors: editor chrome is owned by EditorVisualTheme.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func font_body() -> int:
|
func font_body() -> int:
|
||||||
return integer("ui.fonts.body", 18, 8, 72)
|
return _editor_visual_theme.chrome_px(&"body", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_header() -> int:
|
func font_header() -> int:
|
||||||
return integer("ui.fonts.header", 24, 8, 96)
|
return _editor_visual_theme.chrome_px(&"section", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_menu() -> int:
|
func font_menu() -> int:
|
||||||
return integer("ui.fonts.menu", 22, 8, 72)
|
return _editor_visual_theme.chrome_px(&"menu", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_status() -> int:
|
func font_status() -> int:
|
||||||
return integer("ui.fonts.status", 18, 8, 72)
|
return _editor_visual_theme.chrome_px(&"status", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_mode() -> int:
|
func font_mode() -> int:
|
||||||
return integer("ui.fonts.mode", 26, 8, 96)
|
return _editor_visual_theme.chrome_px(&"mode", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_menu_bar() -> int:
|
func font_menu_bar() -> int:
|
||||||
return integer("ui.fonts.menu_bar", 22, 8, 72)
|
return _editor_visual_theme.chrome_px(&"menu", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_prop_name() -> int:
|
func font_prop_name() -> int:
|
||||||
return integer("ui.fonts.prop_name", 20, 8, 96)
|
return _editor_visual_theme.chrome_px(&"prop_name", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func font_context_menu() -> int:
|
func font_context_menu() -> int:
|
||||||
return integer("ui.fonts.context_menu", 22, 8, 96)
|
return _editor_visual_theme.chrome_px(&"context_menu", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func button_min_height() -> int:
|
func button_min_height() -> int:
|
||||||
return integer("ui.hit_targets.button_min_height", 36, 16, 200)
|
return _editor_visual_theme.chrome_px(&"button_min_height", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func row_min_height() -> int:
|
func row_min_height() -> int:
|
||||||
return integer("ui.hit_targets.row_min_height", 36, 16, 200)
|
return _editor_visual_theme.chrome_px(&"row_min_height", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func mode_button_min_height() -> int:
|
func mode_button_min_height() -> int:
|
||||||
return integer("ui.hit_targets.mode_button_min_height", 56, 16, 200)
|
return _editor_visual_theme.chrome_px(&"mode_button_min_height", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func text_color() -> Color:
|
func text_color() -> Color:
|
||||||
return color("ui.colors.text", Color("#F2F2F2"))
|
return _editor_visual_theme.chrome_color(&"text", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func text_muted_color() -> Color:
|
func text_muted_color() -> Color:
|
||||||
return color("ui.colors.text_muted", Color("#9A9A9A"))
|
return _editor_visual_theme.chrome_color(&"text_muted", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func border_color() -> Color:
|
func border_color() -> Color:
|
||||||
return color("ui.colors.border", Color(1.0, 1.0, 1.0, 0.2))
|
return _editor_visual_theme.chrome_color(&"border", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func accent_color() -> Color:
|
func accent_color() -> Color:
|
||||||
return color("ui.colors.accent", Color("#4FA3FF"))
|
return _editor_visual_theme.chrome_color(&"edit", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func inspector_background_color() -> Color:
|
func inspector_background_color() -> Color:
|
||||||
return color("ui.colors.inspector_background", Color("#262C39"))
|
return _editor_visual_theme.chrome_color(&"surface", &"prop")
|
||||||
|
|
||||||
|
|
||||||
func invalidate_caches() -> void:
|
func invalidate_caches() -> void:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const STAGE_RUN_SESSION_SCRIPT := preload("res://scripts/editors/stage/stage_run
|
|||||||
const STAGE_IMPACT_RESOLVER_SCRIPT := preload("res://scripts/stage/stage_impact_resolver.gd")
|
const STAGE_IMPACT_RESOLVER_SCRIPT := preload("res://scripts/stage/stage_impact_resolver.gd")
|
||||||
const STAGE_ASSET_BROWSER_SCRIPT := preload("res://scripts/editors/stage/stage_asset_browser.gd")
|
const STAGE_ASSET_BROWSER_SCRIPT := preload("res://scripts/editors/stage/stage_asset_browser.gd")
|
||||||
const STAGE_MODAL_VIEWS_SCRIPT := preload("res://scripts/editors/stage/stage_modal_views.gd")
|
const STAGE_MODAL_VIEWS_SCRIPT := preload("res://scripts/editors/stage/stage_modal_views.gd")
|
||||||
|
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
const STAGE_THEME_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_theme_controller.gd")
|
const STAGE_THEME_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_theme_controller.gd")
|
||||||
const STAGE_TARGET_CAPTURE_SCRIPT := preload("res://scripts/editors/stage/stage_target_capture_session.gd")
|
const STAGE_TARGET_CAPTURE_SCRIPT := preload("res://scripts/editors/stage/stage_target_capture_session.gd")
|
||||||
const STAGE_SCENERY_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_scenery_controller.gd")
|
const STAGE_SCENERY_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_scenery_controller.gd")
|
||||||
@@ -62,6 +63,7 @@ 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_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_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_SURFACE_PREVIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
|
||||||
|
const STAGE_GROUND_FINISH_CHOOSER_SCRIPT := preload("res://scripts/shared/stage_ui/stage_ground_finish_chooser.gd")
|
||||||
const STAGE_DEPTH_CHOICE_PREVIEW_SCRIPT := preload("res://scripts/shared/stage_ui/stage_depth_choice_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_FILE_SCRIPT := preload("res://scripts/editors/prop/prop_file.gd")
|
||||||
const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||||
@@ -276,7 +278,7 @@ var _plain_color_draft: Color = Color("#F5EFE0")
|
|||||||
var _new_ground_finish: StringName = &"finish_ground_grass"
|
var _new_ground_finish: StringName = &"finish_ground_grass"
|
||||||
var _hosted_prop_editor: PropEditor
|
var _hosted_prop_editor: PropEditor
|
||||||
var _hosted_artwork_path: String = ""
|
var _hosted_artwork_path: String = ""
|
||||||
var _ground_finish_popup: PopupMenu
|
var _ground_finish_chooser: StageGroundFinishChooser
|
||||||
var _scenery_edit_mode: bool = false
|
var _scenery_edit_mode: bool = false
|
||||||
var _scenery_multi_select: bool = false
|
var _scenery_multi_select: bool = false
|
||||||
var _scenery_single_dragging: bool = false
|
var _scenery_single_dragging: bool = false
|
||||||
@@ -1543,11 +1545,28 @@ func _place_speech_bubble_at(viewport_position: Vector2) -> void:
|
|||||||
_speech_session.place_bubble_at(viewport_position)
|
_speech_session.place_bubble_at(viewport_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _active_styled_dialog() -> Window:
|
||||||
|
if _chrome == null:
|
||||||
|
return null
|
||||||
|
var ui_layer: CanvasLayer = _chrome.get_canvas_layer()
|
||||||
|
for node: Node in ui_layer.find_children("*", "Window", true, false):
|
||||||
|
var dialog: Window = node as Window
|
||||||
|
if dialog != null and STAGE_DIALOG_STYLER.is_styled(dialog) and STAGE_DIALOG_STYLER.is_open(dialog):
|
||||||
|
return dialog
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_key_input(event: InputEvent) -> void:
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
if _hosted_prop_editor != null:
|
if _hosted_prop_editor != null:
|
||||||
return
|
return
|
||||||
if not (event is InputEventKey) or not event.pressed or event.echo:
|
if not (event is InputEventKey) or not event.pressed or event.echo:
|
||||||
return
|
return
|
||||||
|
var active_dialog: Window = _active_styled_dialog()
|
||||||
|
if active_dialog != null:
|
||||||
|
if event.keycode == KEY_ESCAPE:
|
||||||
|
STAGE_DIALOG_STYLER.dismiss_on_escape(active_dialog)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible:
|
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible:
|
||||||
if event.keycode == KEY_ESCAPE:
|
if event.keycode == KEY_ESCAPE:
|
||||||
_dismiss_scenery_erase_piece_dialog()
|
_dismiss_scenery_erase_piece_dialog()
|
||||||
@@ -1670,7 +1689,7 @@ func _space_should_pass_to_gui() -> bool:
|
|||||||
if focus is LineEdit or focus is TextEdit or focus is SpinBox:
|
if focus is LineEdit or focus is TextEdit or focus is SpinBox:
|
||||||
return true
|
return true
|
||||||
for window: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]:
|
for window: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]:
|
||||||
if window != null and window.visible:
|
if window != null and STAGE_DIALOG_STYLER.is_open(window):
|
||||||
return true
|
return true
|
||||||
if _selector_open or (_queue_panel != null and _queue_panel.visible) or (_rule_panel != null and _rule_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible):
|
if _selector_open or (_queue_panel != null and _queue_panel.visible) or (_rule_panel != null and _rule_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible):
|
||||||
return true
|
return true
|
||||||
@@ -1711,9 +1730,13 @@ func _handle_world_click(mb: InputEventMouseButton) -> void:
|
|||||||
return
|
return
|
||||||
if _scenery_controller != null and _scenery_controller.is_active():
|
if _scenery_controller != null and _scenery_controller.is_active():
|
||||||
var scenery_world_position: Vector2 = _viewport_to_world(mb.position)
|
var scenery_world_position: Vector2 = _viewport_to_world(mb.position)
|
||||||
|
var return_to_scenery_selector: bool = mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed and _scenery_controller.is_placement_tool()
|
||||||
if _scenery_controller.handle_mouse_button(mb, scenery_world_position, _is_mouse_over_ui()):
|
if _scenery_controller.handle_mouse_button(mb, scenery_world_position, _is_mouse_over_ui()):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
_refresh_scenery_capture_bar()
|
if return_to_scenery_selector:
|
||||||
|
_return_to_scenery_selector()
|
||||||
|
else:
|
||||||
|
_refresh_scenery_capture_bar()
|
||||||
return
|
return
|
||||||
if _scenery_edit_mode:
|
if _scenery_edit_mode:
|
||||||
var edit_world_position: Vector2 = _viewport_to_world(mb.position)
|
var edit_world_position: Vector2 = _viewport_to_world(mb.position)
|
||||||
@@ -2074,7 +2097,7 @@ func _pause_toggle_blocked() -> bool:
|
|||||||
if focus is LineEdit or focus is TextEdit or focus is SpinBox:
|
if focus is LineEdit or focus is TextEdit or focus is SpinBox:
|
||||||
return true
|
return true
|
||||||
for dialog: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]:
|
for dialog: Window in [_browse_dialog, _speak_dialog, _wait_dialog, _scene_open_dialog, _scene_save_dialog, _speech_save_gate_dialog, _group_preview_save_dialog, _scenery_repair_confirm_dialog, _missing_scenery_dialog, _rename_scene_dialog, _unsaved_changes_dialog, _missing_assets_dialog, _cli_error_dialog, _confirm_dialog]:
|
||||||
if dialog != null and dialog.visible:
|
if dialog != null and STAGE_DIALOG_STYLER.is_open(dialog):
|
||||||
return true
|
return true
|
||||||
return (_queue_panel != null and _queue_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible)
|
return (_queue_panel != null and _queue_panel.visible) or (_action_editor != null and _action_editor.visible) or (_rule_editor != null and _rule_editor.visible)
|
||||||
|
|
||||||
@@ -2630,8 +2653,8 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool:
|
|||||||
if is_autosave:
|
if is_autosave:
|
||||||
return false
|
return false
|
||||||
_pending_group_save_path = path
|
_pending_group_save_path = path
|
||||||
if _group_preview_save_dialog != null and not _group_preview_save_dialog.visible:
|
if _group_preview_save_dialog != null and not STAGE_DIALOG_STYLER.is_open(_group_preview_save_dialog):
|
||||||
_group_preview_save_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_group_preview_save_dialog)
|
||||||
return false
|
return false
|
||||||
if _speech_session != null and _speech_session.is_save_blocked():
|
if _speech_session != null and _speech_session.is_save_blocked():
|
||||||
if is_autosave:
|
if is_autosave:
|
||||||
@@ -2639,8 +2662,8 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool:
|
|||||||
_speech_pending_scene_save_path = path
|
_speech_pending_scene_save_path = path
|
||||||
_speech_pending_scene_save_as = false
|
_speech_pending_scene_save_as = false
|
||||||
_speech_scene_save_pending = true
|
_speech_scene_save_pending = true
|
||||||
if _speech_save_gate_dialog != null and not _speech_save_gate_dialog.visible:
|
if _speech_save_gate_dialog != null and not STAGE_DIALOG_STYLER.is_open(_speech_save_gate_dialog):
|
||||||
_speech_save_gate_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_speech_save_gate_dialog)
|
||||||
return false
|
return false
|
||||||
if _rule_edit_uncommitted:
|
if _rule_edit_uncommitted:
|
||||||
if not is_autosave:
|
if not is_autosave:
|
||||||
@@ -2699,7 +2722,7 @@ func _reset_scene_content() -> void:
|
|||||||
_scenery_erase_piece_dialog.hide()
|
_scenery_erase_piece_dialog.hide()
|
||||||
_clear_erase_scenery_targets()
|
_clear_erase_scenery_targets()
|
||||||
if _scenery_repair_confirm_dialog != null:
|
if _scenery_repair_confirm_dialog != null:
|
||||||
_scenery_repair_confirm_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_scenery_repair_confirm_dialog)
|
||||||
_cancel_scenery_repair_preview()
|
_cancel_scenery_repair_preview()
|
||||||
_scenery_repair_target = null
|
_scenery_repair_target = null
|
||||||
_sky_id = &"sky_neutral"
|
_sky_id = &"sky_neutral"
|
||||||
@@ -2806,36 +2829,42 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
|
|||||||
|
|
||||||
func _show_scene_load_warnings(warnings: Array[String]) -> void:
|
func _show_scene_load_warnings(warnings: Array[String]) -> void:
|
||||||
var dialog: AcceptDialog = AcceptDialog.new()
|
var dialog: AcceptDialog = AcceptDialog.new()
|
||||||
dialog.title = "Some saved actions need a look"
|
dialog.title = "⚠️ Some saved actions need a look"
|
||||||
|
dialog.ok_button_text = "Got it"
|
||||||
var lines: PackedStringArray = PackedStringArray()
|
var lines: PackedStringArray = PackedStringArray()
|
||||||
for index: int in mini(warnings.size(), 12):
|
for index: int in mini(warnings.size(), 5):
|
||||||
lines.append("• " + warnings[index])
|
lines.append("• " + warnings[index])
|
||||||
if warnings.size() > 12:
|
if warnings.size() > 5:
|
||||||
lines.append("• And %d more warning(s)." % (warnings.size() - 12))
|
lines.append("• And %d more warning(s)." % (warnings.size() - 5))
|
||||||
dialog.dialog_text = "These items could not be restored:\n\n%s" % "\n".join(lines)
|
dialog.dialog_text = "These items could not be restored:\n\n%s" % "\n".join(lines)
|
||||||
add_child(dialog)
|
_chrome.get_canvas_layer().add_child(dialog)
|
||||||
|
_apply_dialog_theme(dialog)
|
||||||
dialog.confirmed.connect(dialog.queue_free)
|
dialog.confirmed.connect(dialog.queue_free)
|
||||||
dialog.close_requested.connect(dialog.queue_free)
|
dialog.close_requested.connect(dialog.queue_free)
|
||||||
dialog.popup_centered(Vector2i(620, 420))
|
STAGE_DIALOG_STYLER.popup(dialog)
|
||||||
|
|
||||||
|
|
||||||
func _show_missing_assets_dialog(paths: Array[String]) -> void:
|
func _show_missing_assets_dialog(paths: Array[String]) -> void:
|
||||||
if _missing_assets_dialog == null:
|
if _missing_assets_dialog == null:
|
||||||
return
|
return
|
||||||
var asset_lines: Array[String] = []
|
var asset_lines: Array[String] = []
|
||||||
for path: String in paths:
|
for index: int in mini(paths.size(), 5):
|
||||||
var asset_name := path.get_file()
|
var asset_name: String = paths[index].get_file()
|
||||||
if asset_name.is_empty(): asset_name = "Unnamed .stk asset"
|
if asset_name.is_empty():
|
||||||
asset_lines.append("• %s\n %s" % [asset_name, path if not path.is_empty() else "(no asset path was saved)"])
|
asset_name = "Unnamed stickman"
|
||||||
_missing_assets_dialog.dialog_text = "%d stickman asset(s) could not be loaded:\n\n%s\n\nA red marker was placed where each missing asset belongs." % [paths.size(), "\n\n".join(asset_lines)]
|
asset_lines.append("• %s" % asset_name)
|
||||||
_missing_assets_dialog.popup_centered()
|
if paths.size() > 5:
|
||||||
|
asset_lines.append("• And %d more" % (paths.size() - 5))
|
||||||
|
_missing_assets_dialog.title = "⚠️ Some scene assets are missing"
|
||||||
|
_missing_assets_dialog.dialog_text = "Red markers show where the missing stickmen belong.\n\n%s" % "\n".join(asset_lines)
|
||||||
|
STAGE_DIALOG_STYLER.popup(_missing_assets_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _show_missing_scenery_dialog(count: int) -> void:
|
func _show_missing_scenery_dialog(count: int) -> void:
|
||||||
if _missing_scenery_dialog == null:
|
if _missing_scenery_dialog == null:
|
||||||
return
|
return
|
||||||
_missing_scenery_dialog.dialog_text = "%d scenery picture%s could not be found. The scene still has a marker in each spot, so nothing was lost." % [count, "" if count == 1 else "s"]
|
_missing_scenery_dialog.dialog_text = "%d scenery picture%s could not be found. The scene still has a marker in each spot, so nothing was lost." % [count, "" if count == 1 else "s"]
|
||||||
_missing_scenery_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_missing_scenery_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _add_recent_scene(path: String) -> void:
|
func _add_recent_scene(path: String) -> void:
|
||||||
@@ -2884,7 +2913,12 @@ func _check_autosave_recovery(show_starter_chooser_after_discard: bool = false)
|
|||||||
return
|
return
|
||||||
var autosave_data: Dictionary = inspection.get("data", {}) as Dictionary
|
var autosave_data: Dictionary = inspection.get("data", {}) as Dictionary
|
||||||
var dialog := ConfirmationDialog.new()
|
var dialog := ConfirmationDialog.new()
|
||||||
dialog.name = "AutosaveRecoveryDialog"; dialog.title = "Recover Sandbox Scene?"; dialog.dialog_text = "A previous Play session was not closed cleanly. Recover it?"
|
dialog.name = "AutosaveRecoveryDialog"
|
||||||
|
dialog.title = "🔄 Scene recovery found"
|
||||||
|
dialog.dialog_text = "A recovery copy of this scene is available. Would you like to restore it?\n\nℹ️ Restoring opens the recovered scene for review before you save it."
|
||||||
|
dialog.ok_button_text = "Restore scene"
|
||||||
|
dialog.cancel_button_text = "Discard recovery"
|
||||||
|
dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
dialog.confirmed.connect(func() -> void: _apply_scene_data(autosave_data, AUTOSAVE_PATH, true); dialog.queue_free())
|
dialog.confirmed.connect(func() -> void: _apply_scene_data(autosave_data, AUTOSAVE_PATH, true); dialog.queue_free())
|
||||||
dialog.canceled.connect(func() -> void:
|
dialog.canceled.connect(func() -> void:
|
||||||
_document.remove_autosave()
|
_document.remove_autosave()
|
||||||
@@ -2892,7 +2926,9 @@ func _check_autosave_recovery(show_starter_chooser_after_discard: bool = false)
|
|||||||
if show_starter_chooser_after_discard:
|
if show_starter_chooser_after_discard:
|
||||||
_queue_startup_starter_chooser()
|
_queue_startup_starter_chooser()
|
||||||
)
|
)
|
||||||
get_node(NodePath("UI")).add_child(dialog); _apply_dialog_theme(dialog); dialog.popup_centered()
|
get_node(NodePath("UI")).add_child(dialog)
|
||||||
|
_apply_dialog_theme(dialog, dialog.get_ok_button(), [dialog.get_cancel_button()])
|
||||||
|
STAGE_DIALOG_STYLER.popup(dialog)
|
||||||
|
|
||||||
|
|
||||||
func _queue_startup_starter_chooser() -> void:
|
func _queue_startup_starter_chooser() -> void:
|
||||||
@@ -2924,7 +2960,7 @@ func _startup_modal_is_visible() -> bool:
|
|||||||
if ui_layer != null:
|
if ui_layer != null:
|
||||||
for child: Node in ui_layer.get_children():
|
for child: Node in ui_layer.get_children():
|
||||||
var window := child as Window
|
var window := child as Window
|
||||||
if window != null and window.visible:
|
if (window != null and STAGE_DIALOG_STYLER.is_open(window)) or (child is Control and child.name == "StageDialogOverlay" and (child as Control).visible):
|
||||||
return true
|
return true
|
||||||
for dialog: Window in [
|
for dialog: Window in [
|
||||||
_cli_error_dialog,
|
_cli_error_dialog,
|
||||||
@@ -2940,7 +2976,7 @@ func _startup_modal_is_visible() -> bool:
|
|||||||
_group_preview_save_dialog,
|
_group_preview_save_dialog,
|
||||||
_confirm_dialog,
|
_confirm_dialog,
|
||||||
]:
|
]:
|
||||||
if dialog != null and is_instance_valid(dialog) and dialog.visible:
|
if dialog != null and is_instance_valid(dialog) and STAGE_DIALOG_STYLER.is_open(dialog):
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
@@ -2978,11 +3014,11 @@ func _load_cli_scene(path: String) -> Dictionary:
|
|||||||
return {"ok": true, "error": ""}
|
return {"ok": true, "error": ""}
|
||||||
|
|
||||||
|
|
||||||
func _show_cli_error_dialog(message: String) -> void:
|
func _show_cli_error_dialog(_message: String) -> void:
|
||||||
if _cli_error_dialog == null:
|
if _cli_error_dialog == null:
|
||||||
return
|
return
|
||||||
_cli_error_dialog.dialog_text = message
|
_cli_error_dialog.dialog_text = ""
|
||||||
_cli_error_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_cli_error_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _notification(what: int) -> void:
|
func _notification(what: int) -> void:
|
||||||
@@ -3085,9 +3121,9 @@ func _theme_dialog_size(key: String, fallback: Vector2i) -> Vector2i:
|
|||||||
|
|
||||||
## Dialogs are separate native/embedded windows, so they do not reliably
|
## Dialogs are separate native/embedded windows, so they do not reliably
|
||||||
## inherit the CanvasLayer's visual defaults. Apply only local overrides.
|
## inherit the CanvasLayer's visual defaults. Apply only local overrides.
|
||||||
func _apply_dialog_theme(dialog: Window) -> void:
|
func _apply_dialog_theme(dialog: Window, primary_button: Button = null, destructive_buttons: Array = []) -> void:
|
||||||
if _modal_views != null:
|
if _modal_views != null:
|
||||||
_modal_views.apply_dialog_theme(dialog)
|
_modal_views.apply_dialog_theme(dialog, primary_button, destructive_buttons)
|
||||||
|
|
||||||
|
|
||||||
func _apply_dialog_font_to_children(node: Node, size: int) -> void:
|
func _apply_dialog_font_to_children(node: Node, size: int) -> void:
|
||||||
@@ -3250,7 +3286,7 @@ func _build_ui() -> void:
|
|||||||
_asset_browser.surface_color_confirmed.connect(_record_recent_surface_color)
|
_asset_browser.surface_color_confirmed.connect(_record_recent_surface_color)
|
||||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||||
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
|
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
|
||||||
_build_ground_finish_popup(ui)
|
_build_ground_finish_chooser(ui)
|
||||||
|
|
||||||
_modal_views.build_file_dialogs(ui)
|
_modal_views.build_file_dialogs(ui)
|
||||||
_scene_open_dialog = _modal_views.scene_open_dialog
|
_scene_open_dialog = _modal_views.scene_open_dialog
|
||||||
@@ -3446,7 +3482,7 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
|
|||||||
_missing_scenery_dialog.dialog_text = "This scene has artwork that could not be found. You can still explore the scene and fix it later."
|
_missing_scenery_dialog.dialog_text = "This scene has artwork that could not be found. You can still explore the scene and fix it later."
|
||||||
_missing_scenery_dialog.ok_button_text = "Show Scenery"
|
_missing_scenery_dialog.ok_button_text = "Show Scenery"
|
||||||
_missing_scenery_dialog.cancel_button_text = "Later"
|
_missing_scenery_dialog.cancel_button_text = "Later"
|
||||||
_missing_scenery_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
_missing_scenery_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
_missing_scenery_dialog.confirmed.connect(_enter_scenery_edit_mode)
|
_missing_scenery_dialog.confirmed.connect(_enter_scenery_edit_mode)
|
||||||
ui.add_child(_missing_scenery_dialog)
|
ui.add_child(_missing_scenery_dialog)
|
||||||
_apply_dialog_theme(_missing_scenery_dialog)
|
_apply_dialog_theme(_missing_scenery_dialog)
|
||||||
@@ -3455,7 +3491,7 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
|
|||||||
_scenery_repair_confirm_dialog.title = "Use this artwork here?"
|
_scenery_repair_confirm_dialog.title = "Use this artwork here?"
|
||||||
_scenery_repair_confirm_dialog.ok_button_text = "Use artwork"
|
_scenery_repair_confirm_dialog.ok_button_text = "Use artwork"
|
||||||
_scenery_repair_confirm_dialog.cancel_button_text = "Keep for now"
|
_scenery_repair_confirm_dialog.cancel_button_text = "Keep for now"
|
||||||
_scenery_repair_confirm_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
_scenery_repair_confirm_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
_scenery_repair_confirm_dialog.confirmed.connect(_confirm_scenery_repair)
|
_scenery_repair_confirm_dialog.confirmed.connect(_confirm_scenery_repair)
|
||||||
_scenery_repair_confirm_dialog.canceled.connect(_cancel_scenery_repair_preview)
|
_scenery_repair_confirm_dialog.canceled.connect(_cancel_scenery_repair_preview)
|
||||||
ui.add_child(_scenery_repair_confirm_dialog)
|
ui.add_child(_scenery_repair_confirm_dialog)
|
||||||
@@ -3556,7 +3592,7 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
|
|||||||
var backdrop: ColorRect = ColorRect.new()
|
var backdrop: ColorRect = ColorRect.new()
|
||||||
backdrop.name = "DialogBackdrop"
|
backdrop.name = "DialogBackdrop"
|
||||||
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
backdrop.color = _confirmation_dialog_color("backdrop_color", Color("#10151D66"))
|
backdrop.color = _confirmation_dialog_color("backdrop_color", Color("#080D15B8"))
|
||||||
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
_scenery_erase_piece_dialog.add_child(backdrop)
|
_scenery_erase_piece_dialog.add_child(backdrop)
|
||||||
|
|
||||||
@@ -3572,7 +3608,7 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
|
|||||||
|
|
||||||
var margin: MarginContainer = MarginContainer.new()
|
var margin: MarginContainer = MarginContainer.new()
|
||||||
margin.name = "DialogMargin"
|
margin.name = "DialogMargin"
|
||||||
var padding: int = _confirmation_dialog_integer("padding", 18, 12, 32)
|
var padding: int = _confirmation_dialog_integer("padding", 22, 12, 36)
|
||||||
margin.add_theme_constant_override("margin_left", padding)
|
margin.add_theme_constant_override("margin_left", padding)
|
||||||
margin.add_theme_constant_override("margin_right", padding)
|
margin.add_theme_constant_override("margin_right", padding)
|
||||||
margin.add_theme_constant_override("margin_top", padding)
|
margin.add_theme_constant_override("margin_top", padding)
|
||||||
@@ -3583,14 +3619,14 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
|
|||||||
layout.name = "DialogLayout"
|
layout.name = "DialogLayout"
|
||||||
layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
layout.size_flags_vertical = 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))
|
layout.add_theme_constant_override("separation", _confirmation_dialog_integer("body_gap", 12, 4, 24))
|
||||||
margin.add_child(layout)
|
margin.add_child(layout)
|
||||||
|
|
||||||
_scenery_erase_piece_dialog_title = Label.new()
|
_scenery_erase_piece_dialog_title = Label.new()
|
||||||
_scenery_erase_piece_dialog_title.name = "DialogTitle"
|
_scenery_erase_piece_dialog_title.name = "DialogTitle"
|
||||||
_scenery_erase_piece_dialog_title.text = "Remove this scenery?"
|
_scenery_erase_piece_dialog_title.text = "🧱 Remove this scenery piece?"
|
||||||
_scenery_erase_piece_dialog_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
_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_font_size_override("font_size", _confirmation_dialog_integer("title_font_size", 28, 20, 40))
|
||||||
_scenery_erase_piece_dialog_title.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA")))
|
_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)
|
_apply_ui_font(_scenery_erase_piece_dialog_title)
|
||||||
var title_font: Font = _ui_font
|
var title_font: Font = _ui_font
|
||||||
@@ -3603,7 +3639,7 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
|
|||||||
|
|
||||||
_scenery_erase_piece_dialog_message = Label.new()
|
_scenery_erase_piece_dialog_message = Label.new()
|
||||||
_scenery_erase_piece_dialog_message.name = "DialogMessage"
|
_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.text = "This removes the selected scenery piece from the scene."
|
||||||
_scenery_erase_piece_dialog_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
_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.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_font_size_override("font_size", _confirmation_dialog_integer("message_font_size", 20, 16, 32))
|
||||||
@@ -3623,14 +3659,14 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
|
|||||||
|
|
||||||
_scenery_erase_piece_keep_button = Button.new()
|
_scenery_erase_piece_keep_button = Button.new()
|
||||||
_scenery_erase_piece_keep_button.name = "KeepSceneryButton"
|
_scenery_erase_piece_keep_button.name = "KeepSceneryButton"
|
||||||
_scenery_erase_piece_keep_button.text = "Keep it"
|
_scenery_erase_piece_keep_button.text = "Keep this piece"
|
||||||
_scenery_erase_piece_keep_button.focus_mode = Control.FOCUS_ALL
|
_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_keep_button.pressed.connect(_dismiss_scenery_erase_piece_dialog)
|
||||||
_scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_keep_button)
|
_scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_keep_button)
|
||||||
|
|
||||||
_scenery_erase_piece_remove_button = Button.new()
|
_scenery_erase_piece_remove_button = Button.new()
|
||||||
_scenery_erase_piece_remove_button.name = "RemoveSceneryButton"
|
_scenery_erase_piece_remove_button.name = "RemoveSceneryButton"
|
||||||
_scenery_erase_piece_remove_button.text = "Remove scenery"
|
_scenery_erase_piece_remove_button.text = "Remove piece"
|
||||||
_scenery_erase_piece_remove_button.focus_mode = Control.FOCUS_ALL
|
_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_remove_button.pressed.connect(_confirm_erase_scenery_from_dialog)
|
||||||
_scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_remove_button)
|
_scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_remove_button)
|
||||||
@@ -3642,66 +3678,18 @@ func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
|
|||||||
func _apply_scenery_erase_piece_dialog_style() -> void:
|
func _apply_scenery_erase_piece_dialog_style() -> void:
|
||||||
if _scenery_erase_piece_dialog == null:
|
if _scenery_erase_piece_dialog == null:
|
||||||
return
|
return
|
||||||
var shell: StyleBoxFlat = StyleBoxFlat.new()
|
var shell: StyleBoxFlat = STAGE_DIALOG_STYLER.panel_style(_sandbox_theme, false)
|
||||||
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)
|
_scenery_erase_piece_dialog_panel.add_theme_stylebox_override("panel", shell)
|
||||||
_apply_scenery_erase_piece_action_style(_scenery_erase_piece_keep_button, true)
|
STAGE_DIALOG_STYLER.style_button(_scenery_erase_piece_keep_button, &"primary", _sandbox_theme, _ui_font)
|
||||||
_apply_scenery_erase_piece_action_style(_scenery_erase_piece_remove_button, false)
|
STAGE_DIALOG_STYLER.style_button(_scenery_erase_piece_remove_button, &"danger", _sandbox_theme, _ui_font)
|
||||||
|
_scenery_erase_piece_keep_button.custom_minimum_size = Vector2(
|
||||||
|
float(_confirmation_dialog_integer("keep_button_width", 144, 112, 240)),
|
||||||
func _apply_scenery_erase_piece_action_style(button: Button, primary: bool) -> void:
|
float(_confirmation_dialog_integer("button_height", 52, 44, 80))
|
||||||
if button == null:
|
)
|
||||||
return
|
_scenery_erase_piece_remove_button.custom_minimum_size = Vector2(
|
||||||
var accent: Color = _confirmation_dialog_color("accent_color", Color("#22C6FF"))
|
float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240)),
|
||||||
var blue: Color = _confirmation_dialog_color("action_color", Color("#087DBA"))
|
float(_confirmation_dialog_integer("button_height", 52, 44, 80))
|
||||||
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:
|
func _confirmation_dialog_integer(token: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||||
@@ -3912,43 +3900,41 @@ func _update_scenery_depth_cursor_badge() -> void:
|
|||||||
_scenery_depth_cursor_badge.visible = true
|
_scenery_depth_cursor_badge.visible = true
|
||||||
|
|
||||||
|
|
||||||
func _build_ground_finish_popup(ui: CanvasLayer) -> void:
|
func _build_ground_finish_chooser(ui: CanvasLayer) -> void:
|
||||||
_ground_finish_popup = PopupMenu.new()
|
_ground_finish_chooser = STAGE_GROUND_FINISH_CHOOSER_SCRIPT.new() as StageGroundFinishChooser
|
||||||
_ground_finish_popup.name = "NewGroundLook"
|
_ground_finish_chooser.name = "NewGroundLook"
|
||||||
_ground_finish_popup.theme = _sandbox_theme.control_theme() if _sandbox_theme != null else null
|
_ground_finish_chooser.configure(_sandbox_theme as SandboxTheme, _ui_font, GROUND_FINISH_IDS, GROUND_FINISH_LABELS)
|
||||||
for index: int in GROUND_FINISH_LABELS.size():
|
_ground_finish_chooser.confirmed.connect(_on_ground_finish_draw_requested)
|
||||||
_ground_finish_popup.add_item(GROUND_FINISH_LABELS[index], index)
|
_ground_finish_chooser.canceled.connect(_on_ground_finish_chooser_closed)
|
||||||
_ground_finish_popup.set_item_as_checkable(index, true)
|
ui.add_child(_ground_finish_chooser)
|
||||||
_ground_finish_popup.id_pressed.connect(_on_ground_finish_selected)
|
|
||||||
ui.add_child(_ground_finish_popup)
|
|
||||||
|
|
||||||
|
|
||||||
func _open_ground_finish_popup() -> void:
|
func _open_ground_finish_chooser() -> void:
|
||||||
if _ground_finish_popup == null:
|
if _ground_finish_chooser == null:
|
||||||
return
|
return
|
||||||
for index: int in GROUND_FINISH_IDS.size():
|
_ground_finish_chooser.open_for_finish(_new_ground_finish)
|
||||||
_ground_finish_popup.set_item_checked(index, GROUND_FINISH_IDS[index] == _new_ground_finish)
|
|
||||||
var origin: Vector2 = get_viewport().get_mouse_position()
|
|
||||||
var ground_button: Button = _palette_buttons.get("ground") as Button
|
|
||||||
if ground_button != null:
|
|
||||||
var button_rect: Rect2 = ground_button.get_global_rect()
|
|
||||||
origin = Vector2(button_rect.position.x, button_rect.end.y)
|
|
||||||
var viewport_size: Vector2 = get_viewport_rect().size
|
|
||||||
origin.x = clampf(origin.x, 8.0, maxf(8.0, viewport_size.x - 288.0))
|
|
||||||
origin.y = clampf(origin.y, 8.0, maxf(8.0, viewport_size.y - 320.0))
|
|
||||||
_ground_finish_popup.popup(Rect2i(Vector2i(origin), Vector2i(280, 312)))
|
|
||||||
|
|
||||||
|
|
||||||
func _on_ground_finish_selected(index: int) -> void:
|
func _on_ground_finish_draw_requested() -> void:
|
||||||
if index < 0 or index >= GROUND_FINISH_IDS.size():
|
if _ground_finish_chooser == null:
|
||||||
return
|
return
|
||||||
_new_ground_finish = GROUND_FINISH_IDS[index]
|
var finish_id: StringName = _ground_finish_chooser.selected_finish()
|
||||||
|
var index: int = GROUND_FINISH_IDS.find(finish_id)
|
||||||
|
if index < 0 or index >= GROUND_FINISH_LABELS.size():
|
||||||
|
return
|
||||||
|
_new_ground_finish = finish_id
|
||||||
if _terrain_painter != null:
|
if _terrain_painter != null:
|
||||||
_terrain_painter.set_ground_finish(_new_ground_finish)
|
_terrain_painter.set_ground_finish(_new_ground_finish)
|
||||||
set_placement_mode("ground")
|
set_placement_mode("ground")
|
||||||
_show_toast("New Ground look: %s" % GROUND_FINISH_LABELS[index])
|
_show_toast("New Ground look: %s" % GROUND_FINISH_LABELS[index])
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ground_finish_chooser_closed() -> void:
|
||||||
|
for placement_id: String in _palette_buttons:
|
||||||
|
var palette_button: Button = _palette_buttons[placement_id] as Button
|
||||||
|
palette_button.set_pressed_no_signal(placement_id == _placement_id)
|
||||||
|
|
||||||
|
|
||||||
func _build_use_spot_overlay(ui: CanvasLayer) -> void:
|
func _build_use_spot_overlay(ui: CanvasLayer) -> void:
|
||||||
_use_spot_overlay = STAGE_USE_SPOT_OVERLAY_SCRIPT.new() as StageUseSpotOverlay
|
_use_spot_overlay = STAGE_USE_SPOT_OVERLAY_SCRIPT.new() as StageUseSpotOverlay
|
||||||
_use_spot_overlay.name = "UseSpotOverlay"
|
_use_spot_overlay.name = "UseSpotOverlay"
|
||||||
@@ -3961,53 +3947,14 @@ func _build_use_spot_overlay(ui: CanvasLayer) -> void:
|
|||||||
ui.add_child(_use_choice_popup)
|
ui.add_child(_use_choice_popup)
|
||||||
_spot_erase_dialog = ConfirmationDialog.new()
|
_spot_erase_dialog = ConfirmationDialog.new()
|
||||||
_spot_erase_dialog.name = "EraseUseSpotDialog"
|
_spot_erase_dialog.name = "EraseUseSpotDialog"
|
||||||
_spot_erase_dialog.title = "⚠️ Erase Use spot?"
|
_spot_erase_dialog.title = "📍 Erase this Use spot?"
|
||||||
_spot_erase_dialog.ok_button_text = "Erase spot"
|
_spot_erase_dialog.ok_button_text = "Erase spot"
|
||||||
_spot_erase_dialog.cancel_button_text = "Keep spot"
|
_spot_erase_dialog.cancel_button_text = "Keep spot"
|
||||||
_spot_erase_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(620, 300))
|
_spot_erase_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
_spot_erase_dialog.theme = _sandbox_theme.control_theme() if _sandbox_theme != null else null
|
_spot_erase_dialog.theme = _sandbox_theme.control_theme() if _sandbox_theme != null else null
|
||||||
_spot_erase_dialog.confirmed.connect(_on_spot_erase_confirmed)
|
_spot_erase_dialog.confirmed.connect(_on_spot_erase_confirmed)
|
||||||
ui.add_child(_spot_erase_dialog)
|
ui.add_child(_spot_erase_dialog)
|
||||||
_apply_dialog_theme(_spot_erase_dialog)
|
_apply_dialog_theme(_spot_erase_dialog, _spot_erase_dialog.get_cancel_button(), [_spot_erase_dialog.get_ok_button()])
|
||||||
_style_spot_erase_dialog_buttons()
|
|
||||||
|
|
||||||
|
|
||||||
func _style_spot_erase_dialog_buttons() -> void:
|
|
||||||
if _spot_erase_dialog == null:
|
|
||||||
return
|
|
||||||
var danger: Color = _sandbox_theme.color("overlays.use_preview.invalid_color", Color("#F06464")) if _sandbox_theme != null else Color("#F06464")
|
|
||||||
var safe: Color = _sandbox_theme.color("mode_colors.edit", Color("#22C6FF")) if _sandbox_theme != null else Color("#22C6FF")
|
|
||||||
_style_spot_erase_titlebar(danger.darkened(0.58))
|
|
||||||
_style_spot_erase_action_button(_spot_erase_dialog.get_ok_button(), danger.darkened(0.55), danger)
|
|
||||||
_style_spot_erase_action_button(_spot_erase_dialog.get_cancel_button(), safe.darkened(0.72), safe)
|
|
||||||
|
|
||||||
|
|
||||||
func _style_spot_erase_titlebar(color: Color) -> void:
|
|
||||||
for style_name: String in ["embedded_border", "embedded_unfocused_border"]:
|
|
||||||
var original: StyleBoxFlat = _spot_erase_dialog.get_theme_stylebox(style_name) as StyleBoxFlat
|
|
||||||
if original == null:
|
|
||||||
continue
|
|
||||||
var tinted: StyleBoxFlat = original.duplicate() as StyleBoxFlat
|
|
||||||
tinted.bg_color = color
|
|
||||||
_spot_erase_dialog.add_theme_stylebox_override(style_name, tinted)
|
|
||||||
_spot_erase_dialog.add_theme_color_override("title_color", Color("#FFFFFF"))
|
|
||||||
|
|
||||||
|
|
||||||
func _style_spot_erase_action_button(button: Button, surface: Color, accent: Color) -> void:
|
|
||||||
if button == null:
|
|
||||||
return
|
|
||||||
button.custom_minimum_size = Vector2(156.0, 56.0)
|
|
||||||
button.add_theme_color_override("font_color", Color("#FFFFFF"))
|
|
||||||
button.add_theme_color_override("font_hover_color", Color("#FFFFFF"))
|
|
||||||
button.add_theme_color_override("font_pressed_color", Color("#FFFFFF"))
|
|
||||||
for state: String in ["normal", "hover", "pressed", "hover_pressed"]:
|
|
||||||
var style := StyleBoxFlat.new()
|
|
||||||
style.bg_color = surface.lightened(0.10) if state == "hover" or state == "hover_pressed" else surface
|
|
||||||
style.border_color = accent.lightened(0.18) if state == "hover" or state == "pressed" or state == "hover_pressed" else accent
|
|
||||||
style.set_border_width_all(2 if state == "hover" or state == "pressed" or state == "hover_pressed" else 1)
|
|
||||||
style.set_corner_radius_all(10)
|
|
||||||
style.set_content_margin_all(12.0)
|
|
||||||
button.add_theme_stylebox_override(state, style)
|
|
||||||
|
|
||||||
|
|
||||||
func _stage_panel_integer(key: String, fallback: int, minimum: int, maximum: int) -> int:
|
func _stage_panel_integer(key: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||||
@@ -4153,8 +4100,9 @@ func _on_spot_erase_requested(spot_id: String) -> void:
|
|||||||
_spot_erase_prop_id = prop.stage_object_id
|
_spot_erase_prop_id = prop.stage_object_id
|
||||||
_spot_erase_id = spot_id
|
_spot_erase_id = spot_id
|
||||||
var warning: String = "\nThis spot is used in your story. Those steps will need a new spot." if _use_spot_is_referenced(prop.stage_object_id, spot_id) else ""
|
var warning: String = "\nThis spot is used in your story. Those steps will need a new spot." if _use_spot_is_referenced(prop.stage_object_id, spot_id) else ""
|
||||||
_spot_erase_dialog.dialog_text = "Erase %s (spot %d)?%s" % [String(spot.get("name", "Spot")), int(spot.get("number", 0)), warning]
|
_spot_erase_dialog.dialog_text = "The character will no longer be able to use this spot.%s" % warning
|
||||||
_spot_erase_dialog.popup_centered(_spot_erase_dialog.min_size)
|
STAGE_DIALOG_STYLER.popup(_spot_erase_dialog)
|
||||||
|
STAGE_DIALOG_STYLER.visual_button(_spot_erase_dialog.get_cancel_button()).grab_focus()
|
||||||
|
|
||||||
|
|
||||||
func _on_spot_erase_confirmed() -> void:
|
func _on_spot_erase_confirmed() -> void:
|
||||||
@@ -4993,7 +4941,7 @@ func _on_palette_toggled(pressed: bool, id: String) -> void:
|
|||||||
_open_selector(id)
|
_open_selector(id)
|
||||||
return
|
return
|
||||||
if id == "ground":
|
if id == "ground":
|
||||||
_open_ground_finish_popup()
|
_open_ground_finish_chooser()
|
||||||
return
|
return
|
||||||
set_placement_mode(id)
|
set_placement_mode(id)
|
||||||
else:
|
else:
|
||||||
@@ -5009,7 +4957,7 @@ func _on_palette_card_activated(payload: Dictionary) -> void:
|
|||||||
if bool(payload.get("opens_selector", false)):
|
if bool(payload.get("opens_selector", false)):
|
||||||
_open_selector(id)
|
_open_selector(id)
|
||||||
elif id == "ground":
|
elif id == "ground":
|
||||||
_open_ground_finish_popup()
|
_open_ground_finish_chooser()
|
||||||
else:
|
else:
|
||||||
set_placement_mode(id)
|
set_placement_mode(id)
|
||||||
|
|
||||||
@@ -5056,12 +5004,13 @@ func _is_stage_drop_position(viewport_pos: Vector2) -> bool:
|
|||||||
return hovered == null or hovered.mouse_filter == Control.MOUSE_FILTER_IGNORE
|
return hovered == null or hovered.mouse_filter == Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
|
|
||||||
func _open_selector(kind: String) -> void:
|
func _open_selector(kind: String, preserve_scenery_browse_state: bool = false) -> void:
|
||||||
if kind == "scenery":
|
if kind == "scenery":
|
||||||
if _scenery_edit_mode:
|
if _scenery_edit_mode:
|
||||||
_exit_scenery_edit_mode()
|
_exit_scenery_edit_mode()
|
||||||
if _scenery_controller != null and _scenery_controller.is_active():
|
if _scenery_controller != null and _scenery_controller.is_active():
|
||||||
_scenery_controller.exit_tool()
|
_scenery_controller.exit_tool()
|
||||||
|
_hide_scenery_capture_bar()
|
||||||
_scenery_placed_count = 0
|
_scenery_placed_count = 0
|
||||||
elif _scenery_edit_mode:
|
elif _scenery_edit_mode:
|
||||||
_exit_scenery_edit_mode()
|
_exit_scenery_edit_mode()
|
||||||
@@ -5072,12 +5021,14 @@ func _open_selector(kind: String) -> void:
|
|||||||
(_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind)
|
(_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind)
|
||||||
if kind == "scenery":
|
if kind == "scenery":
|
||||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||||
|
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
|
||||||
var selector_state: Dictionary = _asset_browser.open_selector(
|
var selector_state: Dictionary = _asset_browser.open_selector(
|
||||||
kind,
|
kind,
|
||||||
_spawner.get_selected_stickman_path(),
|
_spawner.get_selected_stickman_path(),
|
||||||
_sky_id,
|
_sky_id,
|
||||||
_background_kind,
|
_background_kind,
|
||||||
_plain_color
|
_plain_color,
|
||||||
|
preserve_scenery_browse_state
|
||||||
)
|
)
|
||||||
if kind == "stickman":
|
if kind == "stickman":
|
||||||
var selected_stickman_path: String = String(selector_state.get("selected_stickman_path", _spawner.get_selected_stickman_path()))
|
var selected_stickman_path: String = String(selector_state.get("selected_stickman_path", _spawner.get_selected_stickman_path()))
|
||||||
@@ -5154,6 +5105,11 @@ func _on_asset_selected(entry: Dictionary) -> void:
|
|||||||
set_placement_mode(kind)
|
set_placement_mode(kind)
|
||||||
|
|
||||||
|
|
||||||
|
func _return_to_scenery_selector() -> void:
|
||||||
|
_hide_scenery_capture_bar()
|
||||||
|
_open_selector("scenery", true)
|
||||||
|
|
||||||
|
|
||||||
func _load_starter_scene(entry: Dictionary) -> void:
|
func _load_starter_scene(entry: Dictionary) -> void:
|
||||||
var path: String = CONTENT_PATHS.canonicalize(String(entry.get("path", "")))
|
var path: String = CONTENT_PATHS.canonicalize(String(entry.get("path", "")))
|
||||||
var read_result: Dictionary = _document.read(path) if _document != null else {}
|
var read_result: Dictionary = _document.read(path) if _document != null else {}
|
||||||
@@ -5600,14 +5556,14 @@ func _start_scenery_repair_preview(entry: Dictionary) -> void:
|
|||||||
_scenery_repair_candidate = candidate
|
_scenery_repair_candidate = candidate
|
||||||
_scenery_repair_path = path
|
_scenery_repair_path = path
|
||||||
if _scenery_repair_confirm_dialog != null:
|
if _scenery_repair_confirm_dialog != null:
|
||||||
_scenery_repair_confirm_dialog.dialog_text = "Use %s in this spot? Only this scenery piece will change." % candidate.display_name
|
_scenery_repair_confirm_dialog.dialog_text = "Use %s in the selected spot?\n\nℹ️ Only this scenery piece will change." % candidate.display_name
|
||||||
call_deferred("_popup_scenery_repair_confirm_after_frame")
|
call_deferred("_popup_scenery_repair_confirm_after_frame")
|
||||||
|
|
||||||
|
|
||||||
func _popup_scenery_repair_confirm_after_frame() -> void:
|
func _popup_scenery_repair_confirm_after_frame() -> void:
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
if _scenery_repair_confirm_dialog != null and _scenery_repair_candidate != null and is_instance_valid(_scenery_repair_candidate):
|
if _scenery_repair_confirm_dialog != null and _scenery_repair_candidate != null and is_instance_valid(_scenery_repair_candidate):
|
||||||
_scenery_repair_confirm_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_scenery_repair_confirm_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _confirm_scenery_repair() -> void:
|
func _confirm_scenery_repair() -> void:
|
||||||
@@ -5667,7 +5623,7 @@ func _exit_scenery_edit_mode() -> void:
|
|||||||
_scenery_erase_piece_dialog.hide()
|
_scenery_erase_piece_dialog.hide()
|
||||||
_clear_erase_scenery_targets()
|
_clear_erase_scenery_targets()
|
||||||
if _scenery_repair_confirm_dialog != null:
|
if _scenery_repair_confirm_dialog != null:
|
||||||
_scenery_repair_confirm_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_scenery_repair_confirm_dialog)
|
||||||
_cancel_scenery_repair_preview()
|
_cancel_scenery_repair_preview()
|
||||||
_scenery_repair_target = null
|
_scenery_repair_target = null
|
||||||
if _scenery_inspector != null:
|
if _scenery_inspector != null:
|
||||||
@@ -5713,7 +5669,7 @@ func _refresh_scenery_capture_bar() -> void:
|
|||||||
actions.append(_make_scenery_depth_capture_action(&"depth_floor", &"floor", "Front", 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 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 · Cancel undoes"
|
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": "done_tool", "label": "Back", "primary": true})
|
||||||
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
||||||
_use_target_capture_bar.set_scenery_capture_actions(prompt, actions)
|
_use_target_capture_bar.set_scenery_capture_actions(prompt, actions)
|
||||||
elif _scenery_controller.get_active_tool() in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"]:
|
elif _scenery_controller.get_active_tool() in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"]:
|
||||||
@@ -5727,8 +5683,7 @@ func _refresh_scenery_capture_bar() -> void:
|
|||||||
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
||||||
_use_target_capture_bar.set_scenery_capture_actions(erase_prompt, actions)
|
_use_target_capture_bar.set_scenery_capture_actions(erase_prompt, actions)
|
||||||
else:
|
else:
|
||||||
actions.append({"id": "choose_scenery", "label": "Choose scenery"})
|
actions.append({"id": "done_tool", "label": "Back", "primary": true})
|
||||||
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":
|
if _scenery_controller.get_active_tool() == &"background_ground":
|
||||||
var ground_prompt: String = "Draw background ground · No walking"
|
var ground_prompt: String = "Draw background ground · No walking"
|
||||||
@@ -5792,8 +5747,6 @@ func _hide_scenery_capture_bar() -> void:
|
|||||||
|
|
||||||
func _on_capture_action_requested(action_id: StringName) -> void:
|
func _on_capture_action_requested(action_id: StringName) -> void:
|
||||||
match action_id:
|
match action_id:
|
||||||
&"choose_scenery":
|
|
||||||
_open_selector("scenery")
|
|
||||||
&"depth_far":
|
&"depth_far":
|
||||||
_scenery_controller.set_depth(&"far")
|
_scenery_controller.set_depth(&"far")
|
||||||
_refresh_scenery_capture_bar()
|
_refresh_scenery_capture_bar()
|
||||||
@@ -5804,8 +5757,12 @@ func _on_capture_action_requested(action_id: StringName) -> void:
|
|||||||
_scenery_controller.set_depth(&"floor")
|
_scenery_controller.set_depth(&"floor")
|
||||||
_refresh_scenery_capture_bar()
|
_refresh_scenery_capture_bar()
|
||||||
&"done_tool":
|
&"done_tool":
|
||||||
_scenery_controller.exit_tool()
|
if _scenery_controller != null and _scenery_controller.is_placement_tool():
|
||||||
_hide_scenery_capture_bar()
|
_return_to_scenery_selector()
|
||||||
|
else:
|
||||||
|
if _scenery_controller != null:
|
||||||
|
_scenery_controller.exit_tool()
|
||||||
|
_hide_scenery_capture_bar()
|
||||||
&"cancel_tool":
|
&"cancel_tool":
|
||||||
_scenery_controller.cancel_active_tool()
|
_scenery_controller.cancel_active_tool()
|
||||||
_hide_scenery_capture_bar()
|
_hide_scenery_capture_bar()
|
||||||
@@ -6147,8 +6104,8 @@ func _request_scene_save(path: String = "", force_save_as: bool = false) -> void
|
|||||||
_speech_pending_scene_save_path = path
|
_speech_pending_scene_save_path = path
|
||||||
_speech_pending_scene_save_as = force_save_as
|
_speech_pending_scene_save_as = force_save_as
|
||||||
_speech_scene_save_pending = true
|
_speech_scene_save_pending = true
|
||||||
if _speech_save_gate_dialog != null and not _speech_save_gate_dialog.visible:
|
if _speech_save_gate_dialog != null and not STAGE_DIALOG_STYLER.is_open(_speech_save_gate_dialog):
|
||||||
_speech_save_gate_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_speech_save_gate_dialog)
|
||||||
return
|
return
|
||||||
if _rule_edit_uncommitted:
|
if _rule_edit_uncommitted:
|
||||||
_show_toast("Finish the rule before saving the scene.")
|
_show_toast("Finish the rule before saving the scene.")
|
||||||
@@ -6207,12 +6164,12 @@ func _cancel_deferred_speech_scene_save(reason: String) -> void:
|
|||||||
|
|
||||||
func _rename_scene_prompt() -> void:
|
func _rename_scene_prompt() -> void:
|
||||||
_scene_name_edit.text = _scene_name
|
_scene_name_edit.text = _scene_name
|
||||||
_rename_scene_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_rename_scene_dialog)
|
||||||
call_deferred("_focus_scene_name_edit")
|
call_deferred("_focus_scene_name_edit")
|
||||||
|
|
||||||
|
|
||||||
func _focus_scene_name_edit() -> void:
|
func _focus_scene_name_edit() -> void:
|
||||||
if _scene_name_edit == null or not _scene_name_edit.visible:
|
if _scene_name_edit == null or not _scene_name_edit.visible or not _scene_name_edit.is_inside_tree():
|
||||||
return
|
return
|
||||||
_scene_name_edit.grab_focus()
|
_scene_name_edit.grab_focus()
|
||||||
_scene_name_edit.select_all()
|
_scene_name_edit.select_all()
|
||||||
@@ -6227,14 +6184,14 @@ func _request_pending_file_action(action: PendingFileAction, path: String = "")
|
|||||||
return
|
return
|
||||||
_pending_file_action = action; _pending_open_path = path
|
_pending_file_action = action; _pending_open_path = path
|
||||||
if _dirty:
|
if _dirty:
|
||||||
_unsaved_changes_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_unsaved_changes_dialog)
|
||||||
else:
|
else:
|
||||||
_continue_pending_file_action()
|
_continue_pending_file_action()
|
||||||
|
|
||||||
|
|
||||||
func _continue_pending_file_action() -> void:
|
func _continue_pending_file_action() -> void:
|
||||||
if _unsaved_changes_dialog != null:
|
if _unsaved_changes_dialog != null:
|
||||||
_unsaved_changes_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_unsaved_changes_dialog)
|
||||||
var action := _pending_file_action; var path := _pending_open_path
|
var action := _pending_file_action; var path := _pending_open_path
|
||||||
_pending_file_action = PendingFileAction.NONE; _pending_open_path = ""
|
_pending_file_action = PendingFileAction.NONE; _pending_open_path = ""
|
||||||
match action:
|
match action:
|
||||||
@@ -6248,12 +6205,12 @@ func _continue_pending_file_action() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_unsaved_save() -> void:
|
func _on_unsaved_save() -> void:
|
||||||
_unsaved_changes_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_unsaved_changes_dialog)
|
||||||
_request_scene_save()
|
_request_scene_save()
|
||||||
|
|
||||||
|
|
||||||
func _on_unsaved_discard() -> void:
|
func _on_unsaved_discard() -> void:
|
||||||
_unsaved_changes_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_unsaved_changes_dialog)
|
||||||
_continue_pending_file_action()
|
_continue_pending_file_action()
|
||||||
|
|
||||||
|
|
||||||
@@ -6361,7 +6318,7 @@ func _on_action_popup_id_pressed(id: int) -> void:
|
|||||||
_open_speech_composer(_context_rig, {"type": "speak", "text": "", "duration": 2.0}, false, "direct")
|
_open_speech_composer(_context_rig, {"type": "speak", "text": "", "duration": 2.0}, false, "direct")
|
||||||
ACT_WAIT:
|
ACT_WAIT:
|
||||||
_wait_spin.value = 1.0
|
_wait_spin.value = 1.0
|
||||||
_wait_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_wait_dialog)
|
||||||
ACT_RAGDOLL:
|
ACT_RAGDOLL:
|
||||||
_context_rig.queue_action({ "type": "ragdoll" })
|
_context_rig.queue_action({ "type": "ragdoll" })
|
||||||
ACT_RECOVER:
|
ACT_RECOVER:
|
||||||
@@ -6657,7 +6614,7 @@ func _handle_action_target_click(world_pos: Vector2) -> void:
|
|||||||
"wait":
|
"wait":
|
||||||
_rule_step = RuleStep.PARAMS
|
_rule_step = RuleStep.PARAMS
|
||||||
_wait_spin.value = 1.0
|
_wait_spin.value = 1.0
|
||||||
_wait_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_wait_dialog)
|
||||||
"pick_up_prop":
|
"pick_up_prop":
|
||||||
_begin_capture(CaptureKind.PROP, "🫴 Click the prop to pick up (Esc or RMB to cancel)", _cb_rule_action_prop_target, _cb_rule_action_capture_cancel)
|
_begin_capture(CaptureKind.PROP, "🫴 Click the prop to pick up (Esc or RMB to cancel)", _cb_rule_action_prop_target, _cb_rule_action_capture_cancel)
|
||||||
"throw_prop":
|
"throw_prop":
|
||||||
@@ -7521,9 +7478,9 @@ func _open_speech_composer(actor: StickmanRig, initial: Dictionary, reactive_opt
|
|||||||
var queue: Array[Dictionary] = actor.get_queue()
|
var queue: Array[Dictionary] = actor.get_queue()
|
||||||
if queue_index >= 0 and queue_index < queue.size() and String(queue[queue_index].get("type", "")) == "speak":
|
if queue_index >= 0 and queue_index < queue.size() and String(queue[queue_index].get("type", "")) == "speak":
|
||||||
speech_initial = queue[queue_index].duplicate(true)
|
speech_initial = queue[queue_index].duplicate(true)
|
||||||
if _speech_recovery_matches_loaded_scene() and not _speech_recovery_dialog.visible and not _speech_recovery_in_progress:
|
if _speech_recovery_matches_loaded_scene() and not STAGE_DIALOG_STYLER.is_open(_speech_recovery_dialog) and not _speech_recovery_in_progress:
|
||||||
_speech_pending_open = {"actor_id": actor.get_instance_id(), "initial": speech_initial, "reactive": reactive_options, "context": context}
|
_speech_pending_open = {"actor_id": actor.get_instance_id(), "initial": speech_initial, "reactive": reactive_options, "context": context}
|
||||||
_speech_recovery_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_speech_recovery_dialog)
|
||||||
return
|
return
|
||||||
_speech_actor = actor
|
_speech_actor = actor
|
||||||
_speech_session_kind = context
|
_speech_session_kind = context
|
||||||
@@ -7547,7 +7504,7 @@ func _update_speech_composition() -> void:
|
|||||||
func _on_speech_committed(action: Dictionary) -> void:
|
func _on_speech_committed(action: Dictionary) -> void:
|
||||||
if _speech_session_kind == "recovered_unbound":
|
if _speech_session_kind == "recovered_unbound":
|
||||||
_speech_pending_commit_action = action.duplicate(true)
|
_speech_pending_commit_action = action.duplicate(true)
|
||||||
_speech_destination_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_speech_destination_dialog)
|
||||||
return
|
return
|
||||||
var speech_updates_uncommitted_rule: bool = _speech_session_kind == "rule_builder" or (
|
var speech_updates_uncommitted_rule: bool = _speech_session_kind == "rule_builder" or (
|
||||||
_speech_session_kind == "action_editor" and _action_editor_kind in ["rule_add", "rule_edit"]
|
_speech_session_kind == "action_editor" and _action_editor_kind in ["rule_add", "rule_edit"]
|
||||||
@@ -7595,9 +7552,9 @@ func _finish_speech_composition(clear_recovery: bool = false) -> void:
|
|||||||
if _speech_session != null:
|
if _speech_session != null:
|
||||||
_speech_session.close(clear_recovery, preserve_draft)
|
_speech_session.close(clear_recovery, preserve_draft)
|
||||||
if _speech_destination_dialog != null:
|
if _speech_destination_dialog != null:
|
||||||
_speech_destination_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_speech_destination_dialog)
|
||||||
if _speech_save_gate_dialog != null:
|
if _speech_save_gate_dialog != null:
|
||||||
_speech_save_gate_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_speech_save_gate_dialog)
|
||||||
_speech_restored_unbound = false
|
_speech_restored_unbound = false
|
||||||
|
|
||||||
|
|
||||||
@@ -7654,7 +7611,7 @@ func _canonicalize_speech_origin(origin: Dictionary) -> Dictionary:
|
|||||||
|
|
||||||
func _check_speech_recovery() -> void:
|
func _check_speech_recovery() -> void:
|
||||||
if _speech_recovery_dialog != null and _speech_recovery_matches_loaded_scene():
|
if _speech_recovery_dialog != null and _speech_recovery_matches_loaded_scene():
|
||||||
_speech_recovery_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_speech_recovery_dialog)
|
||||||
|
|
||||||
|
|
||||||
func _speech_recovery_matches_loaded_scene() -> bool:
|
func _speech_recovery_matches_loaded_scene() -> bool:
|
||||||
@@ -7983,9 +7940,21 @@ func _restore_queue_panel() -> void:
|
|||||||
|
|
||||||
func _ask_confirm(title: String, message: String, callback: Callable) -> void:
|
func _ask_confirm(title: String, message: String, callback: Callable) -> void:
|
||||||
_confirm_callback = callback
|
_confirm_callback = callback
|
||||||
_confirm_dialog.title = title
|
var normalized_title: String = title.to_lower()
|
||||||
|
var destructive: bool = normalized_title.begins_with("delete") or normalized_title.begins_with("clear") or normalized_title.begins_with("remove")
|
||||||
|
_confirm_dialog.title = ("🗑️ " if destructive else "❔ ") + title
|
||||||
_confirm_dialog.dialog_text = message
|
_confirm_dialog.dialog_text = message
|
||||||
_confirm_dialog.popup_centered()
|
if destructive:
|
||||||
|
_confirm_dialog.ok_button_text = "Delete" if normalized_title.begins_with("delete") else ("Remove" if normalized_title.begins_with("remove") else "Clear")
|
||||||
|
_confirm_dialog.cancel_button_text = "Keep"
|
||||||
|
_apply_dialog_theme(_confirm_dialog, _confirm_dialog.get_cancel_button(), [_confirm_dialog.get_ok_button()])
|
||||||
|
else:
|
||||||
|
_confirm_dialog.ok_button_text = "Continue"
|
||||||
|
_confirm_dialog.cancel_button_text = "Cancel"
|
||||||
|
_apply_dialog_theme(_confirm_dialog)
|
||||||
|
STAGE_DIALOG_STYLER.popup(_confirm_dialog)
|
||||||
|
if destructive:
|
||||||
|
STAGE_DIALOG_STYLER.visual_button(_confirm_dialog.get_cancel_button()).grab_focus()
|
||||||
|
|
||||||
|
|
||||||
func _on_confirm_confirmed() -> void:
|
func _on_confirm_confirmed() -> void:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ extends Control
|
|||||||
|
|
||||||
const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd")
|
const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd")
|
||||||
const SANDBOX_THEME := preload("res://scripts/stage/sandbox_theme.gd")
|
const SANDBOX_THEME := preload("res://scripts/stage/sandbox_theme.gd")
|
||||||
|
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
const SPEECH_BUBBLE := preload("res://scripts/speech/stickman_speech_bubble.gd")
|
const SPEECH_BUBBLE := preload("res://scripts/speech/stickman_speech_bubble.gd")
|
||||||
const FIVE_BUBBLE_HINT: String = "Long story? One at a time can be easier to follow."
|
const FIVE_BUBBLE_HINT: String = "Long story? One at a time can be easier to follow."
|
||||||
const MAX_BUBBLE_HINT: String = "You can add up to eight bubbles."
|
const MAX_BUBBLE_HINT: String = "You can add up to eight bubbles."
|
||||||
@@ -510,7 +511,7 @@ func preview_elapsed() -> float:
|
|||||||
|
|
||||||
|
|
||||||
func contains_viewport_point(point: Vector2) -> bool:
|
func contains_viewport_point(point: Vector2) -> bool:
|
||||||
if _cancel_confirm_dialog != null and _cancel_confirm_dialog.visible:
|
if _cancel_confirm_dialog != null and STAGE_DIALOG_STYLER.is_open(_cancel_confirm_dialog):
|
||||||
return true
|
return true
|
||||||
if is_message_dialog_open():
|
if is_message_dialog_open():
|
||||||
return true
|
return true
|
||||||
@@ -608,7 +609,7 @@ func close_without_commit() -> void:
|
|||||||
if _message_dialog != null:
|
if _message_dialog != null:
|
||||||
_message_dialog.hide()
|
_message_dialog.hide()
|
||||||
if _cancel_confirm_dialog != null:
|
if _cancel_confirm_dialog != null:
|
||||||
_cancel_confirm_dialog.hide()
|
STAGE_DIALOG_STYLER.hide(_cancel_confirm_dialog)
|
||||||
visible = false
|
visible = false
|
||||||
_replay_running = false
|
_replay_running = false
|
||||||
_actor = null
|
_actor = null
|
||||||
@@ -623,7 +624,8 @@ func set_cancel_requires_confirmation(required: bool) -> void:
|
|||||||
func request_cancel_speech() -> void:
|
func request_cancel_speech() -> void:
|
||||||
var changed: bool = _cancel_requires_confirmation or get_draft() != _initial_action
|
var changed: bool = _cancel_requires_confirmation or get_draft() != _initial_action
|
||||||
if changed and _cancel_confirm_dialog != null:
|
if changed and _cancel_confirm_dialog != null:
|
||||||
_cancel_confirm_dialog.popup_centered()
|
STAGE_DIALOG_STYLER.popup(_cancel_confirm_dialog)
|
||||||
|
STAGE_DIALOG_STYLER.visual_button(_cancel_confirm_dialog.get_cancel_button()).grab_focus()
|
||||||
return
|
return
|
||||||
cancelled.emit()
|
cancelled.emit()
|
||||||
|
|
||||||
@@ -710,13 +712,21 @@ func _build_ui() -> void:
|
|||||||
_active_editor.text_changed.connect(_on_active_text_changed)
|
_active_editor.text_changed.connect(_on_active_text_changed)
|
||||||
_active_editor.caret_changed.connect(_remember_active_editor_state)
|
_active_editor.caret_changed.connect(_remember_active_editor_state)
|
||||||
_cancel_confirm_dialog = ConfirmationDialog.new()
|
_cancel_confirm_dialog = ConfirmationDialog.new()
|
||||||
_cancel_confirm_dialog.title = "Discard speech changes?"
|
_cancel_confirm_dialog.title = "🗣️ Discard speech changes?"
|
||||||
_cancel_confirm_dialog.dialog_text = "Your speech edits will be lost. Do you want to discard them?"
|
_cancel_confirm_dialog.dialog_text = "Your speech edits will be lost. Do you want to discard them?"
|
||||||
_cancel_confirm_dialog.get_ok_button().text = "Discard changes"
|
_cancel_confirm_dialog.get_ok_button().text = "Discard changes"
|
||||||
_cancel_confirm_dialog.get_cancel_button().text = "Keep editing"
|
_cancel_confirm_dialog.get_cancel_button().text = "Keep editing"
|
||||||
|
_cancel_confirm_dialog.min_size = Vector2i(560, 300)
|
||||||
_cancel_confirm_dialog.confirmed.connect(func() -> void: cancelled.emit())
|
_cancel_confirm_dialog.confirmed.connect(func() -> void: cancelled.emit())
|
||||||
_cancel_confirm_dialog.dialog_hide_on_ok = true
|
_cancel_confirm_dialog.dialog_hide_on_ok = true
|
||||||
add_child(_cancel_confirm_dialog)
|
add_child(_cancel_confirm_dialog)
|
||||||
|
STAGE_DIALOG_STYLER.apply(
|
||||||
|
_cancel_confirm_dialog,
|
||||||
|
_theme,
|
||||||
|
_theme.font(),
|
||||||
|
_cancel_confirm_dialog.get_cancel_button(),
|
||||||
|
[_cancel_confirm_dialog.get_ok_button()]
|
||||||
|
)
|
||||||
_build_message_dialog()
|
_build_message_dialog()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -160,8 +160,12 @@ func open_selector(
|
|||||||
current_stickman_path: String,
|
current_stickman_path: String,
|
||||||
current_sky_id: StringName = &"",
|
current_sky_id: StringName = &"",
|
||||||
background_kind: StringName = &"sky",
|
background_kind: StringName = &"sky",
|
||||||
plain_color: Color = Color("#F5EFE0")
|
plain_color: Color = Color("#F5EFE0"),
|
||||||
|
preserve_scenery_browse_state: bool = false
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
|
var scenery_browse_state: Dictionary = {}
|
||||||
|
if preserve_scenery_browse_state and kind == "scenery" and selector != null:
|
||||||
|
scenery_browse_state = selector.capture_browse_state()
|
||||||
_current_background_kind = &"plain" if background_kind == &"plain" else &"sky"
|
_current_background_kind = &"plain" if background_kind == &"plain" else &"sky"
|
||||||
_current_sky_id = current_sky_id if not current_sky_id.is_empty() else &"sky_neutral"
|
_current_sky_id = current_sky_id if not current_sky_id.is_empty() else &"sky_neutral"
|
||||||
_current_plain_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
|
_current_plain_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
|
||||||
@@ -177,7 +181,7 @@ func open_selector(
|
|||||||
selected_stickman_path = String(entries[0].get("path", ""))
|
selected_stickman_path = String(entries[0].get("path", ""))
|
||||||
var auto_selection: Dictionary = entries[0] if kind == "stickman" and entries.size() == 1 else {}
|
var auto_selection: Dictionary = entries[0] if kind == "stickman" and entries.size() == 1 else {}
|
||||||
if auto_selection.is_empty():
|
if auto_selection.is_empty():
|
||||||
open(kind, entries, _current_sky_id)
|
open(kind, entries, _current_sky_id, scenery_browse_state)
|
||||||
return {
|
return {
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"selected_stickman_path": selected_stickman_path,
|
"selected_stickman_path": selected_stickman_path,
|
||||||
@@ -201,14 +205,19 @@ func set_selector_kind(kind: String) -> void:
|
|||||||
selector_kind = kind
|
selector_kind = kind
|
||||||
|
|
||||||
|
|
||||||
func open(kind: String, entries: Array[Dictionary], current_sky_id: StringName = &"") -> void:
|
func open(
|
||||||
|
kind: String,
|
||||||
|
entries: Array[Dictionary],
|
||||||
|
current_sky_id: StringName = &"",
|
||||||
|
scenery_browse_state: Dictionary = {}
|
||||||
|
) -> void:
|
||||||
selector_kind = kind
|
selector_kind = kind
|
||||||
selector_open = true
|
selector_open = true
|
||||||
if selector_dim != null:
|
if selector_dim != null:
|
||||||
selector_dim.visible = true
|
selector_dim.visible = true
|
||||||
selector_dim.color = Color(0.0, 0.0, 0.0, SELECTOR_DIM_ALPHA)
|
selector_dim.color = Color(0.0, 0.0, 0.0, SELECTOR_DIM_ALPHA)
|
||||||
if selector != null:
|
if selector != null:
|
||||||
selector.open(kind, entries, String(current_sky_id), String(_current_background_kind), _current_plain_color)
|
selector.open(kind, entries, String(current_sky_id), String(_current_background_kind), _current_plain_color, scenery_browse_state)
|
||||||
load_or_enqueue_thumbnails(kind, entries)
|
load_or_enqueue_thumbnails(kind, entries)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -426,17 +426,19 @@ func _build_file_menu(parent: HBoxContainer) -> void:
|
|||||||
var file_popup: PopupMenu = _file_menu.get_popup()
|
var file_popup: PopupMenu = _file_menu.get_popup()
|
||||||
var popup_font_size: int = _theme_integer("fonts.sizes.file_menu_popup", 26, 12, 64)
|
var popup_font_size: int = _theme_integer("fonts.sizes.file_menu_popup", 26, 12, 64)
|
||||||
file_popup.add_theme_font_size_override("font_size", popup_font_size)
|
file_popup.add_theme_font_size_override("font_size", popup_font_size)
|
||||||
file_popup.add_item("New", 0)
|
file_popup.add_item("🆕 New", 0)
|
||||||
file_popup.add_item("Open…", 1)
|
file_popup.add_item("📂 Open…", 1)
|
||||||
var recent_popup := PopupMenu.new()
|
var recent_popup := PopupMenu.new()
|
||||||
recent_popup.name = "OpenRecent"
|
recent_popup.name = "OpenRecent"
|
||||||
recent_popup.add_theme_font_size_override("font_size", popup_font_size)
|
recent_popup.add_theme_font_size_override("font_size", popup_font_size)
|
||||||
recent_popup.id_pressed.connect(_on_recent_scene_requested)
|
recent_popup.id_pressed.connect(_on_recent_scene_requested)
|
||||||
file_popup.add_child(recent_popup)
|
file_popup.add_child(recent_popup)
|
||||||
file_popup.add_submenu_item("Open Recent", "OpenRecent")
|
file_popup.add_submenu_item("🕑 Open Recent", "OpenRecent")
|
||||||
file_popup.add_item("Save", 2)
|
file_popup.add_separator()
|
||||||
file_popup.add_item("Save As…", 3)
|
file_popup.add_item("💾 Save", 2)
|
||||||
file_popup.add_item("Rename Scene…", 4)
|
file_popup.add_item("📄 Save As…", 3)
|
||||||
|
file_popup.add_separator()
|
||||||
|
file_popup.add_item("✏️ Rename Scene…", 4)
|
||||||
file_popup.id_pressed.connect(_on_file_menu_requested)
|
file_popup.id_pressed.connect(_on_file_menu_requested)
|
||||||
file_popup.about_to_popup.connect(_on_recent_menu_requested)
|
file_popup.about_to_popup.connect(_on_recent_menu_requested)
|
||||||
parent.add_child(_file_menu)
|
parent.add_child(_file_menu)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const SPEECH_SHOW_TOOLBAR_SCRIPT := preload("res://scripts/speech/speech_show_to
|
|||||||
const WAYPOINT_CONTEXT_SCRIPT := preload("res://scripts/stage/waypoint_context.gd")
|
const WAYPOINT_CONTEXT_SCRIPT := preload("res://scripts/stage/waypoint_context.gd")
|
||||||
const PROP_LIBRARY_SCRIPT := preload("res://scripts/props/prop_library.gd")
|
const PROP_LIBRARY_SCRIPT := preload("res://scripts/props/prop_library.gd")
|
||||||
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
||||||
|
const DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
|
|
||||||
signal object_selected(node: Node2D)
|
signal object_selected(node: Node2D)
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ var scene_name_edit: LineEdit
|
|||||||
var missing_assets_dialog: AcceptDialog
|
var missing_assets_dialog: AcceptDialog
|
||||||
var cli_error_dialog: AcceptDialog
|
var cli_error_dialog: AcceptDialog
|
||||||
var unsaved_changes_dialog: ConfirmationDialog
|
var unsaved_changes_dialog: ConfirmationDialog
|
||||||
|
var unsaved_discard_button: Button
|
||||||
var rig_context_popup: PopupMenu
|
var rig_context_popup: PopupMenu
|
||||||
var waypoint_context: PopupMenu
|
var waypoint_context: PopupMenu
|
||||||
var confirm_dialog: ConfirmationDialog
|
var confirm_dialog: ConfirmationDialog
|
||||||
@@ -51,7 +53,7 @@ func configure(sandbox_theme: SandboxTheme, ui_font: Font) -> void:
|
|||||||
|
|
||||||
func build_action_dialogs(parent: Node) -> void:
|
func build_action_dialogs(parent: Node) -> void:
|
||||||
speak_dialog = AcceptDialog.new()
|
speak_dialog = AcceptDialog.new()
|
||||||
speak_dialog.title = "Speak"
|
speak_dialog.title = "🗣️ Speak"
|
||||||
_speak_box = VBoxContainer.new()
|
_speak_box = VBoxContainer.new()
|
||||||
speak_dialog.add_child(_speak_box)
|
speak_dialog.add_child(_speak_box)
|
||||||
speak_edit = LineEdit.new()
|
speak_edit = LineEdit.new()
|
||||||
@@ -67,7 +69,7 @@ func build_action_dialogs(parent: Node) -> void:
|
|||||||
apply_dialog_theme(speak_dialog)
|
apply_dialog_theme(speak_dialog)
|
||||||
|
|
||||||
wait_dialog = AcceptDialog.new()
|
wait_dialog = AcceptDialog.new()
|
||||||
wait_dialog.title = "Wait"
|
wait_dialog.title = "⏱️ Wait"
|
||||||
wait_spin = SpinBox.new()
|
wait_spin = SpinBox.new()
|
||||||
wait_spin.min_value = 0.1
|
wait_spin.min_value = 0.1
|
||||||
wait_spin.max_value = 10.0
|
wait_spin.max_value = 10.0
|
||||||
@@ -97,7 +99,7 @@ func build_file_dialogs(parent: Node) -> void:
|
|||||||
|
|
||||||
speech_save_gate_dialog = ConfirmationDialog.new()
|
speech_save_gate_dialog = ConfirmationDialog.new()
|
||||||
speech_save_gate_dialog.name = "FinishSpeechBeforeSaveDialog"
|
speech_save_gate_dialog.name = "FinishSpeechBeforeSaveDialog"
|
||||||
speech_save_gate_dialog.title = "Finish speech before saving"
|
speech_save_gate_dialog.title = "✏️ Finish speech before saving"
|
||||||
speech_save_gate_dialog.dialog_text = "Choose Done speech to finish editing before the scene is saved."
|
speech_save_gate_dialog.dialog_text = "Choose Done speech to finish editing before the scene is saved."
|
||||||
speech_save_gate_dialog.ok_button_text = "Done speech"
|
speech_save_gate_dialog.ok_button_text = "Done speech"
|
||||||
speech_save_gate_dialog.get_cancel_button().text = "Keep editing"
|
speech_save_gate_dialog.get_cancel_button().text = "Keep editing"
|
||||||
@@ -106,50 +108,64 @@ func build_file_dialogs(parent: Node) -> void:
|
|||||||
|
|
||||||
group_preview_save_dialog = ConfirmationDialog.new()
|
group_preview_save_dialog = ConfirmationDialog.new()
|
||||||
group_preview_save_dialog.name = "GroupPreviewSaveDialog"
|
group_preview_save_dialog.name = "GroupPreviewSaveDialog"
|
||||||
group_preview_save_dialog.title = "Finish scenery preview?"
|
group_preview_save_dialog.title = "🧱 Finish scenery preview?"
|
||||||
group_preview_save_dialog.dialog_text = "Keep the scenery changes before saving, or cancel this save."
|
group_preview_save_dialog.dialog_text = "Keep the scenery changes before saving, or cancel this save."
|
||||||
group_preview_save_dialog.ok_button_text = "Keep changes"
|
group_preview_save_dialog.ok_button_text = "Keep changes"
|
||||||
group_preview_save_dialog.cancel_button_text = "Cancel save"
|
group_preview_save_dialog.cancel_button_text = "Cancel save"
|
||||||
group_preview_save_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300))
|
group_preview_save_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
parent.add_child(group_preview_save_dialog)
|
parent.add_child(group_preview_save_dialog)
|
||||||
apply_dialog_theme(group_preview_save_dialog)
|
apply_dialog_theme(group_preview_save_dialog)
|
||||||
|
|
||||||
rename_scene_dialog = ConfirmationDialog.new()
|
rename_scene_dialog = ConfirmationDialog.new()
|
||||||
rename_scene_dialog.name = "RenameSceneDialog"
|
rename_scene_dialog.name = "RenameSceneDialog"
|
||||||
rename_scene_dialog.title = "Rename Scene"
|
rename_scene_dialog.title = "📝 Rename Scene"
|
||||||
|
rename_scene_dialog.ok_button_text = "Save name"
|
||||||
rename_scene_dialog.cancel_button_text = "Cancel"
|
rename_scene_dialog.cancel_button_text = "Cancel"
|
||||||
rename_scene_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300))
|
rename_scene_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
|
var scene_name_hint: Label = Label.new()
|
||||||
|
scene_name_hint.name = "SceneNameHint"
|
||||||
|
scene_name_hint.text = "Choose a short name that helps you find this scene later."
|
||||||
|
scene_name_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
scene_name_hint.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
var scene_name_label: Label = Label.new()
|
||||||
|
scene_name_label.name = "SceneNameLabel"
|
||||||
|
scene_name_label.text = "Scene name"
|
||||||
scene_name_edit = LineEdit.new()
|
scene_name_edit = LineEdit.new()
|
||||||
scene_name_edit.name = "SceneNameEdit"
|
scene_name_edit.name = "SceneNameEdit"
|
||||||
scene_name_edit.placeholder_text = "Scene name"
|
scene_name_edit.placeholder_text = "Scene name"
|
||||||
scene_name_edit.custom_minimum_size = Vector2(460.0, 0.0)
|
scene_name_edit.custom_minimum_size = Vector2(460.0, 48.0)
|
||||||
|
rename_scene_dialog.add_child(scene_name_hint)
|
||||||
|
rename_scene_dialog.add_child(scene_name_label)
|
||||||
rename_scene_dialog.add_child(scene_name_edit)
|
rename_scene_dialog.add_child(scene_name_edit)
|
||||||
parent.add_child(rename_scene_dialog)
|
parent.add_child(rename_scene_dialog)
|
||||||
apply_dialog_theme(rename_scene_dialog)
|
apply_dialog_theme(rename_scene_dialog)
|
||||||
|
|
||||||
missing_assets_dialog = AcceptDialog.new()
|
missing_assets_dialog = AcceptDialog.new()
|
||||||
missing_assets_dialog.name = "MissingAssetsDialog"
|
missing_assets_dialog.name = "MissingAssetsDialog"
|
||||||
missing_assets_dialog.title = "Missing Scene Assets"
|
missing_assets_dialog.title = "⚠️ Some scene assets are missing"
|
||||||
missing_assets_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300))
|
missing_assets_dialog.ok_button_text = "Got it"
|
||||||
|
missing_assets_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
parent.add_child(missing_assets_dialog)
|
parent.add_child(missing_assets_dialog)
|
||||||
apply_dialog_theme(missing_assets_dialog)
|
apply_dialog_theme(missing_assets_dialog)
|
||||||
|
|
||||||
cli_error_dialog = AcceptDialog.new()
|
cli_error_dialog = AcceptDialog.new()
|
||||||
cli_error_dialog.name = "CliErrorDialog"
|
cli_error_dialog.name = "CliErrorDialog"
|
||||||
cli_error_dialog.title = "Could Not Load Scene"
|
cli_error_dialog.title = "⚠️ Could not load scene"
|
||||||
cli_error_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300))
|
cli_error_dialog.ok_button_text = "Got it"
|
||||||
|
cli_error_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
parent.add_child(cli_error_dialog)
|
parent.add_child(cli_error_dialog)
|
||||||
apply_dialog_theme(cli_error_dialog)
|
apply_dialog_theme(cli_error_dialog)
|
||||||
|
|
||||||
unsaved_changes_dialog = ConfirmationDialog.new()
|
unsaved_changes_dialog = ConfirmationDialog.new()
|
||||||
unsaved_changes_dialog.name = "UnsavedChangesDialog"
|
unsaved_changes_dialog.name = "UnsavedChangesDialog"
|
||||||
unsaved_changes_dialog.title = "⚠️ Unsaved Changes"
|
unsaved_changes_dialog.title = "💾 Unsaved Changes"
|
||||||
unsaved_changes_dialog.dialog_text = "Would you like to save your scene before continuing?"
|
unsaved_changes_dialog.dialog_text = "Would you like to save your scene before continuing?"
|
||||||
unsaved_changes_dialog.ok_button_text = "💾 Save"
|
unsaved_changes_dialog.ok_button_text = "Save scene"
|
||||||
unsaved_changes_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300))
|
unsaved_changes_dialog.cancel_button_text = "Cancel"
|
||||||
unsaved_changes_dialog.add_button("🗑️ Discard", false, "discard")
|
unsaved_changes_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
|
unsaved_discard_button = unsaved_changes_dialog.add_button("Discard", false, "discard")
|
||||||
parent.add_child(unsaved_changes_dialog)
|
parent.add_child(unsaved_changes_dialog)
|
||||||
apply_dialog_theme(unsaved_changes_dialog)
|
apply_dialog_theme(unsaved_changes_dialog, unsaved_changes_dialog.get_ok_button(), [unsaved_discard_button])
|
||||||
|
|
||||||
|
|
||||||
func build_context_and_editor_views(
|
func build_context_and_editor_views(
|
||||||
@@ -169,7 +185,11 @@ func build_context_and_editor_views(
|
|||||||
parent.add_child(waypoint_context)
|
parent.add_child(waypoint_context)
|
||||||
|
|
||||||
confirm_dialog = ConfirmationDialog.new()
|
confirm_dialog = ConfirmationDialog.new()
|
||||||
confirm_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(620, 300))
|
confirm_dialog.title = "❔ Confirm this change"
|
||||||
|
confirm_dialog.dialog_text = "This action updates the current scene. Do you want to continue?"
|
||||||
|
confirm_dialog.ok_button_text = "Continue"
|
||||||
|
confirm_dialog.cancel_button_text = "Cancel"
|
||||||
|
confirm_dialog.min_size = theme_dialog_size("confirm_size", Vector2i(560, 300))
|
||||||
parent.add_child(confirm_dialog)
|
parent.add_child(confirm_dialog)
|
||||||
apply_dialog_theme(confirm_dialog)
|
apply_dialog_theme(confirm_dialog)
|
||||||
|
|
||||||
@@ -196,18 +216,20 @@ func build_context_and_editor_views(
|
|||||||
parent.add_child(speech_show_toolbar)
|
parent.add_child(speech_show_toolbar)
|
||||||
|
|
||||||
speech_recovery_dialog = ConfirmationDialog.new()
|
speech_recovery_dialog = ConfirmationDialog.new()
|
||||||
speech_recovery_dialog.title = "Speech draft found"
|
speech_recovery_dialog.title = "🗣️ Speech draft found"
|
||||||
speech_recovery_dialog.dialog_text = "Restore your unsaved speech draft?"
|
speech_recovery_dialog.dialog_text = "There is an unsaved speech draft. Would you like to restore it?\n\nℹ️ Restore brings back the draft so you can continue editing."
|
||||||
speech_recovery_dialog.get_ok_button().text = "Restore"
|
speech_recovery_dialog.get_ok_button().text = "Restore draft"
|
||||||
speech_recovery_dialog.get_cancel_button().text = "Discard"
|
speech_recovery_dialog.get_cancel_button().text = "Discard draft"
|
||||||
parent.add_child(speech_recovery_dialog)
|
parent.add_child(speech_recovery_dialog)
|
||||||
|
apply_dialog_theme(speech_recovery_dialog, speech_recovery_dialog.get_ok_button(), [speech_recovery_dialog.get_cancel_button()])
|
||||||
|
|
||||||
speech_destination_dialog = ConfirmationDialog.new()
|
speech_destination_dialog = ConfirmationDialog.new()
|
||||||
speech_destination_dialog.title = "Save recovered speech"
|
speech_destination_dialog.title = "🎭 Save recovered speech"
|
||||||
speech_destination_dialog.dialog_text = "Add this speech to the chosen character’s action queue?"
|
speech_destination_dialog.dialog_text = "Add this speech to the chosen character’s action queue?"
|
||||||
speech_destination_dialog.get_ok_button().text = "Add to queue"
|
speech_destination_dialog.get_ok_button().text = "Add to queue"
|
||||||
speech_destination_dialog.get_cancel_button().text = "Keep editing"
|
speech_destination_dialog.get_cancel_button().text = "Keep editing"
|
||||||
parent.add_child(speech_destination_dialog)
|
parent.add_child(speech_destination_dialog)
|
||||||
|
apply_dialog_theme(speech_destination_dialog)
|
||||||
|
|
||||||
|
|
||||||
func show_objects_here(
|
func show_objects_here(
|
||||||
@@ -310,35 +332,11 @@ func theme_dialog_size(key: String, fallback: Vector2i) -> Vector2i:
|
|||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
## Dialogs are separate native/embedded windows, so apply local visual overrides.
|
## Keep the native dialog objects as signal sources and draw their shared surface in the stage CanvasLayer.
|
||||||
func apply_dialog_theme(dialog: Window) -> void:
|
func apply_dialog_theme(dialog: Window, primary_button: Button = null, destructive_buttons: Array = []) -> void:
|
||||||
if dialog == null:
|
if dialog == null:
|
||||||
return
|
return
|
||||||
var size: int = _sandbox_theme.integer("chrome.dialogs.font_size", 22, 12, 48) if _sandbox_theme != null else 22
|
DIALOG_STYLER.apply(dialog, _sandbox_theme, _ui_font, primary_button, destructive_buttons)
|
||||||
var message_size: int = _sandbox_theme.integer("chrome.dialogs.message_font_size", 26, 12, 64) if _sandbox_theme != null else 26
|
|
||||||
dialog.add_theme_font_size_override("font_size", size)
|
|
||||||
if _ui_font != null:
|
|
||||||
dialog.add_theme_font_override("font", _ui_font)
|
|
||||||
if dialog is AcceptDialog:
|
|
||||||
var message: Label = (dialog as AcceptDialog).get_label()
|
|
||||||
if message != null:
|
|
||||||
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
message.add_theme_font_size_override("font_size", message_size)
|
|
||||||
if _ui_font != null:
|
|
||||||
message.add_theme_font_override("font", _ui_font)
|
|
||||||
var ok: Button = (dialog as AcceptDialog).get_ok_button()
|
|
||||||
if ok != null:
|
|
||||||
ok.add_theme_font_size_override("font_size", size)
|
|
||||||
if _ui_font != null:
|
|
||||||
ok.add_theme_font_override("font", _ui_font)
|
|
||||||
if dialog is ConfirmationDialog:
|
|
||||||
var cancel: Button = (dialog as ConfirmationDialog).get_cancel_button()
|
|
||||||
if cancel != null:
|
|
||||||
cancel.add_theme_font_size_override("font_size", size)
|
|
||||||
if _ui_font != null:
|
|
||||||
cancel.add_theme_font_override("font", _ui_font)
|
|
||||||
_apply_dialog_font_to_children(dialog, size)
|
|
||||||
|
|
||||||
|
|
||||||
func apply_dialog_font_to_children(node: Node, size: int) -> void:
|
func apply_dialog_font_to_children(node: Node, size: int) -> void:
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ func is_active() -> bool:
|
|||||||
return not _active_tool.is_empty()
|
return not _active_tool.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func is_placement_tool() -> bool:
|
||||||
|
return _is_placement_tool()
|
||||||
|
|
||||||
|
|
||||||
func is_group_preview_pending() -> bool:
|
func is_group_preview_pending() -> bool:
|
||||||
return _group_pending
|
return _group_pending
|
||||||
|
|
||||||
|
|||||||
@@ -893,7 +893,7 @@ func _apply_compact_action_labels(compact: bool) -> void:
|
|||||||
"resize_group": "Size",
|
"resize_group": "Size",
|
||||||
"keep_group": "Keep",
|
"keep_group": "Keep",
|
||||||
"cancel_group": "Cancel",
|
"cancel_group": "Cancel",
|
||||||
"done_tool": "Done",
|
"done_tool": "Back",
|
||||||
"cancel_tool": "Cancel",
|
"cancel_tool": "Cancel",
|
||||||
"depth_far": "Back",
|
"depth_far": "Back",
|
||||||
"depth_near": "Middle",
|
"depth_near": "Middle",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ signal shape_selected()
|
|||||||
signal shape_copy_requested(shape_dict: Dictionary)
|
signal shape_copy_requested(shape_dict: Dictionary)
|
||||||
signal shape_paste_requested(world_pos: Vector2)
|
signal shape_paste_requested(world_pos: Vector2)
|
||||||
signal color_selected(color: Color)
|
signal color_selected(color: Color)
|
||||||
|
signal presentation_safe
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Exported / public properties
|
# Exported / public properties
|
||||||
@@ -63,9 +64,10 @@ const ZOOM_STEP: float = 1.10
|
|||||||
|
|
||||||
@onready var title_label: Label = %TitleLabel
|
@onready var title_label: Label = %TitleLabel
|
||||||
@onready var drawing_area: Control = %DrawingArea
|
@onready var drawing_area: Control = %DrawingArea
|
||||||
@onready var context_menu: PopupMenu = %ContextMenu
|
@onready var context_menu: EditorContextMenu = %ContextMenu
|
||||||
@onready var color_picker_popup: PopupPanel = %ColorPickerPopup
|
@onready var color_picker_popup: PopupPanel = %ColorPickerPopup
|
||||||
@onready var color_picker: ColorPicker = %ColorPicker
|
@onready var color_picker: ColorPicker = %ColorPicker
|
||||||
|
@onready var color_picker_button_row: HBoxContainer = %ButtonRow
|
||||||
@onready var cancel_button: Button = %CancelButton
|
@onready var cancel_button: Button = %CancelButton
|
||||||
@onready var ok_button: Button = %OKButton
|
@onready var ok_button: Button = %OKButton
|
||||||
|
|
||||||
@@ -89,6 +91,12 @@ var _snap_enabled: bool = false
|
|||||||
var _is_panning: bool = false
|
var _is_panning: bool = false
|
||||||
var _pan_start: Vector2 = Vector2.ZERO
|
var _pan_start: Vector2 = Vector2.ZERO
|
||||||
var _restore_color: Color = Color.BLACK
|
var _restore_color: Color = Color.BLACK
|
||||||
|
var _grid_color: Color = GRID_COLOR
|
||||||
|
var _selection_core_color: Color = Color.WHITE
|
||||||
|
var _selection_halo_color: Color = Color(0.0, 0.0, 0.0, 0.85)
|
||||||
|
var _hint_color: Color = Color(0.67, 0.70, 0.75, 0.8)
|
||||||
|
var _button_target_height: float = 44.0
|
||||||
|
var _editor_visual_theme: EditorVisualTheme
|
||||||
|
|
||||||
# Phase 4: tracks which shape was right-clicked for context menu ops.
|
# Phase 4: tracks which shape was right-clicked for context menu ops.
|
||||||
var _context_shape_idx: int = -1
|
var _context_shape_idx: int = -1
|
||||||
@@ -117,6 +125,20 @@ func _ready() -> void:
|
|||||||
ok_button.pressed.connect(_on_color_picker_ok)
|
ok_button.pressed.connect(_on_color_picker_ok)
|
||||||
if not cancel_button.pressed.is_connected(_on_color_picker_cancel):
|
if not cancel_button.pressed.is_connected(_on_color_picker_cancel):
|
||||||
cancel_button.pressed.connect(_on_color_picker_cancel)
|
cancel_button.pressed.connect(_on_color_picker_cancel)
|
||||||
|
if not context_menu.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||||
|
context_menu.popup_hide.connect(_on_presentation_popup_hidden)
|
||||||
|
if _editor_visual_theme != null:
|
||||||
|
context_menu.configure(_editor_visual_theme, &"stickman")
|
||||||
|
if not color_picker_popup.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||||
|
color_picker_popup.popup_hide.connect(_on_presentation_popup_hidden)
|
||||||
|
cancel_button.text = "Cancel"
|
||||||
|
cancel_button.custom_minimum_size.y = _button_target_height
|
||||||
|
ok_button.text = "Use Color"
|
||||||
|
ok_button.custom_minimum_size.y = _button_target_height
|
||||||
|
color_picker_button_row.add_theme_constant_override("separation", 12)
|
||||||
|
if _editor_visual_theme != null:
|
||||||
|
_editor_visual_theme.style_button_role(ok_button, &"primary", &"stickman")
|
||||||
|
_editor_visual_theme.style_button_role(cancel_button, &"secondary", &"stickman")
|
||||||
|
|
||||||
_drawing_area_initialized = true
|
_drawing_area_initialized = true
|
||||||
|
|
||||||
@@ -262,6 +284,77 @@ func reset_view() -> void:
|
|||||||
_pan_offset = Vector2.ZERO
|
_pan_offset = Vector2.ZERO
|
||||||
drawing_area.queue_redraw()
|
drawing_area.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func can_switch_presentation() -> bool:
|
||||||
|
return not _is_panning \
|
||||||
|
and _dragging_vertex < 0 \
|
||||||
|
and not _is_dragging_shape \
|
||||||
|
and not context_menu.visible \
|
||||||
|
and not color_picker_popup.visible
|
||||||
|
|
||||||
|
|
||||||
|
func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo: Color, hint_color: Color = Color(0.67, 0.70, 0.75, 0.8), button_target_height: float = 44.0, editor_visual_theme: EditorVisualTheme = null) -> void:
|
||||||
|
_grid_color = grid_color
|
||||||
|
_selection_core_color = selection_core
|
||||||
|
_selection_halo_color = selection_halo
|
||||||
|
_hint_color = hint_color
|
||||||
|
_button_target_height = button_target_height
|
||||||
|
_editor_visual_theme = editor_visual_theme
|
||||||
|
if context_menu != null and _editor_visual_theme != null:
|
||||||
|
context_menu.configure(_editor_visual_theme, &"stickman")
|
||||||
|
if drawing_area != null:
|
||||||
|
drawing_area.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func set_active_presentation(active: bool, surface: Color, border: Color, accent: Color, section_font_size: int = 22) -> void:
|
||||||
|
var style: StyleBoxFlat = StyleBoxFlat.new()
|
||||||
|
style.bg_color = surface
|
||||||
|
style.border_color = accent if active else border
|
||||||
|
style.set_border_width_all(2 if active else 1)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 6
|
||||||
|
style.content_margin_right = 6
|
||||||
|
style.content_margin_top = 4
|
||||||
|
style.content_margin_bottom = 4
|
||||||
|
add_theme_stylebox_override("panel", style)
|
||||||
|
title_label.add_theme_color_override("font_color", accent if active else _selection_core_color)
|
||||||
|
title_label.add_theme_font_size_override("font_size", section_font_size)
|
||||||
|
|
||||||
|
|
||||||
|
func add_shape(kind: StringName, at_world: Vector2) -> bool:
|
||||||
|
match kind:
|
||||||
|
&"line": _create_line(at_world)
|
||||||
|
&"rectangle": _create_rectangle(at_world)
|
||||||
|
&"circle": _create_circle(at_world)
|
||||||
|
_:
|
||||||
|
return false
|
||||||
|
drawing_area.queue_redraw()
|
||||||
|
_emit_changed()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func selected_shape_index() -> int:
|
||||||
|
return _selected_shape_idx
|
||||||
|
|
||||||
|
|
||||||
|
func has_selected_shape() -> bool:
|
||||||
|
return _selected_shape_idx >= 0 and _selected_shape_idx < shapes.size()
|
||||||
|
|
||||||
|
|
||||||
|
func open_selected_color_picker() -> void:
|
||||||
|
if not has_selected_shape():
|
||||||
|
return
|
||||||
|
_context_shape_idx = _selected_shape_idx
|
||||||
|
_show_color_picker()
|
||||||
|
|
||||||
|
|
||||||
|
func get_view_context() -> Dictionary:
|
||||||
|
return {"zoom": _zoom, "pan_offset": _pan_offset}
|
||||||
|
|
||||||
|
|
||||||
|
func drawing_center_world() -> Vector2:
|
||||||
|
return _screen_to_world(drawing_area.size * 0.5)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Phase 5: Clipboard
|
# Phase 5: Clipboard
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -332,6 +425,24 @@ func _screen_to_world(pos: Vector2) -> Vector2:
|
|||||||
return (pos - _pan_offset) / _zoom
|
return (pos - _pan_offset) / _zoom
|
||||||
|
|
||||||
|
|
||||||
|
func _global_point_target_rect(world_pos: Vector2) -> Rect2:
|
||||||
|
var screen_pos := drawing_area.global_position + _pan_offset + world_pos * _zoom
|
||||||
|
return Rect2(screen_pos - Vector2(6.0, 6.0), Vector2(12.0, 12.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _global_shape_target_rect(sd: Dictionary) -> Rect2:
|
||||||
|
var points: PackedVector2Array = sd.get("points", PackedVector2Array())
|
||||||
|
if points.is_empty():
|
||||||
|
return Rect2()
|
||||||
|
var min_point := points[0]
|
||||||
|
var max_point := points[0]
|
||||||
|
for point: Vector2 in points:
|
||||||
|
min_point = min_point.min(point)
|
||||||
|
max_point = max_point.max(point)
|
||||||
|
var screen_position := drawing_area.global_position + _pan_offset + min_point * _zoom
|
||||||
|
return Rect2(screen_position, (max_point - min_point) * _zoom).grow(4.0)
|
||||||
|
|
||||||
|
|
||||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
func _snap_to_grid(pos: Vector2) -> Vector2:
|
||||||
var gs := float(_grid_size)
|
var gs := float(_grid_size)
|
||||||
if gs <= 0.0:
|
if gs <= 0.0:
|
||||||
@@ -358,11 +469,11 @@ func _on_drawing_area_draw() -> void:
|
|||||||
drawing_area.draw_string(
|
drawing_area.draw_string(
|
||||||
font,
|
font,
|
||||||
hint_pos,
|
hint_pos,
|
||||||
"Right-click to add shape",
|
"Use Add Shape above",
|
||||||
HORIZONTAL_ALIGNMENT_CENTER,
|
HORIZONTAL_ALIGNMENT_CENTER,
|
||||||
-1,
|
-1,
|
||||||
14,
|
14,
|
||||||
Color(0.5, 0.5, 0.5, 0.6)
|
_hint_color
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -386,7 +497,8 @@ func _on_drawing_area_draw() -> void:
|
|||||||
|
|
||||||
# 3. Selection highlight (only on selected shape)
|
# 3. Selection highlight (only on selected shape)
|
||||||
if i == _selected_shape_idx:
|
if i == _selected_shape_idx:
|
||||||
_draw_polyline(pts, Color.WHITE, closed, 2.5)
|
_draw_polyline(pts, _selection_halo_color, closed, 5.0)
|
||||||
|
_draw_polyline(pts, _selection_core_color, closed, 2.5)
|
||||||
|
|
||||||
# 4. Vertex handles (only on selected shape)
|
# 4. Vertex handles (only on selected shape)
|
||||||
for j: int in range(count):
|
for j: int in range(count):
|
||||||
@@ -396,6 +508,7 @@ func _on_drawing_area_draw() -> void:
|
|||||||
if sd_vf.size() > j:
|
if sd_vf.size() > j:
|
||||||
flag = sd_vf[j]
|
flag = sd_vf[j]
|
||||||
|
|
||||||
|
drawing_area.draw_circle(pt, (HANDLE_RADIUS + 2.0) / _zoom, _selection_halo_color)
|
||||||
var handle_color := color
|
var handle_color := color
|
||||||
if j == _dragging_vertex:
|
if j == _dragging_vertex:
|
||||||
handle_color = Color.YELLOW
|
handle_color = Color.YELLOW
|
||||||
@@ -423,12 +536,12 @@ func _draw_grid() -> void:
|
|||||||
|
|
||||||
var x: float = start_x
|
var x: float = start_x
|
||||||
while x <= world_end.x:
|
while x <= world_end.x:
|
||||||
drawing_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), GRID_COLOR, line_width)
|
drawing_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), _grid_color, line_width)
|
||||||
x += gs
|
x += gs
|
||||||
|
|
||||||
var y: float = start_y
|
var y: float = start_y
|
||||||
while y <= world_end.y:
|
while y <= world_end.y:
|
||||||
drawing_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), GRID_COLOR, line_width)
|
drawing_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), _grid_color, line_width)
|
||||||
y += gs
|
y += gs
|
||||||
|
|
||||||
|
|
||||||
@@ -470,6 +583,7 @@ func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
|||||||
_dragging_vertex = -1
|
_dragging_vertex = -1
|
||||||
else:
|
else:
|
||||||
_is_panning = false
|
_is_panning = false
|
||||||
|
_emit_presentation_safe()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Mouse wheel -> zoom
|
# 2. Mouse wheel -> zoom
|
||||||
@@ -546,9 +660,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
|||||||
if hit_idx >= 0:
|
if hit_idx >= 0:
|
||||||
_context_shape_idx = _selected_shape_idx
|
_context_shape_idx = _selected_shape_idx
|
||||||
context_menu.clear()
|
context_menu.clear()
|
||||||
context_menu.add_item("Remove Point", 6)
|
context_menu.add_destructive_item("Remove Point", 6)
|
||||||
context_menu.set_meta("vertex_index", hit_idx)
|
context_menu.set_meta("vertex_index", hit_idx)
|
||||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||||
return
|
return
|
||||||
|
|
||||||
for i: int in range(shapes.size() - 1, -1, -1):
|
for i: int in range(shapes.size() - 1, -1, -1):
|
||||||
@@ -559,9 +673,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
|||||||
if hit_idx >= 0:
|
if hit_idx >= 0:
|
||||||
_context_shape_idx = i
|
_context_shape_idx = i
|
||||||
context_menu.clear()
|
context_menu.clear()
|
||||||
context_menu.add_item("Remove Point", 6)
|
context_menu.add_destructive_item("Remove Point", 6)
|
||||||
context_menu.set_meta("vertex_index", hit_idx)
|
context_menu.set_meta("vertex_index", hit_idx)
|
||||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
context_menu.popup_at(mb.global_position, _global_point_target_rect(world_pos))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 2: Check if right-click is over any shape (reverse order = front first).
|
# Step 2: Check if right-click is over any shape (reverse order = front first).
|
||||||
@@ -585,22 +699,22 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
|||||||
context_menu.add_item("Create Point", 3)
|
context_menu.add_item("Create Point", 3)
|
||||||
context_menu.add_separator()
|
context_menu.add_separator()
|
||||||
|
|
||||||
context_menu.add_item("Color...", 4)
|
context_menu.add_item("Change Color…", 4)
|
||||||
context_menu.add_separator()
|
context_menu.add_separator()
|
||||||
context_menu.add_item("Send Back", 7)
|
context_menu.add_item("Copy Shape", 9)
|
||||||
context_menu.add_item("Bring Forward", 8)
|
|
||||||
context_menu.add_separator()
|
|
||||||
context_menu.add_item("Copy", 9)
|
|
||||||
if _has_clipboard:
|
if _has_clipboard:
|
||||||
context_menu.add_item("Paste", 10)
|
context_menu.add_item("Paste Shape", 10)
|
||||||
context_menu.add_separator()
|
context_menu.add_separator()
|
||||||
context_menu.add_item("Mirror X", 11)
|
context_menu.add_item("Flip Left-Right", 11)
|
||||||
context_menu.add_item("Mirror Y", 12)
|
context_menu.add_item("Flip Up-Down", 12)
|
||||||
context_menu.add_separator()
|
context_menu.add_separator()
|
||||||
context_menu.add_item("Delete", 5)
|
context_menu.add_item("Bring Forward", 8)
|
||||||
|
context_menu.add_item("Send Back", 7)
|
||||||
|
context_menu.add_separator()
|
||||||
|
context_menu.add_destructive_item("Erase Shape", 5)
|
||||||
|
|
||||||
context_menu.set_meta("click_position", world_pos)
|
context_menu.set_meta("click_position", world_pos)
|
||||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
context_menu.popup_at(mb.global_position, _global_shape_target_rect(found_sd))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 3: No shape under mouse -> shape creation menu.
|
# Step 3: No shape under mouse -> shape creation menu.
|
||||||
@@ -610,9 +724,9 @@ func _handle_right_click(mb: InputEventMouseButton, world_pos: Vector2) -> void:
|
|||||||
context_menu.add_item("Circle", 2)
|
context_menu.add_item("Circle", 2)
|
||||||
if _has_clipboard:
|
if _has_clipboard:
|
||||||
context_menu.add_separator()
|
context_menu.add_separator()
|
||||||
context_menu.add_item("Paste", 10)
|
context_menu.add_item("Paste Shape", 10)
|
||||||
context_menu.set_meta("click_position", world_pos)
|
context_menu.set_meta("click_position", world_pos)
|
||||||
context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
context_menu.popup_at(mb.global_position)
|
||||||
|
|
||||||
|
|
||||||
func _handle_left_press(world_pos: Vector2) -> void:
|
func _handle_left_press(world_pos: Vector2) -> void:
|
||||||
@@ -660,6 +774,7 @@ func _handle_left_release() -> void:
|
|||||||
_shape_drag_original_points = PackedVector2Array()
|
_shape_drag_original_points = PackedVector2Array()
|
||||||
drawing_area.queue_redraw()
|
drawing_area.queue_redraw()
|
||||||
_emit_changed()
|
_emit_changed()
|
||||||
|
_emit_presentation_safe()
|
||||||
return
|
return
|
||||||
|
|
||||||
if _dragging_vertex >= 0:
|
if _dragging_vertex >= 0:
|
||||||
@@ -667,6 +782,22 @@ func _handle_left_release() -> void:
|
|||||||
_drag_offset = Vector2.ZERO
|
_drag_offset = Vector2.ZERO
|
||||||
drawing_area.queue_redraw()
|
drawing_area.queue_redraw()
|
||||||
_emit_changed()
|
_emit_changed()
|
||||||
|
_emit_presentation_safe()
|
||||||
|
return
|
||||||
|
_emit_presentation_safe()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_presentation_popup_hidden() -> void:
|
||||||
|
_emit_presentation_safe()
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_presentation_safe() -> void:
|
||||||
|
call_deferred("_emit_presentation_safe_if_idle")
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_presentation_safe_if_idle() -> void:
|
||||||
|
if can_switch_presentation():
|
||||||
|
presentation_safe.emit()
|
||||||
|
|
||||||
|
|
||||||
func _on_context_menu_id_pressed(id: int) -> void:
|
func _on_context_menu_id_pressed(id: int) -> void:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
class_name StickmanEditor
|
||||||
extends Control
|
extends Control
|
||||||
## StickmanEditor - Main controller for the Stickman Studio editor.
|
## StickmanEditor - Main controller for the Stickman Studio editor.
|
||||||
##
|
##
|
||||||
@@ -15,6 +16,12 @@ extends Control
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
||||||
|
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||||
|
const SandboxThemeScript := preload("res://scripts/stage/sandbox_theme.gd")
|
||||||
|
const StageDialogStylerScript := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
|
|
||||||
|
enum WorkspaceView { OVERVIEW, FOCUS }
|
||||||
|
enum PendingDocumentAction { NONE, LOAD, CLEAR, QUIT }
|
||||||
|
|
||||||
const BODY_PART_NAMES: PackedStringArray = [
|
const BODY_PART_NAMES: PackedStringArray = [
|
||||||
"head", "torso",
|
"head", "torso",
|
||||||
@@ -89,6 +96,10 @@ const SETTINGS_VERSION := "1.0"
|
|||||||
const DEFAULT_GRID_SIZE := 15
|
const DEFAULT_GRID_SIZE := 15
|
||||||
const MIN_GRID_SIZE := 1
|
const MIN_GRID_SIZE := 1
|
||||||
const MAX_GRID_SIZE := 100
|
const MAX_GRID_SIZE := 100
|
||||||
|
const OVERVIEW_PANEL_MIN_WIDTH: float = 230.0
|
||||||
|
const OVERVIEW_PREVIEW_MIN_WIDTH: float = 320.0
|
||||||
|
const FOCUS_PANEL_MIN_WIDTH: float = 350.0
|
||||||
|
const WORKSPACE_GAP: float = 8.0
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# On-ready node references
|
# On-ready node references
|
||||||
@@ -97,6 +108,18 @@ const MAX_GRID_SIZE := 100
|
|||||||
@onready var _stickman_name_edit: LineEdit = %StickmanNameEdit
|
@onready var _stickman_name_edit: LineEdit = %StickmanNameEdit
|
||||||
@onready var _menu_bar: MenuBar = %MenuBar
|
@onready var _menu_bar: MenuBar = %MenuBar
|
||||||
@onready var _body_part_panels: Dictionary = {} ## { String : BodyPartPanel }
|
@onready var _body_part_panels: Dictionary = {} ## { String : BodyPartPanel }
|
||||||
|
@onready var _body_part_workspace: Control = %BodyPartWorkspace
|
||||||
|
@onready var _overview_scroll: ScrollContainer = %OverviewScroll
|
||||||
|
@onready var _focus_host: VBoxContainer = %FocusHost
|
||||||
|
@onready var _focus_panel_host: Control = %FocusPanelHost
|
||||||
|
@onready var _workspace_row: HSplitContainer = %WorkspaceRow
|
||||||
|
@onready var _overview_button: Button = %OverviewButton
|
||||||
|
@onready var _focus_button: Button = %FocusButton
|
||||||
|
@onready var _part_picker: EditorSelectionDropdown = %PartPicker
|
||||||
|
@onready var _add_shape_button: Button = %AddShapeButton
|
||||||
|
@onready var _color_button: Button = %ColorButton
|
||||||
|
@onready var _whole_stickman_button: Button = %WholeStickmanButton
|
||||||
|
@onready var _back_to_part_button: Button = %BackToPartButton
|
||||||
@onready var _whole_preview: WholeStickmanPreview = %WholeStickmanPreview
|
@onready var _whole_preview: WholeStickmanPreview = %WholeStickmanPreview
|
||||||
@onready var _save_dialog: FileDialog = %SaveDialog
|
@onready var _save_dialog: FileDialog = %SaveDialog
|
||||||
@onready var _load_dialog: FileDialog = %LoadDialog
|
@onready var _load_dialog: FileDialog = %LoadDialog
|
||||||
@@ -106,6 +129,25 @@ const MAX_GRID_SIZE := 100
|
|||||||
@onready var _grid_size_spin_box: SpinBox = %GridSizeSpinBox
|
@onready var _grid_size_spin_box: SpinBox = %GridSizeSpinBox
|
||||||
@onready var _status_cursor_coords: Label = %CursorCoords
|
@onready var _status_cursor_coords: Label = %CursorCoords
|
||||||
@onready var _status_snap_status: Label = %SnapStatus
|
@onready var _status_snap_status: Label = %SnapStatus
|
||||||
|
@onready var _status_save_status: Label = %SaveStatus
|
||||||
|
@onready var _unsaved_dialog: ConfirmationDialog = %UnsavedChangesDialog
|
||||||
|
|
||||||
|
var _editor_visual_theme: EditorVisualTheme
|
||||||
|
var _dialog_theme: SandboxTheme
|
||||||
|
var _body_part_origins: Dictionary = {}
|
||||||
|
var _workspace_view: WorkspaceView = WorkspaceView.OVERVIEW
|
||||||
|
var _view_choice_was_manual: bool = false
|
||||||
|
var _active_part_name: String = "head"
|
||||||
|
var _pending_view: int = -1
|
||||||
|
var _pending_part_name: String = ""
|
||||||
|
var _workspace_controls_initialized: bool = false
|
||||||
|
var _view_button_group: ButtonGroup
|
||||||
|
var _add_shape_menu: PopupMenu
|
||||||
|
var _preview_takes_workspace: bool = false
|
||||||
|
var _current_path: String = ""
|
||||||
|
var _dirty: bool = false
|
||||||
|
var _applying_document: bool = false
|
||||||
|
var _pending_document_action: PendingDocumentAction = PendingDocumentAction.NONE
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Grid / snap state
|
# Grid / snap state
|
||||||
@@ -131,19 +173,45 @@ var _view_menu: PopupMenu
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_editor_visual_theme = EditorVisualThemeScript.new()
|
||||||
|
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||||
|
_dialog_theme = SandboxThemeScript.new()
|
||||||
|
_dialog_theme.load_from(SandboxThemeScript.DEFAULT_PATH)
|
||||||
|
theme = _editor_visual_theme.control_theme(&"stickman")
|
||||||
_setup_menu_bar()
|
_setup_menu_bar()
|
||||||
_setup_dialogs()
|
_setup_dialogs()
|
||||||
_populate_body_part_panels()
|
_populate_body_part_panels()
|
||||||
|
_setup_workspace_controls()
|
||||||
_whole_preview.part_moved.connect(_on_preview_part_moved)
|
_whole_preview.part_moved.connect(_on_preview_part_moved)
|
||||||
|
_whole_preview.part_transform_committed.connect(_on_preview_part_transform_committed)
|
||||||
|
_whole_preview.part_order_changed.connect(_on_preview_part_order_changed)
|
||||||
_whole_preview.set_body_parts(_collect_all_shape_data())
|
_whole_preview.set_body_parts(_collect_all_shape_data())
|
||||||
|
_whole_preview.set_canvas_tokens(
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"grid_color"),
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"selection_core"),
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||||
|
_editor_visual_theme.chrome_color(&"preview", &"stickman")
|
||||||
|
)
|
||||||
|
_whole_preview.set_context_menu_visual_theme(_editor_visual_theme, &"stickman")
|
||||||
|
_stickman_name_edit.text_changed.connect(_on_stickman_name_changed)
|
||||||
|
|
||||||
_load_settings()
|
_load_settings()
|
||||||
_update_snap_status_label()
|
_update_snap_status_label()
|
||||||
_broadcast_settings()
|
_broadcast_settings()
|
||||||
_broadcast_recent_colors()
|
_broadcast_recent_colors()
|
||||||
|
_update_active_part_presentation()
|
||||||
|
_update_document_status()
|
||||||
|
|
||||||
if not _grid_config_dialog.confirmed.is_connected(_on_grid_config_confirmed):
|
if not _grid_config_dialog.confirmed.is_connected(_on_grid_config_confirmed):
|
||||||
_grid_config_dialog.confirmed.connect(_on_grid_config_confirmed)
|
_grid_config_dialog.confirmed.connect(_on_grid_config_confirmed)
|
||||||
|
call_deferred("_initialize_workspace_presentation")
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||||
|
_request_document_action(PendingDocumentAction.QUIT)
|
||||||
|
elif what == NOTIFICATION_RESIZED:
|
||||||
|
call_deferred("_refresh_responsive_workspace")
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
@@ -151,7 +219,7 @@ func _process(_delta: float) -> void:
|
|||||||
|
|
||||||
for part_name: String in BODY_PART_NAMES:
|
for part_name: String in BODY_PART_NAMES:
|
||||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||||
if panel and panel.is_cursor_over_drawing(mouse_pos):
|
if panel and panel.is_visible_in_tree() and panel.is_cursor_over_drawing(mouse_pos):
|
||||||
var world_pos := panel.global_to_world(mouse_pos)
|
var world_pos := panel.global_to_world(mouse_pos)
|
||||||
_status_cursor_coords.text = "X: %d Y: %d" % [int(world_pos.x), int(world_pos.y)]
|
_status_cursor_coords.text = "X: %d Y: %d" % [int(world_pos.x), int(world_pos.y)]
|
||||||
return
|
return
|
||||||
@@ -170,17 +238,18 @@ func _process(_delta: float) -> void:
|
|||||||
func _setup_menu_bar() -> void:
|
func _setup_menu_bar() -> void:
|
||||||
var file_menu: PopupMenu = PopupMenu.new()
|
var file_menu: PopupMenu = PopupMenu.new()
|
||||||
file_menu.name = "FileMenu"
|
file_menu.name = "FileMenu"
|
||||||
file_menu.add_item("Save", 0)
|
file_menu.add_item("📂 Load…", 1)
|
||||||
file_menu.add_item("Load", 1)
|
|
||||||
file_menu.add_separator()
|
file_menu.add_separator()
|
||||||
file_menu.add_item("Clear", 2)
|
file_menu.add_item("💾 Save", 0)
|
||||||
|
file_menu.add_separator()
|
||||||
|
file_menu.add_item("🆕 Start Over", 2)
|
||||||
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
||||||
_menu_bar.add_child(file_menu)
|
_menu_bar.add_child(file_menu)
|
||||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
||||||
|
|
||||||
_edit_menu = PopupMenu.new()
|
_edit_menu = PopupMenu.new()
|
||||||
_edit_menu.name = "EditMenu"
|
_edit_menu.name = "EditMenu"
|
||||||
_edit_menu.add_item("Configure Grid...", 0)
|
_edit_menu.add_item("⚙️ Configure Grid…", 0)
|
||||||
_edit_menu.add_separator()
|
_edit_menu.add_separator()
|
||||||
_edit_menu.add_item(_snap_menu_label(), 1)
|
_edit_menu.add_item(_snap_menu_label(), 1)
|
||||||
_edit_menu.id_pressed.connect(_on_edit_menu_id_pressed)
|
_edit_menu.id_pressed.connect(_on_edit_menu_id_pressed)
|
||||||
@@ -190,7 +259,7 @@ func _setup_menu_bar() -> void:
|
|||||||
|
|
||||||
var view_menu: PopupMenu = PopupMenu.new()
|
var view_menu: PopupMenu = PopupMenu.new()
|
||||||
view_menu.name = "ViewMenu"
|
view_menu.name = "ViewMenu"
|
||||||
view_menu.add_item("Reset Views", 0)
|
view_menu.add_item("🔍 Reset Views", 0)
|
||||||
view_menu.add_separator()
|
view_menu.add_separator()
|
||||||
view_menu.add_item(_guide_menu_label(), 1)
|
view_menu.add_item(_guide_menu_label(), 1)
|
||||||
view_menu.id_pressed.connect(_on_view_menu_id_pressed)
|
view_menu.id_pressed.connect(_on_view_menu_id_pressed)
|
||||||
@@ -201,12 +270,340 @@ func _setup_menu_bar() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _setup_dialogs() -> void:
|
func _setup_dialogs() -> void:
|
||||||
|
_save_dialog.theme = theme
|
||||||
|
_load_dialog.theme = theme
|
||||||
var content_dir: String = ProjectSettings.globalize_path(ContentPathsScript.STICKMEN_DIR)
|
var content_dir: String = ProjectSettings.globalize_path(ContentPathsScript.STICKMEN_DIR)
|
||||||
_save_dialog.current_dir = content_dir
|
_save_dialog.current_dir = content_dir
|
||||||
_load_dialog.current_dir = content_dir
|
_load_dialog.current_dir = content_dir
|
||||||
_save_dialog.file_selected.connect(_on_save_file_selected)
|
_save_dialog.file_selected.connect(_on_save_file_selected)
|
||||||
_load_dialog.file_selected.connect(_on_load_file_selected)
|
_load_dialog.file_selected.connect(_on_load_file_selected)
|
||||||
|
_editor_visual_theme.style_button_role(_save_dialog.get_ok_button(), &"primary", &"stickman")
|
||||||
|
_editor_visual_theme.style_button_role(_save_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||||
|
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"stickman")
|
||||||
|
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||||
_clear_confirm.confirmed.connect(_on_clear_confirmed)
|
_clear_confirm.confirmed.connect(_on_clear_confirmed)
|
||||||
|
_clear_confirm.title = "Start Over?"
|
||||||
|
_clear_confirm.dialog_text = "This will clear your current stickman. Start over?"
|
||||||
|
_clear_confirm.ok_button_text = "Start Over"
|
||||||
|
_clear_confirm.set_meta(StageDialogStylerScript.ICON_META, "🧹")
|
||||||
|
_clear_confirm.get_cancel_button().text = "Cancel"
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_clear_confirm, _dialog_theme, _dialog_theme.font(),
|
||||||
|
null, [_clear_confirm.get_ok_button()], [], false
|
||||||
|
)
|
||||||
|
_unsaved_dialog.title = "Unsaved Changes"
|
||||||
|
_unsaved_dialog.set_meta(StageDialogStylerScript.ICON_META, "💾")
|
||||||
|
var discard_button: Button = _unsaved_dialog.add_button("Discard", false, "discard")
|
||||||
|
_unsaved_dialog.get_cancel_button().text = "Cancel"
|
||||||
|
_unsaved_dialog.ok_button_text = "Save"
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_unsaved_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_unsaved_dialog.get_ok_button(), [discard_button]
|
||||||
|
)
|
||||||
|
_error_dialog.get_ok_button().text = "Got it"
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_error_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_error_dialog.get_ok_button()
|
||||||
|
)
|
||||||
|
_grid_config_dialog.set_meta(StageDialogStylerScript.ICON_META, "📏")
|
||||||
|
StageDialogStylerScript.apply(
|
||||||
|
_grid_config_dialog, _dialog_theme, _dialog_theme.font(),
|
||||||
|
_grid_config_dialog.get_ok_button()
|
||||||
|
)
|
||||||
|
_unsaved_dialog.confirmed.connect(_on_unsaved_save_pressed)
|
||||||
|
_unsaved_dialog.custom_action.connect(_on_unsaved_custom_action)
|
||||||
|
_unsaved_dialog.canceled.connect(_cancel_pending_document_action)
|
||||||
|
get_tree().auto_accept_quit = false
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_workspace_controls() -> void:
|
||||||
|
_view_button_group = ButtonGroup.new()
|
||||||
|
_overview_button.button_group = _view_button_group
|
||||||
|
_focus_button.button_group = _view_button_group
|
||||||
|
_overview_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||||
|
_focus_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"mode_button_min_height", &"stickman"))
|
||||||
|
_part_picker.custom_minimum_size = Vector2(220.0, float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")))
|
||||||
|
_part_picker.configure(
|
||||||
|
theme,
|
||||||
|
_editor_visual_theme.chrome_color(&"surface", &"stickman"),
|
||||||
|
_editor_visual_theme.chrome_color(&"selected_surface", &"stickman"),
|
||||||
|
_editor_visual_theme.chrome_color(&"border", &"stickman"),
|
||||||
|
_editor_visual_theme.chrome_color(&"edit", &"stickman"),
|
||||||
|
_editor_visual_theme.chrome_color(&"focus", &"stickman"),
|
||||||
|
_editor_visual_theme.chrome_color(&"text", &"stickman"),
|
||||||
|
_editor_visual_theme.chrome_px(&"menu_popup", &"stickman"),
|
||||||
|
12
|
||||||
|
)
|
||||||
|
_add_shape_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||||
|
_editor_visual_theme.style_button_role(_add_shape_button, &"primary", &"stickman")
|
||||||
|
_color_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||||
|
_editor_visual_theme.style_button_role(_color_button, &"secondary", &"stickman")
|
||||||
|
_whole_stickman_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||||
|
_back_to_part_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||||
|
_status_cursor_coords.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||||
|
_status_save_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||||
|
_status_snap_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||||
|
for part_name: String in BODY_PART_NAMES:
|
||||||
|
_part_picker.add_item(BODY_PART_DISPLAY[part_name])
|
||||||
|
_overview_button.pressed.connect(func() -> void: request_workspace_view(WorkspaceView.OVERVIEW))
|
||||||
|
_focus_button.pressed.connect(func() -> void: request_workspace_view(WorkspaceView.FOCUS))
|
||||||
|
_part_picker.item_selected.connect(_on_part_picker_selected)
|
||||||
|
_add_shape_button.pressed.connect(_on_add_shape_button_pressed)
|
||||||
|
_color_button.pressed.connect(_on_color_button_pressed)
|
||||||
|
_whole_stickman_button.pressed.connect(_on_whole_stickman_button_pressed)
|
||||||
|
_back_to_part_button.pressed.connect(_on_back_to_part_button_pressed)
|
||||||
|
_add_shape_menu = PopupMenu.new()
|
||||||
|
_add_shape_menu.add_item("〰️ Line", 0)
|
||||||
|
_add_shape_menu.add_item("▭ Rectangle", 1)
|
||||||
|
_add_shape_menu.add_item("⚪ Circle", 2)
|
||||||
|
_add_shape_menu.id_pressed.connect(_on_add_shape_menu_id_pressed)
|
||||||
|
_add_shape_menu.theme = theme
|
||||||
|
add_child(_add_shape_menu)
|
||||||
|
_workspace_controls_initialized = true
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_workspace_presentation() -> void:
|
||||||
|
if _workspace_row.size.x <= 0.0:
|
||||||
|
await get_tree().process_frame
|
||||||
|
if not _view_choice_was_manual:
|
||||||
|
var initial_view: WorkspaceView = WorkspaceView.OVERVIEW if _overview_width_fits() else WorkspaceView.FOCUS
|
||||||
|
request_workspace_view(initial_view, false)
|
||||||
|
_refresh_responsive_workspace()
|
||||||
|
|
||||||
|
|
||||||
|
func request_workspace_view(view: WorkspaceView, user_chosen: bool = true) -> void:
|
||||||
|
if user_chosen:
|
||||||
|
_view_choice_was_manual = true
|
||||||
|
var panel: BodyPartPanel = _active_panel()
|
||||||
|
if panel != null and not panel.can_switch_presentation():
|
||||||
|
_pending_view = int(view)
|
||||||
|
return
|
||||||
|
_pending_view = -1
|
||||||
|
_apply_workspace_view(view)
|
||||||
|
|
||||||
|
|
||||||
|
func request_active_part(part_name: String) -> void:
|
||||||
|
if not BODY_PART_NAMES.has(part_name):
|
||||||
|
return
|
||||||
|
var panel: BodyPartPanel = _active_panel()
|
||||||
|
if panel != null and not panel.can_switch_presentation():
|
||||||
|
_pending_part_name = part_name
|
||||||
|
return
|
||||||
|
_pending_part_name = ""
|
||||||
|
_set_active_part(part_name)
|
||||||
|
|
||||||
|
|
||||||
|
func get_workspace_view() -> WorkspaceView:
|
||||||
|
return _workspace_view
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_part_name() -> String:
|
||||||
|
return _active_part_name
|
||||||
|
|
||||||
|
|
||||||
|
func has_unsaved_changes() -> bool:
|
||||||
|
return _dirty
|
||||||
|
|
||||||
|
|
||||||
|
func current_path() -> String:
|
||||||
|
return _current_path
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_pending_presentation() -> void:
|
||||||
|
var panel: BodyPartPanel = _active_panel()
|
||||||
|
if panel != null and not panel.can_switch_presentation():
|
||||||
|
return
|
||||||
|
if not _pending_part_name.is_empty():
|
||||||
|
var requested_part: String = _pending_part_name
|
||||||
|
_pending_part_name = ""
|
||||||
|
_set_active_part(requested_part)
|
||||||
|
if _pending_view >= 0:
|
||||||
|
var requested_view: WorkspaceView = _pending_view
|
||||||
|
_pending_view = -1
|
||||||
|
_apply_workspace_view(requested_view)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_workspace_view(view: WorkspaceView) -> void:
|
||||||
|
if view == _workspace_view:
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
_refresh_responsive_workspace()
|
||||||
|
return
|
||||||
|
if _workspace_view == WorkspaceView.FOCUS:
|
||||||
|
_restore_panel_to_overview(_active_part_name)
|
||||||
|
_workspace_view = view
|
||||||
|
_preview_takes_workspace = false
|
||||||
|
if _workspace_view == WorkspaceView.FOCUS:
|
||||||
|
_move_panel_to_focus(_active_part_name)
|
||||||
|
_overview_scroll.visible = _workspace_view == WorkspaceView.OVERVIEW
|
||||||
|
_focus_host.visible = _workspace_view == WorkspaceView.FOCUS
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
_update_active_part_presentation()
|
||||||
|
_refresh_responsive_workspace()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_active_part(part_name: String) -> void:
|
||||||
|
if not BODY_PART_NAMES.has(part_name) or part_name == _active_part_name:
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
return
|
||||||
|
if _workspace_view == WorkspaceView.FOCUS:
|
||||||
|
_restore_panel_to_overview(_active_part_name)
|
||||||
|
_active_part_name = part_name
|
||||||
|
if _workspace_view == WorkspaceView.FOCUS:
|
||||||
|
_move_panel_to_focus(_active_part_name)
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
_update_active_part_presentation()
|
||||||
|
|
||||||
|
|
||||||
|
func _active_panel() -> BodyPartPanel:
|
||||||
|
return _body_part_panels.get(_active_part_name) as BodyPartPanel
|
||||||
|
|
||||||
|
|
||||||
|
func _move_panel_to_focus(part_name: String) -> void:
|
||||||
|
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||||
|
if panel == null or panel.get_parent() == _focus_panel_host:
|
||||||
|
return
|
||||||
|
var current_focus_panel: Control = _focus_panel_host.get_child(0) as Control if _focus_panel_host.get_child_count() > 0 else null
|
||||||
|
if current_focus_panel != null:
|
||||||
|
var current_part: String = String(current_focus_panel.get_meta("body_part_name", ""))
|
||||||
|
if not current_part.is_empty():
|
||||||
|
_restore_panel_to_overview(current_part)
|
||||||
|
var old_parent: Node = panel.get_parent()
|
||||||
|
if old_parent == null:
|
||||||
|
return
|
||||||
|
old_parent.remove_child(panel)
|
||||||
|
_focus_panel_host.add_child(panel)
|
||||||
|
panel.set_meta("body_part_name", part_name)
|
||||||
|
panel.layout_mode = 1
|
||||||
|
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
panel.custom_minimum_size = Vector2(FOCUS_PANEL_MIN_WIDTH, 300.0)
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
|
||||||
|
func _restore_panel_to_overview(part_name: String) -> void:
|
||||||
|
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||||
|
var origin: Dictionary = _body_part_origins.get(part_name, {})
|
||||||
|
if panel == null or origin.is_empty():
|
||||||
|
return
|
||||||
|
var origin_parent: Node = origin.get("parent") as Node
|
||||||
|
if origin_parent == null or panel.get_parent() == origin_parent:
|
||||||
|
return
|
||||||
|
var old_parent: Node = panel.get_parent()
|
||||||
|
if old_parent != null:
|
||||||
|
old_parent.remove_child(panel)
|
||||||
|
panel.layout_mode = 2
|
||||||
|
panel.custom_minimum_size = origin.get("minimum_size", Vector2(230.0, 160.0))
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
origin_parent.add_child(panel)
|
||||||
|
origin_parent.move_child(panel, int(origin.get("index", origin_parent.get_child_count() - 1)))
|
||||||
|
panel.remove_meta("body_part_name")
|
||||||
|
|
||||||
|
|
||||||
|
func _overview_width_fits() -> bool:
|
||||||
|
var required_width: float = OVERVIEW_PANEL_MIN_WIDTH * 2.0 + OVERVIEW_PREVIEW_MIN_WIDTH + WORKSPACE_GAP * 2.0
|
||||||
|
return _available_workspace_width() >= required_width
|
||||||
|
|
||||||
|
|
||||||
|
func _available_workspace_width() -> float:
|
||||||
|
var viewport_width: float = get_viewport_rect().size.x
|
||||||
|
return minf(size.x, viewport_width) if viewport_width > 0.0 else _workspace_row.size.x
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_responsive_workspace() -> void:
|
||||||
|
if not is_node_ready():
|
||||||
|
return
|
||||||
|
if not _view_choice_was_manual and _workspace_controls_initialized:
|
||||||
|
var responsive_view: WorkspaceView = WorkspaceView.OVERVIEW if _overview_width_fits() else WorkspaceView.FOCUS
|
||||||
|
if responsive_view != _workspace_view:
|
||||||
|
request_workspace_view(responsive_view, false)
|
||||||
|
var narrow_preview: bool = _available_workspace_width() < FOCUS_PANEL_MIN_WIDTH + OVERVIEW_PREVIEW_MIN_WIDTH + WORKSPACE_GAP
|
||||||
|
if _workspace_view == WorkspaceView.OVERVIEW:
|
||||||
|
_preview_takes_workspace = false
|
||||||
|
_body_part_workspace.visible = true
|
||||||
|
_whole_preview.visible = not narrow_preview
|
||||||
|
_whole_stickman_button.visible = false
|
||||||
|
_back_to_part_button.visible = false
|
||||||
|
return
|
||||||
|
if narrow_preview and _preview_takes_workspace:
|
||||||
|
_body_part_workspace.visible = false
|
||||||
|
_whole_preview.visible = true
|
||||||
|
_whole_stickman_button.visible = false
|
||||||
|
_back_to_part_button.visible = true
|
||||||
|
elif narrow_preview:
|
||||||
|
_body_part_workspace.visible = true
|
||||||
|
_whole_preview.visible = false
|
||||||
|
_whole_stickman_button.visible = true
|
||||||
|
_back_to_part_button.visible = false
|
||||||
|
else:
|
||||||
|
_preview_takes_workspace = false
|
||||||
|
_body_part_workspace.visible = true
|
||||||
|
_whole_preview.visible = true
|
||||||
|
_whole_stickman_button.visible = false
|
||||||
|
_back_to_part_button.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _update_active_part_presentation() -> void:
|
||||||
|
if _editor_visual_theme == null:
|
||||||
|
return
|
||||||
|
var surface: Color = _editor_visual_theme.chrome_color(&"surface", &"stickman")
|
||||||
|
var border: Color = _editor_visual_theme.chrome_color(&"border", &"stickman")
|
||||||
|
var accent: Color = _editor_visual_theme.chrome_color(&"edit", &"stickman")
|
||||||
|
var section_size: int = _editor_visual_theme.chrome_px(&"section", &"stickman")
|
||||||
|
for part_name: String in BODY_PART_NAMES:
|
||||||
|
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||||
|
if panel != null:
|
||||||
|
panel.set_active_presentation(part_name == _active_part_name, surface, border, accent, section_size)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_workspace_controls() -> void:
|
||||||
|
if not _workspace_controls_initialized:
|
||||||
|
return
|
||||||
|
_overview_button.set_pressed_no_signal(_workspace_view == WorkspaceView.OVERVIEW)
|
||||||
|
_focus_button.set_pressed_no_signal(_workspace_view == WorkspaceView.FOCUS)
|
||||||
|
var part_index: int = BODY_PART_NAMES.find(_active_part_name)
|
||||||
|
if part_index >= 0:
|
||||||
|
_part_picker.select(part_index)
|
||||||
|
var active: BodyPartPanel = _active_panel()
|
||||||
|
var selected: bool = active != null and active.has_selected_shape()
|
||||||
|
_color_button.disabled = not selected
|
||||||
|
_color_button.tooltip_text = "Select a shape to choose its color." if not selected else "Choose this shape's color."
|
||||||
|
_update_active_part_presentation()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_part_picker_selected(index: int) -> void:
|
||||||
|
if index >= 0 and index < BODY_PART_NAMES.size():
|
||||||
|
request_active_part(BODY_PART_NAMES[index])
|
||||||
|
|
||||||
|
|
||||||
|
func _on_add_shape_button_pressed() -> void:
|
||||||
|
var rect: Rect2 = Rect2(_add_shape_button.global_position + Vector2(0, _add_shape_button.size.y), Vector2(_add_shape_button.size.x, 1.0))
|
||||||
|
_add_shape_menu.popup(rect)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_add_shape_menu_id_pressed(id: int) -> void:
|
||||||
|
var kind: StringName = [&"line", &"rectangle", &"circle"][clampi(id, 0, 2)]
|
||||||
|
var panel: BodyPartPanel = _active_panel()
|
||||||
|
if panel != null:
|
||||||
|
panel.add_shape(kind, panel.drawing_center_world())
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_color_button_pressed() -> void:
|
||||||
|
var panel: BodyPartPanel = _active_panel()
|
||||||
|
if panel != null:
|
||||||
|
panel.open_selected_color_picker()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_whole_stickman_button_pressed() -> void:
|
||||||
|
_preview_takes_workspace = true
|
||||||
|
_refresh_responsive_workspace()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_to_part_button_pressed() -> void:
|
||||||
|
_preview_takes_workspace = false
|
||||||
|
_refresh_responsive_workspace()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Body-part panel population
|
# Body-part panel population
|
||||||
@@ -230,7 +627,17 @@ func _populate_body_part_panels() -> void:
|
|||||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
||||||
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
||||||
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
||||||
|
panel.presentation_safe.connect(_apply_pending_presentation)
|
||||||
|
panel.set_canvas_tokens(
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"grid_color"),
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"selection_core"),
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||||
|
_editor_visual_theme.chrome_color(&"text_muted", &"stickman"),
|
||||||
|
float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")),
|
||||||
|
_editor_visual_theme
|
||||||
|
)
|
||||||
_body_part_panels[part_name] = panel
|
_body_part_panels[part_name] = panel
|
||||||
|
_body_part_origins[part_name] = {"parent": left_col, "index": left_col.get_child_count(), "minimum_size": panel.custom_minimum_size}
|
||||||
left_col.add_child(panel)
|
left_col.add_child(panel)
|
||||||
|
|
||||||
for part_name: String in center_names:
|
for part_name: String in center_names:
|
||||||
@@ -242,7 +649,17 @@ func _populate_body_part_panels() -> void:
|
|||||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
||||||
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
||||||
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
||||||
|
panel.presentation_safe.connect(_apply_pending_presentation)
|
||||||
|
panel.set_canvas_tokens(
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"grid_color"),
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"selection_core"),
|
||||||
|
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||||
|
_editor_visual_theme.chrome_color(&"text_muted", &"stickman"),
|
||||||
|
float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")),
|
||||||
|
_editor_visual_theme
|
||||||
|
)
|
||||||
_body_part_panels[part_name] = panel
|
_body_part_panels[part_name] = panel
|
||||||
|
_body_part_origins[part_name] = {"parent": center_col, "index": center_col.get_child_count(), "minimum_size": panel.custom_minimum_size}
|
||||||
center_col.add_child(panel)
|
center_col.add_child(panel)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -252,18 +669,128 @@ func _populate_body_part_panels() -> void:
|
|||||||
func _on_file_menu_id_pressed(id: int) -> void:
|
func _on_file_menu_id_pressed(id: int) -> void:
|
||||||
match id:
|
match id:
|
||||||
0: # Save
|
0: # Save
|
||||||
_save_dialog.popup_centered_ratio(0.6)
|
_save_current_document()
|
||||||
1: # Load
|
1: # Load
|
||||||
|
_request_document_action(PendingDocumentAction.LOAD)
|
||||||
|
2: # Start Over
|
||||||
|
_request_document_action(PendingDocumentAction.CLEAR)
|
||||||
|
|
||||||
|
|
||||||
|
func _save_current_document() -> void:
|
||||||
|
if _current_path.is_empty():
|
||||||
|
_save_dialog.popup_centered_ratio(0.6)
|
||||||
|
return
|
||||||
|
save_to_path(_current_path)
|
||||||
|
|
||||||
|
|
||||||
|
func save_to_path(path: String) -> bool:
|
||||||
|
var save_path: String = ContentPathsScript.canonicalize(path)
|
||||||
|
if save_path.get_extension().to_lower() != "stk":
|
||||||
|
save_path = save_path.get_basename() + ".stk"
|
||||||
|
var file: FileAccess = FileAccess.open(save_path, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
_show_error("Could not save the stickman. Check that the file location is available.")
|
||||||
|
return false
|
||||||
|
file.store_string(JSON.stringify(_build_json_data(), "\t", false))
|
||||||
|
var write_error: Error = file.get_error()
|
||||||
|
file.close()
|
||||||
|
if write_error != OK:
|
||||||
|
_show_error("Could not finish saving the stickman. Check the file location and try again.")
|
||||||
|
return false
|
||||||
|
_current_path = save_path
|
||||||
|
_dirty = false
|
||||||
|
_update_document_status()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _request_document_action(action: PendingDocumentAction) -> void:
|
||||||
|
if _dirty:
|
||||||
|
_pending_document_action = action
|
||||||
|
match action:
|
||||||
|
PendingDocumentAction.LOAD:
|
||||||
|
_unsaved_dialog.dialog_text = "Save changes before loading another stickman?"
|
||||||
|
PendingDocumentAction.CLEAR:
|
||||||
|
_unsaved_dialog.dialog_text = "Save changes before starting over with a blank stickman?"
|
||||||
|
PendingDocumentAction.QUIT:
|
||||||
|
_unsaved_dialog.dialog_text = "Save changes before closing Stickman Studio?"
|
||||||
|
StageDialogStylerScript.popup(_unsaved_dialog)
|
||||||
|
return
|
||||||
|
_continue_document_action(action)
|
||||||
|
|
||||||
|
|
||||||
|
func _continue_document_action(action: PendingDocumentAction) -> void:
|
||||||
|
match action:
|
||||||
|
PendingDocumentAction.LOAD:
|
||||||
_load_dialog.popup_centered_ratio(0.6)
|
_load_dialog.popup_centered_ratio(0.6)
|
||||||
2: # Clear
|
PendingDocumentAction.CLEAR:
|
||||||
_clear_confirm.popup_centered()
|
StageDialogStylerScript.popup(_clear_confirm)
|
||||||
|
PendingDocumentAction.QUIT:
|
||||||
|
get_tree().quit()
|
||||||
|
_pending_document_action = PendingDocumentAction.NONE
|
||||||
|
|
||||||
|
|
||||||
|
func _on_unsaved_save_pressed() -> void:
|
||||||
|
if _pending_document_action == PendingDocumentAction.NONE:
|
||||||
|
return
|
||||||
|
if _current_path.is_empty():
|
||||||
|
_save_dialog.popup_centered_ratio(0.6)
|
||||||
|
return
|
||||||
|
if save_to_path(_current_path):
|
||||||
|
_resume_pending_document_action()
|
||||||
|
else:
|
||||||
|
_cancel_pending_document_action()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_unsaved_custom_action(action: StringName) -> void:
|
||||||
|
if action != &"discard":
|
||||||
|
return
|
||||||
|
_resume_pending_document_action()
|
||||||
|
|
||||||
|
|
||||||
|
func _resume_pending_document_action() -> void:
|
||||||
|
var action: PendingDocumentAction = _pending_document_action
|
||||||
|
_pending_document_action = PendingDocumentAction.NONE
|
||||||
|
if action == PendingDocumentAction.CLEAR:
|
||||||
|
_clear_all()
|
||||||
|
return
|
||||||
|
_continue_document_action(action)
|
||||||
|
|
||||||
|
|
||||||
|
func _cancel_pending_document_action() -> void:
|
||||||
|
_pending_document_action = PendingDocumentAction.NONE
|
||||||
|
|
||||||
|
|
||||||
|
func _on_stickman_name_changed(_value: String) -> void:
|
||||||
|
if not _applying_document:
|
||||||
|
_mark_dirty()
|
||||||
|
|
||||||
|
|
||||||
|
func _mark_dirty() -> void:
|
||||||
|
if _applying_document:
|
||||||
|
return
|
||||||
|
_dirty = true
|
||||||
|
_update_document_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_document_status() -> void:
|
||||||
|
if _status_save_status == null:
|
||||||
|
return
|
||||||
|
if _dirty:
|
||||||
|
_status_save_status.text = "Unsaved changes"
|
||||||
|
_status_save_status.add_theme_color_override("font_color", _editor_visual_theme.chrome_color(&"dirty", &"stickman"))
|
||||||
|
elif _current_path.is_empty():
|
||||||
|
_status_save_status.text = "Not saved yet"
|
||||||
|
_status_save_status.add_theme_color_override("font_color", _editor_visual_theme.chrome_color(&"text_muted", &"stickman"))
|
||||||
|
else:
|
||||||
|
_status_save_status.text = "Saved"
|
||||||
|
_status_save_status.add_theme_color_override("font_color", _editor_visual_theme.chrome_color(&"saved", &"stickman"))
|
||||||
|
|
||||||
|
|
||||||
func _on_edit_menu_id_pressed(id: int) -> void:
|
func _on_edit_menu_id_pressed(id: int) -> void:
|
||||||
match id:
|
match id:
|
||||||
0: # Configure Grid...
|
0: # Configure Grid...
|
||||||
_grid_size_spin_box.value = float(_grid_size)
|
_grid_size_spin_box.value = float(_grid_size)
|
||||||
_grid_config_dialog.popup_centered()
|
StageDialogStylerScript.popup(_grid_config_dialog)
|
||||||
1: # Snap to Grid (checkable)
|
1: # Snap to Grid (checkable)
|
||||||
_snap_enabled = not _snap_enabled
|
_snap_enabled = not _snap_enabled
|
||||||
if _edit_menu:
|
if _edit_menu:
|
||||||
@@ -308,7 +835,7 @@ func _update_guide_menu_item() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _guide_menu_label() -> String:
|
func _guide_menu_label() -> String:
|
||||||
return "Hide Pose Guide" if _show_guide else "Show Pose Guide"
|
return "👁️ Hide Pose Guide" if _show_guide else "👁️ Show Pose Guide"
|
||||||
|
|
||||||
|
|
||||||
func _on_grid_config_confirmed() -> void:
|
func _on_grid_config_confirmed() -> void:
|
||||||
@@ -320,19 +847,12 @@ func _on_grid_config_confirmed() -> void:
|
|||||||
|
|
||||||
func _on_save_file_selected(path: String) -> void:
|
func _on_save_file_selected(path: String) -> void:
|
||||||
path = ContentPathsScript.canonicalize(path)
|
path = ContentPathsScript.canonicalize(path)
|
||||||
if not path.ends_with(".stk"):
|
if path.get_extension().to_lower() != "stk":
|
||||||
path += ".stk"
|
path = path.get_basename() + ".stk"
|
||||||
|
if not save_to_path(path):
|
||||||
var data := _build_json_data()
|
_cancel_pending_document_action()
|
||||||
var json_text := JSON.stringify(data, "\t", false)
|
|
||||||
|
|
||||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
||||||
if file == null:
|
|
||||||
_show_error("Failed to save file: " + str(FileAccess.get_open_error()))
|
|
||||||
return
|
return
|
||||||
|
_resume_pending_document_action()
|
||||||
file.store_string(json_text)
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_load_file_selected(path: String) -> void:
|
func _on_load_file_selected(path: String) -> void:
|
||||||
@@ -358,6 +878,9 @@ func _on_load_file_selected(path: String) -> void:
|
|||||||
if not err.is_empty():
|
if not err.is_empty():
|
||||||
_show_error(err)
|
_show_error(err)
|
||||||
return
|
return
|
||||||
|
_current_path = path
|
||||||
|
_dirty = false
|
||||||
|
_update_document_status()
|
||||||
|
|
||||||
|
|
||||||
func _on_clear_confirmed() -> void:
|
func _on_clear_confirmed() -> void:
|
||||||
@@ -366,7 +889,7 @@ func _on_clear_confirmed() -> void:
|
|||||||
|
|
||||||
func _show_error(message: String) -> void:
|
func _show_error(message: String) -> void:
|
||||||
_error_dialog.dialog_text = message
|
_error_dialog.dialog_text = message
|
||||||
_error_dialog.popup_centered()
|
StageDialogStylerScript.popup(_error_dialog)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Signal handlers - body-part panels
|
# Signal handlers - body-part panels
|
||||||
@@ -379,19 +902,31 @@ func _on_body_part_shape_changed(_new_data: Array, part_name: String) -> void:
|
|||||||
_whole_preview.set_part_rotation(part_name, 0.0)
|
_whole_preview.set_part_rotation(part_name, 0.0)
|
||||||
_whole_preview.set_part_scale(part_name, Vector2(1.0, 1.0))
|
_whole_preview.set_part_scale(part_name, Vector2(1.0, 1.0))
|
||||||
_broadcast_clipboard_state()
|
_broadcast_clipboard_state()
|
||||||
|
_mark_dirty()
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
|
||||||
|
|
||||||
func _on_body_part_shape_selected(part_name: String) -> void:
|
func _on_body_part_shape_selected(part_name: String) -> void:
|
||||||
|
request_active_part(part_name)
|
||||||
for other_name: String in BODY_PART_NAMES:
|
for other_name: String in BODY_PART_NAMES:
|
||||||
if other_name != part_name:
|
if other_name != part_name:
|
||||||
var panel: BodyPartPanel = _body_part_panels.get(other_name) as BodyPartPanel
|
var panel: BodyPartPanel = _body_part_panels.get(other_name) as BodyPartPanel
|
||||||
if panel:
|
if panel:
|
||||||
panel.deselect()
|
panel.deselect()
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
|
||||||
|
|
||||||
func _on_preview_part_moved(_part_name: String, _new_position: Vector2) -> void:
|
func _on_preview_part_moved(_part_name: String, _new_position: Vector2) -> void:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _on_preview_part_transform_committed(_part_name: String, _position: Vector2, _rotation_deg: float, _scale: Vector2) -> void:
|
||||||
|
_mark_dirty()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_preview_part_order_changed(_part_name: String, _new_order: Array[String]) -> void:
|
||||||
|
_mark_dirty()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Data collection / JSON helpers
|
# Data collection / JSON helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -478,13 +1013,13 @@ func _apply_json_data(json: Variant) -> String:
|
|||||||
if version not in SUPPORTED_VERSIONS:
|
if version not in SUPPORTED_VERSIONS:
|
||||||
return "Unsupported file version: '%s' (expected '1.0', '1.1', '1.2', '1.3', '1.4', or '1.5')" % version
|
return "Unsupported file version: '%s' (expected '1.0', '1.1', '1.2', '1.3', '1.4', or '1.5')" % version
|
||||||
|
|
||||||
_stickman_name_edit.text = dict.get("stickman_name", "")
|
|
||||||
|
|
||||||
var body_parts: Variant = dict.get("body_parts")
|
var body_parts: Variant = dict.get("body_parts")
|
||||||
if not body_parts is Dictionary:
|
if not body_parts is Dictionary:
|
||||||
return "Invalid file format: 'body_parts' must be a dictionary."
|
return "Invalid file format: 'body_parts' must be a dictionary."
|
||||||
|
|
||||||
var body_dict: Dictionary = body_parts as Dictionary
|
var body_dict: Dictionary = body_parts as Dictionary
|
||||||
|
_applying_document = true
|
||||||
|
_stickman_name_edit.text = str(dict.get("stickman_name", ""))
|
||||||
|
|
||||||
_clear_all_panels()
|
_clear_all_panels()
|
||||||
|
|
||||||
@@ -546,6 +1081,9 @@ func _apply_json_data(json: Variant) -> String:
|
|||||||
_whole_preview.reset_view()
|
_whole_preview.reset_view()
|
||||||
_whole_preview.set_all_part_positions(positions)
|
_whole_preview.set_all_part_positions(positions)
|
||||||
_whole_preview.set_body_parts(_collect_all_shape_data())
|
_whole_preview.set_body_parts(_collect_all_shape_data())
|
||||||
|
_applying_document = false
|
||||||
|
_dirty = false
|
||||||
|
_update_document_status()
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -561,12 +1099,18 @@ func _clear_all_panels() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _clear_all() -> void:
|
func _clear_all() -> void:
|
||||||
|
_applying_document = true
|
||||||
_clear_all_panels()
|
_clear_all_panels()
|
||||||
_whole_preview.clear_all()
|
_whole_preview.clear_all()
|
||||||
_whole_preview.set_all_part_positions(DEFAULT_POSITIONS.duplicate())
|
_whole_preview.set_all_part_positions(DEFAULT_POSITIONS.duplicate())
|
||||||
_stickman_name_edit.text = ""
|
_stickman_name_edit.text = ""
|
||||||
_shape_clipboard.clear()
|
_shape_clipboard.clear()
|
||||||
_broadcast_clipboard_state()
|
_broadcast_clipboard_state()
|
||||||
|
_applying_document = false
|
||||||
|
_current_path = ""
|
||||||
|
_dirty = false
|
||||||
|
_update_document_status()
|
||||||
|
_refresh_workspace_controls()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Phase 5: Shape clipboard
|
# Phase 5: Shape clipboard
|
||||||
@@ -611,6 +1155,7 @@ func _on_color_selected(color: Color, _part_name: String) -> void:
|
|||||||
_recent_colors.pop_back()
|
_recent_colors.pop_back()
|
||||||
_save_settings()
|
_save_settings()
|
||||||
_broadcast_recent_colors()
|
_broadcast_recent_colors()
|
||||||
|
_mark_dirty()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Phase 3: Settings persistence
|
# Phase 3: Settings persistence
|
||||||
@@ -680,7 +1225,7 @@ func _broadcast_settings() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _snap_menu_label() -> String:
|
func _snap_menu_label() -> String:
|
||||||
return "[√] Snap to Grid" if _snap_enabled else "Snap to Grid"
|
return "🧲 [√] Snap to Grid" if _snap_enabled else "🧲 Snap to Grid"
|
||||||
|
|
||||||
|
|
||||||
func _update_snap_status_label() -> void:
|
func _update_snap_status_label() -> void:
|
||||||
|
|||||||
@@ -18,12 +18,15 @@ class_name WholeStickmanPreview
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
signal part_moved(part_name: String, new_position: Vector2)
|
signal part_moved(part_name: String, new_position: Vector2)
|
||||||
|
signal part_transform_committed(part_name: String, position: Vector2, rotation_deg: float, scale: Vector2)
|
||||||
|
signal part_order_changed(part_name: String, new_order: Array[String])
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const GRID_COLOR := Color(1.0, 1.0, 1.0, 0.15)
|
const GRID_COLOR := Color(1.0, 1.0, 1.0, 0.15)
|
||||||
|
const EditorContextMenuScript := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||||
const MIN_ZOOM: float = 0.3
|
const MIN_ZOOM: float = 0.3
|
||||||
const MAX_ZOOM: float = 3.0
|
const MAX_ZOOM: float = 3.0
|
||||||
const ZOOM_STEP: float = 1.10
|
const ZOOM_STEP: float = 1.10
|
||||||
@@ -140,13 +143,21 @@ var _pan_start: Vector2 = Vector2.ZERO
|
|||||||
|
|
||||||
# Phase 5: z-ordering
|
# Phase 5: z-ordering
|
||||||
var _part_order: Array[String] = []
|
var _part_order: Array[String] = []
|
||||||
var _context_menu: PopupMenu
|
var _context_menu: EditorContextMenu
|
||||||
|
|
||||||
# Phase 6: selection gizmos rendered on top
|
# Phase 6: selection gizmos rendered on top
|
||||||
var _selected_gizmo_bounds: Rect2 = Rect2()
|
var _selected_gizmo_bounds: Rect2 = Rect2()
|
||||||
|
|
||||||
# Phase 7: pose silhouette guide (default ON per user decision)
|
# Phase 7: pose silhouette guide (default ON per user decision)
|
||||||
var _show_guide: bool = true
|
var _show_guide: bool = true
|
||||||
|
var _grid_color: Color = GRID_COLOR
|
||||||
|
var _selection_core_color: Color = Color.WHITE
|
||||||
|
var _selection_halo_color: Color = Color(0.0, 0.0, 0.0, 0.85)
|
||||||
|
var _preview_color: Color = Color(0.13, 0.78, 1.0, 0.4)
|
||||||
|
var _interaction_start_part: String = ""
|
||||||
|
var _interaction_start_position: Vector2 = Vector2.ZERO
|
||||||
|
var _interaction_start_rotation: float = 0.0
|
||||||
|
var _interaction_start_scale: Vector2 = Vector2.ONE
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public API
|
# Public API
|
||||||
@@ -271,6 +282,19 @@ func set_show_guide(enabled: bool) -> void:
|
|||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo: Color, preview_color: Color = Color(0.13, 0.78, 1.0, 0.4)) -> void:
|
||||||
|
_grid_color = grid_color
|
||||||
|
_selection_core_color = selection_core
|
||||||
|
_selection_halo_color = selection_halo
|
||||||
|
_preview_color = preview_color
|
||||||
|
preview_area.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func set_context_menu_visual_theme(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||||
|
if _context_menu != null:
|
||||||
|
_context_menu.configure(visual_theme, editor)
|
||||||
|
|
||||||
|
|
||||||
## Returns the preview-space position of a guide joint (a master-space
|
## Returns the preview-space position of a guide joint (a master-space
|
||||||
## GUIDE_JOINTS entry mapped through _guide_to_preview). Guarded against
|
## GUIDE_JOINTS entry mapped through _guide_to_preview). Guarded against
|
||||||
## unknown joint names.
|
## unknown joint names.
|
||||||
@@ -316,7 +340,7 @@ func _ready() -> void:
|
|||||||
if not preview_area.gui_input.is_connected(_on_preview_gui_input):
|
if not preview_area.gui_input.is_connected(_on_preview_gui_input):
|
||||||
preview_area.gui_input.connect(_on_preview_gui_input)
|
preview_area.gui_input.connect(_on_preview_gui_input)
|
||||||
|
|
||||||
_context_menu = PopupMenu.new()
|
_context_menu = EditorContextMenuScript.new() as EditorContextMenu
|
||||||
_context_menu.name = "PreviewContextMenu"
|
_context_menu.name = "PreviewContextMenu"
|
||||||
add_child(_context_menu)
|
add_child(_context_menu)
|
||||||
_context_menu.id_pressed.connect(_on_context_menu_id_pressed)
|
_context_menu.id_pressed.connect(_on_context_menu_id_pressed)
|
||||||
@@ -399,25 +423,35 @@ func _on_preview_draw() -> void:
|
|||||||
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
|
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
|
||||||
var bounds: Variant = _part_bounds.get(_dragging_part)
|
var bounds: Variant = _part_bounds.get(_dragging_part)
|
||||||
if bounds is Rect2:
|
if bounds is Rect2:
|
||||||
preview_area.draw_rect(bounds as Rect2, Color(1.0, 0.8, 0.0, 0.35), false, 1.5)
|
preview_area.draw_rect(bounds as Rect2, _preview_color, false, 1.5 / _zoom)
|
||||||
|
|
||||||
# Draw selection gizmos on top of all parts
|
# Draw selection gizmos on top of all parts
|
||||||
if _selected_gizmo_bounds.has_area():
|
if _selected_gizmo_bounds.has_area():
|
||||||
var b: Rect2 = _selected_gizmo_bounds
|
var b: Rect2 = _selected_gizmo_bounds
|
||||||
preview_area.draw_rect(b, Color.WHITE, false, 1.5 / _zoom)
|
preview_area.draw_rect(b, _selection_halo_color, false, 4.0 / _zoom)
|
||||||
|
preview_area.draw_rect(b, _selection_core_color, false, 1.5 / _zoom)
|
||||||
var rot_center := Vector2(b.position.x + b.size.x * 0.5, b.end.y + ROTATION_CIRCLE_OFFSET)
|
var rot_center := Vector2(b.position.x + b.size.x * 0.5, b.end.y + ROTATION_CIRCLE_OFFSET)
|
||||||
preview_area.draw_circle(rot_center, ROTATION_CIRCLE_RADIUS / _zoom, Color.WHITE)
|
preview_area.draw_circle(rot_center, (ROTATION_CIRCLE_RADIUS + 2.0) / _zoom, _selection_halo_color)
|
||||||
|
preview_area.draw_circle(rot_center, ROTATION_CIRCLE_RADIUS / _zoom, _selection_core_color)
|
||||||
var cross_half := SCALE_CROSS_SIZE / _zoom
|
var cross_half := SCALE_CROSS_SIZE / _zoom
|
||||||
for i: int in range(4):
|
for i: int in range(4):
|
||||||
var corner := _get_corner_position(b, i)
|
var corner := _get_corner_position(b, i)
|
||||||
preview_area.draw_line(
|
preview_area.draw_line(
|
||||||
corner + Vector2(-cross_half, 0),
|
corner + Vector2(-cross_half, 0),
|
||||||
corner + Vector2(cross_half, 0),
|
corner + Vector2(cross_half, 0),
|
||||||
Color.WHITE, 1.5 / _zoom)
|
_selection_halo_color, 4.0 / _zoom)
|
||||||
preview_area.draw_line(
|
preview_area.draw_line(
|
||||||
corner + Vector2(0, -cross_half),
|
corner + Vector2(0, -cross_half),
|
||||||
corner + Vector2(0, cross_half),
|
corner + Vector2(0, cross_half),
|
||||||
Color.WHITE, 1.5 / _zoom)
|
_selection_halo_color, 4.0 / _zoom)
|
||||||
|
preview_area.draw_line(
|
||||||
|
corner + Vector2(-cross_half, 0),
|
||||||
|
corner + Vector2(cross_half, 0),
|
||||||
|
_selection_core_color, 1.5 / _zoom)
|
||||||
|
preview_area.draw_line(
|
||||||
|
corner + Vector2(0, -cross_half),
|
||||||
|
corner + Vector2(0, cross_half),
|
||||||
|
_selection_core_color, 1.5 / _zoom)
|
||||||
|
|
||||||
|
|
||||||
func _draw_grid() -> void:
|
func _draw_grid() -> void:
|
||||||
@@ -436,12 +470,12 @@ func _draw_grid() -> void:
|
|||||||
|
|
||||||
var x: float = start_x
|
var x: float = start_x
|
||||||
while x <= world_end.x:
|
while x <= world_end.x:
|
||||||
preview_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), GRID_COLOR, line_width)
|
preview_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), _grid_color, line_width)
|
||||||
x += gs
|
x += gs
|
||||||
|
|
||||||
var y: float = start_y
|
var y: float = start_y
|
||||||
while y <= world_end.y:
|
while y <= world_end.y:
|
||||||
preview_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), GRID_COLOR, line_width)
|
preview_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), _grid_color, line_width)
|
||||||
y += gs
|
y += gs
|
||||||
|
|
||||||
|
|
||||||
@@ -688,6 +722,7 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
|||||||
var rot_center := Vector2(bounds.position.x + bounds.size.x * 0.5, bounds.end.y + ROTATION_CIRCLE_OFFSET)
|
var rot_center := Vector2(bounds.position.x + bounds.size.x * 0.5, bounds.end.y + ROTATION_CIRCLE_OFFSET)
|
||||||
if world_pos.distance_to(rot_center) <= ROTATION_CIRCLE_HIT_RADIUS / _zoom:
|
if world_pos.distance_to(rot_center) <= ROTATION_CIRCLE_HIT_RADIUS / _zoom:
|
||||||
_interaction = Interaction.ROTATE
|
_interaction = Interaction.ROTATE
|
||||||
|
_capture_transform_start(_selected_part)
|
||||||
_rotate_start_rotation = _part_rotations[_selected_part]
|
_rotate_start_rotation = _part_rotations[_selected_part]
|
||||||
_rotate_total_delta = 0.0
|
_rotate_total_delta = 0.0
|
||||||
var bounds_center := bounds.position + bounds.size * 0.5
|
var bounds_center := bounds.position + bounds.size * 0.5
|
||||||
@@ -700,6 +735,7 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
|||||||
var corner := _get_corner_position(bounds, i)
|
var corner := _get_corner_position(bounds, i)
|
||||||
if world_pos.distance_to(corner) <= SCALE_CROSS_HIT_RADIUS / _zoom:
|
if world_pos.distance_to(corner) <= SCALE_CROSS_HIT_RADIUS / _zoom:
|
||||||
_interaction = Interaction.SCALE
|
_interaction = Interaction.SCALE
|
||||||
|
_capture_transform_start(_selected_part)
|
||||||
_scale_corner = i
|
_scale_corner = i
|
||||||
_scale_anchor = _get_opposite_corner(bounds, i)
|
_scale_anchor = _get_opposite_corner(bounds, i)
|
||||||
_scale_original_size = _compute_original_bounds_size(_selected_part)
|
_scale_original_size = _compute_original_bounds_size(_selected_part)
|
||||||
@@ -717,6 +753,7 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
|||||||
if rect.grow(8.0).has_point(world_pos):
|
if rect.grow(8.0).has_point(world_pos):
|
||||||
_selected_part = part_name
|
_selected_part = part_name
|
||||||
_interaction = Interaction.TRANSLATE
|
_interaction = Interaction.TRANSLATE
|
||||||
|
_capture_transform_start(_selected_part)
|
||||||
_dragging_part = part_name
|
_dragging_part = part_name
|
||||||
var pos: Vector2 = _part_positions.get(part_name, Vector2.ZERO)
|
var pos: Vector2 = _part_positions.get(part_name, Vector2.ZERO)
|
||||||
_drag_offset = world_pos - pos
|
_drag_offset = world_pos - pos
|
||||||
@@ -731,13 +768,40 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
|||||||
|
|
||||||
func _end_interaction() -> void:
|
func _end_interaction() -> void:
|
||||||
if _interaction != Interaction.NONE:
|
if _interaction != Interaction.NONE:
|
||||||
|
var changed_part: String = _interaction_start_part
|
||||||
|
var changed: bool = changed_part != "" and (
|
||||||
|
_part_positions.get(changed_part, Vector2.ZERO) != _interaction_start_position
|
||||||
|
or not is_equal_approx(float(_part_rotations.get(changed_part, 0.0)), _interaction_start_rotation)
|
||||||
|
or Vector2(_part_scales.get(changed_part, Vector2.ONE)) != _interaction_start_scale
|
||||||
|
)
|
||||||
if _interaction == Interaction.TRANSLATE and not _dragging_part.is_empty():
|
if _interaction == Interaction.TRANSLATE and not _dragging_part.is_empty():
|
||||||
_dragging_part = ""
|
_dragging_part = ""
|
||||||
_drag_offset = Vector2.ZERO
|
_drag_offset = Vector2.ZERO
|
||||||
_interaction = Interaction.NONE
|
_interaction = Interaction.NONE
|
||||||
|
if changed:
|
||||||
|
_emit_transform_committed(changed_part)
|
||||||
|
_interaction_start_part = ""
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _capture_transform_start(part_name: String) -> void:
|
||||||
|
_interaction_start_part = part_name
|
||||||
|
_interaction_start_position = _part_positions.get(part_name, Vector2.ZERO)
|
||||||
|
_interaction_start_rotation = float(_part_rotations.get(part_name, 0.0))
|
||||||
|
_interaction_start_scale = _part_scales.get(part_name, Vector2.ONE)
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_transform_committed(part_name: String) -> void:
|
||||||
|
if part_name.is_empty():
|
||||||
|
return
|
||||||
|
part_transform_committed.emit(
|
||||||
|
part_name,
|
||||||
|
_part_positions.get(part_name, Vector2.ZERO),
|
||||||
|
float(_part_rotations.get(part_name, 0.0)),
|
||||||
|
_part_scales.get(part_name, Vector2.ONE)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _handle_translate_drag(mm: InputEventMouseMotion) -> void:
|
func _handle_translate_drag(mm: InputEventMouseMotion) -> void:
|
||||||
if _dragging_part.is_empty():
|
if _dragging_part.is_empty():
|
||||||
return
|
return
|
||||||
@@ -966,12 +1030,14 @@ func _handle_right_click(mb: InputEventMouseButton) -> void:
|
|||||||
if rect.has_point(world_pos):
|
if rect.has_point(world_pos):
|
||||||
_selected_part = part_name
|
_selected_part = part_name
|
||||||
_context_menu.clear()
|
_context_menu.clear()
|
||||||
_context_menu.add_item("Send Back", 0)
|
|
||||||
_context_menu.add_item("Bring Forward", 1)
|
_context_menu.add_item("Bring Forward", 1)
|
||||||
|
_context_menu.add_item("Send Back", 0)
|
||||||
_context_menu.add_separator()
|
_context_menu.add_separator()
|
||||||
_context_menu.add_item("Mirror X", 2)
|
_context_menu.add_item("Flip Left-Right", 2)
|
||||||
_context_menu.add_item("Mirror Y", 3)
|
_context_menu.add_item("Flip Up-Down", 3)
|
||||||
_context_menu.popup_on_parent(Rect2(mb.global_position, Vector2.ONE))
|
var screen_position := preview_area.global_position + _pan_offset + rect.position * _zoom
|
||||||
|
var target_rect := Rect2(screen_position, rect.size * _zoom).grow(4.0)
|
||||||
|
_context_menu.popup_at(mb.global_position, target_rect)
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -996,6 +1062,7 @@ func _send_back_part() -> void:
|
|||||||
_part_order.remove_at(idx)
|
_part_order.remove_at(idx)
|
||||||
_part_order.insert(idx - 1, _selected_part)
|
_part_order.insert(idx - 1, _selected_part)
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
|
part_order_changed.emit(_selected_part, _part_order.duplicate())
|
||||||
|
|
||||||
|
|
||||||
func _bring_forward_part() -> void:
|
func _bring_forward_part() -> void:
|
||||||
@@ -1007,6 +1074,7 @@ func _bring_forward_part() -> void:
|
|||||||
_part_order.remove_at(idx)
|
_part_order.remove_at(idx)
|
||||||
_part_order.insert(idx + 1, _selected_part)
|
_part_order.insert(idx + 1, _selected_part)
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
|
part_order_changed.emit(_selected_part, _part_order.duplicate())
|
||||||
|
|
||||||
|
|
||||||
func _mirror_part_x() -> void:
|
func _mirror_part_x() -> void:
|
||||||
@@ -1016,6 +1084,7 @@ func _mirror_part_x() -> void:
|
|||||||
_part_scales[_selected_part] = Vector2(-scl.x, scl.y)
|
_part_scales[_selected_part] = Vector2(-scl.x, scl.y)
|
||||||
_rebuild_bounds()
|
_rebuild_bounds()
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
|
_emit_transform_committed(_selected_part)
|
||||||
|
|
||||||
|
|
||||||
func _mirror_part_y() -> void:
|
func _mirror_part_y() -> void:
|
||||||
@@ -1025,3 +1094,4 @@ func _mirror_part_y() -> void:
|
|||||||
_part_scales[_selected_part] = Vector2(scl.x, -scl.y)
|
_part_scales[_selected_part] = Vector2(scl.x, -scl.y)
|
||||||
_rebuild_bounds()
|
_rebuild_bounds()
|
||||||
preview_area.queue_redraw()
|
preview_area.queue_redraw()
|
||||||
|
_emit_transform_committed(_selected_part)
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
class_name EditorContextMenu
|
||||||
|
extends PopupPanel
|
||||||
|
## Shared Studio List popup used by right-click menus in both standalone editors.
|
||||||
|
|
||||||
|
signal id_pressed(id: int)
|
||||||
|
|
||||||
|
enum ItemRole { NORMAL, DESTRUCTIVE }
|
||||||
|
|
||||||
|
const DEFAULT_EDITOR := &"prop"
|
||||||
|
const DEFAULT_MIN_WIDTH := 208
|
||||||
|
const DEFAULT_ROW_HEIGHT := 48
|
||||||
|
const DEFAULT_GROUP_GAP := 8
|
||||||
|
const SHELL_INSET := 8
|
||||||
|
const ROW_RADIUS := 6
|
||||||
|
const TARGET_GAP := 12.0
|
||||||
|
|
||||||
|
var _visual_theme: EditorVisualTheme
|
||||||
|
var _editor: StringName = DEFAULT_EDITOR
|
||||||
|
var _font_size_override: int = -1
|
||||||
|
var _shell: MarginContainer
|
||||||
|
var _scroll: ScrollContainer
|
||||||
|
var _items: VBoxContainer
|
||||||
|
var _row_buttons: Array[Button] = []
|
||||||
|
var _row_ids: Dictionary = {}
|
||||||
|
var _keyboard_navigation_active: bool = false
|
||||||
|
var _focused_row_index: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_contents()
|
||||||
|
if not about_to_popup.is_connected(_reset_keyboard_navigation):
|
||||||
|
about_to_popup.connect(_reset_keyboard_navigation)
|
||||||
|
if not popup_hide.is_connected(_reset_keyboard_navigation):
|
||||||
|
popup_hide.connect(_reset_keyboard_navigation)
|
||||||
|
if _visual_theme != null:
|
||||||
|
_apply_visual_style()
|
||||||
|
|
||||||
|
|
||||||
|
func configure(visual_theme: EditorVisualTheme, editor: StringName) -> void:
|
||||||
|
_visual_theme = visual_theme
|
||||||
|
_editor = editor
|
||||||
|
if is_node_ready():
|
||||||
|
_apply_visual_style()
|
||||||
|
|
||||||
|
|
||||||
|
func set_font_size(size: int) -> void:
|
||||||
|
_font_size_override = maxi(1, size)
|
||||||
|
if is_node_ready():
|
||||||
|
_apply_visual_style()
|
||||||
|
|
||||||
|
|
||||||
|
func clear() -> void:
|
||||||
|
_reset_keyboard_navigation()
|
||||||
|
_row_buttons.clear()
|
||||||
|
_row_ids.clear()
|
||||||
|
if _items == null:
|
||||||
|
return
|
||||||
|
for child: Node in _items.get_children():
|
||||||
|
_items.remove_child(child)
|
||||||
|
child.free()
|
||||||
|
|
||||||
|
|
||||||
|
func add_item(label: String, id: int, role: ItemRole = ItemRole.NORMAL) -> void:
|
||||||
|
if _items == null:
|
||||||
|
_build_contents()
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = label
|
||||||
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.custom_minimum_size = Vector2(_menu_min_width(), _menu_row_height())
|
||||||
|
button.focus_mode = Control.FOCUS_NONE
|
||||||
|
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
button.add_theme_font_size_override("font_size", _font_size())
|
||||||
|
button.add_theme_color_override("font_color", _text_color(role))
|
||||||
|
button.add_theme_color_override("font_hover_color", _text_color(role))
|
||||||
|
button.add_theme_color_override("font_pressed_color", _text_color(role))
|
||||||
|
button.add_theme_color_override("font_focus_color", _text_color(role))
|
||||||
|
button.add_theme_stylebox_override("normal", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||||
|
button.add_theme_stylebox_override("hover", _make_hover_box())
|
||||||
|
button.add_theme_stylebox_override("pressed", _make_hover_box(true))
|
||||||
|
button.add_theme_stylebox_override("focus", _make_focus_box())
|
||||||
|
button.add_theme_stylebox_override("disabled", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||||
|
button.set_meta("context_menu_id", id)
|
||||||
|
button.set_meta("context_menu_role", role)
|
||||||
|
button.pressed.connect(_on_row_pressed.bind(button))
|
||||||
|
button.mouse_entered.connect(_on_row_mouse_entered.bind(button))
|
||||||
|
_items.add_child(button)
|
||||||
|
_row_buttons.append(button)
|
||||||
|
_row_ids[id] = button
|
||||||
|
|
||||||
|
|
||||||
|
func add_destructive_item(label: String, id: int) -> void:
|
||||||
|
add_item(label, id, ItemRole.DESTRUCTIVE)
|
||||||
|
|
||||||
|
|
||||||
|
func add_separator() -> void:
|
||||||
|
if _items == null:
|
||||||
|
_build_contents()
|
||||||
|
var spacer := MarginContainer.new()
|
||||||
|
spacer.custom_minimum_size.y = float(DEFAULT_GROUP_GAP + 1)
|
||||||
|
spacer.add_theme_constant_override("margin_top", int(DEFAULT_GROUP_GAP / 2))
|
||||||
|
spacer.add_theme_constant_override("margin_bottom", int(DEFAULT_GROUP_GAP / 2))
|
||||||
|
var line := ColorRect.new()
|
||||||
|
line.color = _theme_color(&"border", Color(1, 1, 1, 0.2))
|
||||||
|
line.custom_minimum_size.y = 1.0
|
||||||
|
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
spacer.add_child(line)
|
||||||
|
_items.add_child(spacer)
|
||||||
|
|
||||||
|
|
||||||
|
func popup_at(global_position: Vector2, avoid_rect: Rect2 = Rect2()) -> void:
|
||||||
|
if _items == null:
|
||||||
|
_build_contents()
|
||||||
|
var parent_viewport: Viewport = get_parent().get_viewport() if get_parent() != null else get_viewport()
|
||||||
|
var safe_rect: Rect2 = parent_viewport.get_visible_rect()
|
||||||
|
var margin := 8.0
|
||||||
|
var popup_size := _items.get_combined_minimum_size() + Vector2(SHELL_INSET * 2, SHELL_INSET * 2)
|
||||||
|
var border_width := float(_theme_px(&"normal_border", 1))
|
||||||
|
popup_size += Vector2(border_width * 2.0, border_width * 2.0)
|
||||||
|
popup_size.x = maxf(popup_size.x, float(_menu_min_width() + SHELL_INSET * 2))
|
||||||
|
var max_popup_size := Vector2(maxf(1.0, safe_rect.size.x - margin * 2.0), maxf(1.0, safe_rect.size.y - margin * 2.0))
|
||||||
|
popup_size = popup_size.min(max_popup_size)
|
||||||
|
if _scroll != null:
|
||||||
|
var content_height: float = _items.get_combined_minimum_size().y
|
||||||
|
var available_content_height: float = max_popup_size.y - float(SHELL_INSET * 2) - border_width * 2.0
|
||||||
|
_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO if content_height > available_content_height else ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
size = Vector2i(popup_size.ceil())
|
||||||
|
if _scroll != null:
|
||||||
|
_scroll.scroll_vertical = 0
|
||||||
|
var max_x := maxf(safe_rect.position.x + margin, safe_rect.end.x - popup_size.x - margin)
|
||||||
|
var max_y := maxf(safe_rect.position.y + margin, safe_rect.end.y - popup_size.y - margin)
|
||||||
|
var popup_origin := _choose_popup_origin(
|
||||||
|
global_position,
|
||||||
|
avoid_rect,
|
||||||
|
popup_size,
|
||||||
|
safe_rect,
|
||||||
|
margin,
|
||||||
|
max_x,
|
||||||
|
max_y
|
||||||
|
)
|
||||||
|
var popup_position := Vector2i(popup_origin.round())
|
||||||
|
popup_on_parent(Rect2i(popup_position, Vector2i.ONE))
|
||||||
|
# PopupPanel sizes itself from the child layout on popup. Reapply the measured
|
||||||
|
# target size after opening so the scroll region can honor the 48 px rows.
|
||||||
|
size = Vector2i(popup_size.ceil())
|
||||||
|
var actual_size := Vector2(size)
|
||||||
|
if popup_position.x + actual_size.x > safe_rect.end.x - margin:
|
||||||
|
popup_origin.x = global_position.x - actual_size.x - margin
|
||||||
|
if popup_position.y + actual_size.y > safe_rect.end.y - margin:
|
||||||
|
popup_origin.y = global_position.y - actual_size.y - margin
|
||||||
|
var actual_max_x := maxf(safe_rect.position.x + margin, safe_rect.end.x - actual_size.x - margin)
|
||||||
|
var actual_max_y := maxf(safe_rect.position.y + margin, safe_rect.end.y - actual_size.y - margin)
|
||||||
|
popup_origin.x = clampf(popup_origin.x, safe_rect.position.x + margin, actual_max_x)
|
||||||
|
popup_origin.y = clampf(popup_origin.y, safe_rect.position.y + margin, actual_max_y)
|
||||||
|
position = Vector2i(popup_origin.round())
|
||||||
|
|
||||||
|
|
||||||
|
func _choose_popup_origin(
|
||||||
|
global_position: Vector2,
|
||||||
|
avoid_rect: Rect2,
|
||||||
|
popup_size: Vector2,
|
||||||
|
safe_rect: Rect2,
|
||||||
|
margin: float,
|
||||||
|
max_x: float,
|
||||||
|
max_y: float
|
||||||
|
) -> Vector2:
|
||||||
|
var low_x: float = safe_rect.position.x + margin
|
||||||
|
var low_y: float = safe_rect.position.y + margin
|
||||||
|
var cursor_fallback := Vector2(
|
||||||
|
clampf(global_position.x + margin, low_x, max_x),
|
||||||
|
clampf(global_position.y + margin, low_y, max_y)
|
||||||
|
)
|
||||||
|
if not avoid_rect.has_area():
|
||||||
|
return cursor_fallback
|
||||||
|
|
||||||
|
var aligned_y := clampf(global_position.y - 20.0, low_y, max_y)
|
||||||
|
var aligned_x := clampf(global_position.x + margin, low_x, max_x)
|
||||||
|
var candidates: Array[Vector2] = [
|
||||||
|
Vector2(avoid_rect.end.x + TARGET_GAP, aligned_y),
|
||||||
|
Vector2(avoid_rect.position.x - popup_size.x - TARGET_GAP, aligned_y),
|
||||||
|
Vector2(aligned_x, avoid_rect.end.y + TARGET_GAP),
|
||||||
|
Vector2(aligned_x, avoid_rect.position.y - popup_size.y - TARGET_GAP),
|
||||||
|
cursor_fallback,
|
||||||
|
]
|
||||||
|
for candidate: Vector2 in candidates:
|
||||||
|
if candidate.x < low_x or candidate.y < low_y or candidate.x > max_x or candidate.y > max_y:
|
||||||
|
continue
|
||||||
|
var candidate_rect := Rect2(candidate, popup_size)
|
||||||
|
if not candidate_rect.intersects(avoid_rect):
|
||||||
|
return candidate
|
||||||
|
return cursor_fallback
|
||||||
|
|
||||||
|
|
||||||
|
func get_row_button(id: int) -> Button:
|
||||||
|
return _row_ids.get(id) as Button
|
||||||
|
|
||||||
|
|
||||||
|
func _build_contents() -> void:
|
||||||
|
if _items != null:
|
||||||
|
return
|
||||||
|
_shell = MarginContainer.new()
|
||||||
|
_shell.name = "MenuShell"
|
||||||
|
_shell.add_theme_constant_override("margin_left", SHELL_INSET)
|
||||||
|
_shell.add_theme_constant_override("margin_top", SHELL_INSET)
|
||||||
|
_shell.add_theme_constant_override("margin_right", SHELL_INSET)
|
||||||
|
_shell.add_theme_constant_override("margin_bottom", SHELL_INSET)
|
||||||
|
_shell.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_shell.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
add_child(_shell)
|
||||||
|
_scroll = ScrollContainer.new()
|
||||||
|
_scroll.name = "MenuScroll"
|
||||||
|
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
_shell.add_child(_scroll)
|
||||||
|
_items = VBoxContainer.new()
|
||||||
|
_items.name = "MenuItems"
|
||||||
|
_items.add_theme_constant_override("separation", 0)
|
||||||
|
_items.custom_minimum_size.x = float(_menu_min_width())
|
||||||
|
_scroll.add_child(_items)
|
||||||
|
_apply_visual_style()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_visual_style() -> void:
|
||||||
|
if _visual_theme == null:
|
||||||
|
return
|
||||||
|
var surface := _theme_color(&"surface", Color("#252B38"))
|
||||||
|
var border := _theme_color(&"border", Color("#FFFFFF33"))
|
||||||
|
var radius := _theme_px(&"panel", 10)
|
||||||
|
var panel := StyleBoxFlat.new()
|
||||||
|
panel.bg_color = surface
|
||||||
|
panel.border_color = border
|
||||||
|
panel.set_border_width_all(_theme_px(&"normal_border", 1))
|
||||||
|
panel.set_corner_radius_all(radius)
|
||||||
|
panel.shadow_color = Color(0.0, 0.0, 0.0, 0.28)
|
||||||
|
panel.shadow_size = 8
|
||||||
|
panel.shadow_offset = Vector2(0.0, 3.0)
|
||||||
|
add_theme_stylebox_override("panel", panel)
|
||||||
|
if _shell != null:
|
||||||
|
_shell.add_theme_constant_override("margin_left", SHELL_INSET)
|
||||||
|
_shell.add_theme_constant_override("margin_top", SHELL_INSET)
|
||||||
|
_shell.add_theme_constant_override("margin_right", SHELL_INSET)
|
||||||
|
_shell.add_theme_constant_override("margin_bottom", SHELL_INSET)
|
||||||
|
if _items != null:
|
||||||
|
_items.custom_minimum_size.x = float(_menu_min_width())
|
||||||
|
for button: Button in _row_buttons:
|
||||||
|
if not is_instance_valid(button):
|
||||||
|
continue
|
||||||
|
button.custom_minimum_size = Vector2(_menu_min_width(), _menu_row_height())
|
||||||
|
button.add_theme_font_size_override("font_size", _font_size())
|
||||||
|
var role := int(button.get_meta("context_menu_role", ItemRole.NORMAL))
|
||||||
|
var item_role: ItemRole = ItemRole.DESTRUCTIVE if role == ItemRole.DESTRUCTIVE else ItemRole.NORMAL
|
||||||
|
button.add_theme_color_override("font_color", _text_color(item_role))
|
||||||
|
button.add_theme_color_override("font_hover_color", _text_color(item_role))
|
||||||
|
button.add_theme_color_override("font_pressed_color", _text_color(item_role))
|
||||||
|
button.add_theme_color_override("font_focus_color", _text_color(item_role))
|
||||||
|
button.add_theme_stylebox_override("normal", _make_row_box(Color(0.0, 0.0, 0.0, 0.0), Color(0, 0, 0, 0), 0))
|
||||||
|
button.add_theme_stylebox_override("hover", _make_hover_box())
|
||||||
|
button.add_theme_stylebox_override("pressed", _make_hover_box(true))
|
||||||
|
button.add_theme_stylebox_override("focus", _make_focus_box())
|
||||||
|
|
||||||
|
|
||||||
|
func _make_row_box(fill: Color, border: Color, border_width: int) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = fill
|
||||||
|
style.border_color = border
|
||||||
|
style.set_border_width_all(border_width)
|
||||||
|
style.set_corner_radius_all(ROW_RADIUS)
|
||||||
|
style.content_margin_left = 12
|
||||||
|
style.content_margin_right = 12
|
||||||
|
style.content_margin_top = 4
|
||||||
|
style.content_margin_bottom = 4
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _make_hover_box(pressed: bool = false) -> StyleBoxFlat:
|
||||||
|
var hover_color := _theme_color(&"surface_hover", Color("#30394A"))
|
||||||
|
if pressed:
|
||||||
|
hover_color = hover_color.darkened(0.1)
|
||||||
|
var style := _make_row_box(hover_color, _theme_color(&"edit", Color("#22C6FF")), 0)
|
||||||
|
style.border_width_left = 3
|
||||||
|
style.content_margin_left = 9
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _make_focus_box() -> StyleBoxFlat:
|
||||||
|
var style := _make_row_box(Color(0.0, 0.0, 0.0, 0.0), _theme_color(&"focus", Color("#8BD9FF")), 2)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _on_row_pressed(button: Button) -> void:
|
||||||
|
if not is_instance_valid(button):
|
||||||
|
return
|
||||||
|
var id: int = int(button.get_meta("context_menu_id", -1))
|
||||||
|
id_pressed.emit(id)
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not visible or not event is InputEventKey:
|
||||||
|
return
|
||||||
|
var key_event := event as InputEventKey
|
||||||
|
if not key_event.pressed:
|
||||||
|
return
|
||||||
|
if key_event.keycode == KEY_DOWN:
|
||||||
|
_move_keyboard_focus(1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif key_event.keycode == KEY_UP:
|
||||||
|
_move_keyboard_focus(-1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _move_keyboard_focus(direction: int) -> void:
|
||||||
|
if _row_buttons.is_empty():
|
||||||
|
return
|
||||||
|
var candidate_index: int
|
||||||
|
if not _keyboard_navigation_active:
|
||||||
|
candidate_index = 0 if direction > 0 else _row_buttons.size() - 1
|
||||||
|
else:
|
||||||
|
candidate_index = _focused_row_index + direction
|
||||||
|
for _attempt: int in range(_row_buttons.size()):
|
||||||
|
candidate_index = posmod(candidate_index, _row_buttons.size())
|
||||||
|
var button: Button = _row_buttons[candidate_index]
|
||||||
|
if is_instance_valid(button) and button.visible and not button.disabled:
|
||||||
|
_enable_keyboard_navigation()
|
||||||
|
_focused_row_index = candidate_index
|
||||||
|
button.grab_focus()
|
||||||
|
return
|
||||||
|
candidate_index += direction
|
||||||
|
|
||||||
|
|
||||||
|
func _enable_keyboard_navigation() -> void:
|
||||||
|
_keyboard_navigation_active = true
|
||||||
|
for button: Button in _row_buttons:
|
||||||
|
if is_instance_valid(button):
|
||||||
|
button.focus_mode = Control.FOCUS_ALL
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_keyboard_navigation() -> void:
|
||||||
|
_keyboard_navigation_active = false
|
||||||
|
_focused_row_index = -1
|
||||||
|
if not is_node_ready():
|
||||||
|
return
|
||||||
|
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||||
|
if focus_owner != null and _row_buttons.has(focus_owner):
|
||||||
|
focus_owner.release_focus()
|
||||||
|
for button: Button in _row_buttons:
|
||||||
|
if is_instance_valid(button):
|
||||||
|
button.focus_mode = Control.FOCUS_NONE
|
||||||
|
|
||||||
|
|
||||||
|
func _on_row_mouse_entered(_button: Button) -> void:
|
||||||
|
_reset_keyboard_navigation()
|
||||||
|
|
||||||
|
|
||||||
|
func _theme_color(role: StringName, fallback: Color) -> Color:
|
||||||
|
if _visual_theme == null:
|
||||||
|
return fallback
|
||||||
|
return _visual_theme.chrome_color(role, _editor)
|
||||||
|
|
||||||
|
|
||||||
|
func _theme_px(role: StringName, fallback: int) -> int:
|
||||||
|
if _visual_theme == null:
|
||||||
|
return fallback
|
||||||
|
return _visual_theme.chrome_px(role, _editor)
|
||||||
|
|
||||||
|
|
||||||
|
func _font_size() -> int:
|
||||||
|
if _font_size_override > 0:
|
||||||
|
return _font_size_override
|
||||||
|
return _theme_px(&"context_menu", 20)
|
||||||
|
|
||||||
|
|
||||||
|
func _menu_row_height() -> int:
|
||||||
|
return maxi(_theme_px(&"context_menu_row_height", DEFAULT_ROW_HEIGHT), DEFAULT_ROW_HEIGHT)
|
||||||
|
|
||||||
|
|
||||||
|
func _menu_min_width() -> int:
|
||||||
|
return maxi(_theme_px(&"context_menu_min_width", DEFAULT_MIN_WIDTH), DEFAULT_MIN_WIDTH)
|
||||||
|
|
||||||
|
|
||||||
|
func _text_color(role: ItemRole) -> Color:
|
||||||
|
if role == ItemRole.DESTRUCTIVE:
|
||||||
|
return _theme_color(&"destructive", Color("#E66B75"))
|
||||||
|
return _theme_color(&"text", Color("#F5F7FA"))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bjidsdsxomilt
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
class_name EditorSelectionDropdown
|
||||||
|
extends MenuButton
|
||||||
|
## Single-choice editor picker with a Studio-style selected row and no radio lane.
|
||||||
|
|
||||||
|
signal item_selected(index: int)
|
||||||
|
|
||||||
|
var selected: int = -1:
|
||||||
|
set(value):
|
||||||
|
_set_selected(value)
|
||||||
|
get:
|
||||||
|
return _selected_index
|
||||||
|
|
||||||
|
var _selected_index: int = -1
|
||||||
|
var _popup_menu: PopupMenu
|
||||||
|
var _field_icons: Array[Texture2D] = []
|
||||||
|
var _control_theme: Theme
|
||||||
|
var _surface_color: Color = Color("#252B38")
|
||||||
|
var _selected_surface_color: Color = Color("#103B4D")
|
||||||
|
var _border_color: Color = Color("#FFFFFF33")
|
||||||
|
var _accent_color: Color = Color("#22C6FF")
|
||||||
|
var _focus_color: Color = Color("#8BD9FF")
|
||||||
|
var _text_color: Color = Color("#F5F7FA")
|
||||||
|
var _font_size: int = 18
|
||||||
|
var _row_separation: int = 6
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
flat = false
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
_popup_menu = get_popup()
|
||||||
|
_popup_menu.shrink_width = false
|
||||||
|
_popup_menu.transparent = false
|
||||||
|
_popup_menu.transparent_bg = false
|
||||||
|
_popup_menu.id_pressed.connect(_on_popup_item_pressed)
|
||||||
|
about_to_popup.connect(_on_about_to_popup)
|
||||||
|
_apply_popup_theme()
|
||||||
|
_apply_field_theme()
|
||||||
|
_sync_selected_item()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
control_theme: Theme,
|
||||||
|
surface_color: Color,
|
||||||
|
selected_surface_color: Color,
|
||||||
|
border_color: Color,
|
||||||
|
accent_color: Color,
|
||||||
|
focus_color: Color,
|
||||||
|
text_color: Color,
|
||||||
|
font_size: int,
|
||||||
|
row_separation: int = 6
|
||||||
|
) -> void:
|
||||||
|
_control_theme = control_theme
|
||||||
|
_surface_color = surface_color
|
||||||
|
_selected_surface_color = selected_surface_color
|
||||||
|
_border_color = border_color
|
||||||
|
_accent_color = accent_color
|
||||||
|
_focus_color = focus_color
|
||||||
|
_text_color = text_color
|
||||||
|
_font_size = font_size
|
||||||
|
_row_separation = maxi(0, row_separation)
|
||||||
|
if is_node_ready():
|
||||||
|
_apply_popup_theme()
|
||||||
|
|
||||||
|
|
||||||
|
func add_item(label: String, id: int = -1) -> void:
|
||||||
|
var index: int = get_popup().item_count
|
||||||
|
var item_id: int = index if id < 0 else id
|
||||||
|
add_styled_item(label, null, null, item_id)
|
||||||
|
|
||||||
|
|
||||||
|
func add_styled_item(
|
||||||
|
label: String,
|
||||||
|
field_icon: Texture2D = null,
|
||||||
|
popup_icon: Texture2D = null,
|
||||||
|
id: int = -1
|
||||||
|
) -> void:
|
||||||
|
var popup: PopupMenu = get_popup()
|
||||||
|
var index: int = popup.item_count
|
||||||
|
var item_id: int = index if id < 0 else id
|
||||||
|
_field_icons.append(field_icon)
|
||||||
|
if popup_icon != null:
|
||||||
|
popup.add_icon_item(popup_icon, label, item_id)
|
||||||
|
popup.set_item_icon_max_width(index, 44)
|
||||||
|
else:
|
||||||
|
popup.add_item(label, item_id)
|
||||||
|
popup.set_item_as_radio_checkable(index, false)
|
||||||
|
if _selected_index < 0:
|
||||||
|
_set_selected(index)
|
||||||
|
|
||||||
|
|
||||||
|
func get_item_text(index: int) -> String:
|
||||||
|
return get_popup().get_item_text(index)
|
||||||
|
|
||||||
|
|
||||||
|
func get_item_icon(index: int) -> Texture2D:
|
||||||
|
return _field_icons[index] if index >= 0 and index < _field_icons.size() else null
|
||||||
|
|
||||||
|
|
||||||
|
func select(index: int) -> void:
|
||||||
|
_set_selected(index)
|
||||||
|
|
||||||
|
|
||||||
|
func clear() -> void:
|
||||||
|
get_popup().clear()
|
||||||
|
_field_icons.clear()
|
||||||
|
_selected_index = -1
|
||||||
|
_sync_selected_item()
|
||||||
|
|
||||||
|
|
||||||
|
func hide_popup() -> void:
|
||||||
|
get_popup().hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_selected(index: int) -> void:
|
||||||
|
var count: int = get_popup().item_count
|
||||||
|
if index < 0 or count == 0:
|
||||||
|
_selected_index = -1
|
||||||
|
else:
|
||||||
|
_selected_index = clampi(index, 0, count - 1)
|
||||||
|
_sync_selected_item()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_selected_item() -> void:
|
||||||
|
if not is_instance_valid(get_popup()):
|
||||||
|
return
|
||||||
|
if _selected_index < 0 or _selected_index >= get_popup().item_count:
|
||||||
|
text = ""
|
||||||
|
icon = null
|
||||||
|
return
|
||||||
|
text = get_popup().get_item_text(_selected_index)
|
||||||
|
icon = _field_icons[_selected_index] if _selected_index < _field_icons.size() else null
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_popup_theme() -> void:
|
||||||
|
if _popup_menu == null:
|
||||||
|
return
|
||||||
|
if _control_theme != null:
|
||||||
|
_popup_menu.theme = _control_theme
|
||||||
|
var panel := StyleBoxFlat.new()
|
||||||
|
var opaque_surface: Color = _surface_color
|
||||||
|
opaque_surface.a = 1.0
|
||||||
|
panel.bg_color = opaque_surface
|
||||||
|
panel.border_color = _border_color
|
||||||
|
panel.set_border_width_all(1)
|
||||||
|
panel.set_corner_radius_all(8)
|
||||||
|
panel.content_margin_left = 6.0
|
||||||
|
panel.content_margin_top = 6.0
|
||||||
|
panel.content_margin_right = 6.0
|
||||||
|
panel.content_margin_bottom = 6.0
|
||||||
|
_popup_menu.add_theme_stylebox_override("panel", panel)
|
||||||
|
var selected_row := StyleBoxFlat.new()
|
||||||
|
selected_row.bg_color = _selected_surface_color
|
||||||
|
selected_row.border_color = _accent_color
|
||||||
|
selected_row.set_border_width_all(2)
|
||||||
|
selected_row.set_corner_radius_all(7)
|
||||||
|
selected_row.content_margin_left = 5.0
|
||||||
|
selected_row.content_margin_top = 12.0
|
||||||
|
selected_row.content_margin_right = 5.0
|
||||||
|
selected_row.content_margin_bottom = 12.0
|
||||||
|
_popup_menu.add_theme_stylebox_override("hover", selected_row)
|
||||||
|
_popup_menu.add_theme_color_override("font_hover_color", _text_color)
|
||||||
|
_popup_menu.add_theme_color_override("font_color", _text_color)
|
||||||
|
_popup_menu.add_theme_font_size_override("font_size", _font_size)
|
||||||
|
_popup_menu.add_theme_constant_override("icon_max_width", 44)
|
||||||
|
_popup_menu.add_theme_constant_override("h_separation", 10)
|
||||||
|
_popup_menu.add_theme_constant_override("v_separation", _row_separation)
|
||||||
|
_popup_menu.add_theme_constant_override("item_start_padding", 4)
|
||||||
|
_popup_menu.add_theme_constant_override("item_end_padding", 4)
|
||||||
|
add_theme_stylebox_override("focus", _make_focus_style())
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_field_theme() -> void:
|
||||||
|
for state: String in ["normal", "hover", "pressed", "disabled"]:
|
||||||
|
var original: StyleBox = get_theme_stylebox(state, "Button")
|
||||||
|
if original == null:
|
||||||
|
continue
|
||||||
|
var field_style: StyleBox = original.duplicate()
|
||||||
|
field_style.content_margin_right = maxf(field_style.content_margin_right, 28.0)
|
||||||
|
add_theme_stylebox_override(state, field_style)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_focus_style() -> StyleBoxFlat:
|
||||||
|
var focus_box := StyleBoxFlat.new()
|
||||||
|
focus_box.bg_color = Color(0.0, 0.0, 0.0, 0.0)
|
||||||
|
focus_box.border_color = _focus_color
|
||||||
|
focus_box.set_border_width_all(2)
|
||||||
|
focus_box.set_corner_radius_all(8)
|
||||||
|
focus_box.content_margin_right = 28.0
|
||||||
|
return focus_box
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var center := Vector2(size.x - 16.0, size.y * 0.5)
|
||||||
|
var points := PackedVector2Array([
|
||||||
|
center + Vector2(-4.0, -2.0),
|
||||||
|
center + Vector2(0.0, 2.0),
|
||||||
|
center + Vector2(4.0, -2.0),
|
||||||
|
])
|
||||||
|
draw_polyline(points, _text_color.darkened(0.28), 2.0, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_about_to_popup() -> void:
|
||||||
|
var target_width: int = maxi(roundi(size.x), _popup_menu.min_size.x)
|
||||||
|
_popup_menu.min_size = Vector2i(target_width, _popup_menu.min_size.y)
|
||||||
|
_popup_menu.size = Vector2i(maxi(_popup_menu.size.x, target_width), _popup_menu.size.y)
|
||||||
|
call_deferred("_sync_popup_size")
|
||||||
|
if _selected_index >= 0:
|
||||||
|
_popup_menu.set_focused_item(_selected_index)
|
||||||
|
else:
|
||||||
|
_popup_menu.set_focused_item(-1)
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_popup_size() -> void:
|
||||||
|
if _popup_menu == null:
|
||||||
|
return
|
||||||
|
var target_width: int = maxi(roundi(size.x), _popup_menu.min_size.x)
|
||||||
|
_popup_menu.min_size = Vector2i(target_width, _popup_menu.min_size.y)
|
||||||
|
_popup_menu.size = Vector2i(maxi(_popup_menu.size.x, target_width), _popup_menu.size.y)
|
||||||
|
if _selected_index >= 0:
|
||||||
|
_popup_menu.set_focused_item(_selected_index)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_popup_item_pressed(id: int) -> void:
|
||||||
|
var index: int = get_popup().get_item_index(id)
|
||||||
|
if index < 0 or index == _selected_index:
|
||||||
|
return
|
||||||
|
_set_selected(index)
|
||||||
|
item_selected.emit(index)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://qlg14pj6jfj7
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
class_name EditorVisualTheme
|
||||||
|
extends RefCounted
|
||||||
|
## Validated semantic chrome tokens shared by the standalone editors.
|
||||||
|
## Canvas artwork and editor-specific drawing values stay with their owners.
|
||||||
|
|
||||||
|
const SCHEMA_VERSION: String = "1.0"
|
||||||
|
const DEFAULT_PATH: String = "res://editor_visual_theme.json"
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: Dictionary = {
|
||||||
|
"version": SCHEMA_VERSION,
|
||||||
|
"chrome": {
|
||||||
|
"colors": {
|
||||||
|
"background": "#171B24F2",
|
||||||
|
"surface": "#252B38FA",
|
||||||
|
"surface_hover": "#30394A",
|
||||||
|
"selected_surface": "#103B4D",
|
||||||
|
"text": "#F5F7FA",
|
||||||
|
"text_muted": "#AAB2C0",
|
||||||
|
"edit": "#22C6FF",
|
||||||
|
"preview": "#22C6FF66",
|
||||||
|
"focus": "#8BD9FF",
|
||||||
|
"action_primary": "#087DBA",
|
||||||
|
"destructive": "#E66B75",
|
||||||
|
"border": "#FFFFFF33",
|
||||||
|
"disabled": "#77808F",
|
||||||
|
"saved": "#8FD6A4",
|
||||||
|
"dirty": "#FFD166",
|
||||||
|
"error": "#FF8A8A",
|
||||||
|
},
|
||||||
|
"fonts": {
|
||||||
|
"body": 18,
|
||||||
|
"helper": 16,
|
||||||
|
"section": 22,
|
||||||
|
"title": 26,
|
||||||
|
"menu": 20,
|
||||||
|
"menu_bar": 22,
|
||||||
|
"menu_popup": 26,
|
||||||
|
"status": 16,
|
||||||
|
"mode": 24,
|
||||||
|
"prop_name": 20,
|
||||||
|
"context_menu": 20,
|
||||||
|
},
|
||||||
|
"spacing": {"small": 8, "medium": 12, "large": 16},
|
||||||
|
"radii": {"control": 8, "panel": 10},
|
||||||
|
"borders": {"normal": 1, "focus": 2},
|
||||||
|
"targets": {
|
||||||
|
"button_min_height": 44,
|
||||||
|
"row_min_height": 44,
|
||||||
|
"mode_button_min_height": 56,
|
||||||
|
"context_menu_row_height": 48,
|
||||||
|
"context_menu_min_width": 208,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"editors": {"prop": {}, "stickman": {}},
|
||||||
|
"stickman_canvas": {
|
||||||
|
"grid_color": "#FFFFFF26",
|
||||||
|
"selection_core": "#FFFFFF",
|
||||||
|
"selection_halo": "#000000D9",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_ROLE_SECTIONS: Dictionary = {
|
||||||
|
"background": "colors",
|
||||||
|
"surface": "colors",
|
||||||
|
"surface_hover": "colors",
|
||||||
|
"selected_surface": "colors",
|
||||||
|
"text": "colors",
|
||||||
|
"text_muted": "colors",
|
||||||
|
"edit": "colors",
|
||||||
|
"preview": "colors",
|
||||||
|
"focus": "colors",
|
||||||
|
"action_primary": "colors",
|
||||||
|
"destructive": "colors",
|
||||||
|
"border": "colors",
|
||||||
|
"disabled": "colors",
|
||||||
|
"saved": "colors",
|
||||||
|
"dirty": "colors",
|
||||||
|
"error": "colors",
|
||||||
|
}
|
||||||
|
const SIZE_ROLE_SECTIONS: Dictionary = {
|
||||||
|
"body": "fonts",
|
||||||
|
"helper": "fonts",
|
||||||
|
"section": "fonts",
|
||||||
|
"title": "fonts",
|
||||||
|
"menu": "fonts",
|
||||||
|
"menu_bar": "fonts",
|
||||||
|
"menu_popup": "fonts",
|
||||||
|
"status": "fonts",
|
||||||
|
"mode": "fonts",
|
||||||
|
"prop_name": "fonts",
|
||||||
|
"context_menu": "fonts",
|
||||||
|
"small": "spacing",
|
||||||
|
"medium": "spacing",
|
||||||
|
"large": "spacing",
|
||||||
|
"control": "radii",
|
||||||
|
"panel": "radii",
|
||||||
|
"normal_border": "borders",
|
||||||
|
"focus_border": "borders",
|
||||||
|
"button_min_height": "targets",
|
||||||
|
"row_min_height": "targets",
|
||||||
|
"mode_button_min_height": "targets",
|
||||||
|
"context_menu_row_height": "targets",
|
||||||
|
"context_menu_min_width": "targets",
|
||||||
|
}
|
||||||
|
const CONFIG_SIZE_KEYS: Dictionary = {
|
||||||
|
"normal_border": "normal",
|
||||||
|
"focus_border": "focus",
|
||||||
|
}
|
||||||
|
|
||||||
|
var _config: Dictionary = DEFAULT_CONFIG.duplicate(true)
|
||||||
|
var _warned: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||||
|
_warned.clear()
|
||||||
|
_config = DEFAULT_CONFIG.duplicate(true)
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
_warn_once(path, "EditorVisualTheme: '%s' was not found; using defaults." % path)
|
||||||
|
return false
|
||||||
|
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
_warn_once(path, "EditorVisualTheme: '%s' could not be read; using defaults." % path)
|
||||||
|
return false
|
||||||
|
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||||
|
file.close()
|
||||||
|
if not parsed is Dictionary:
|
||||||
|
_warn_once(path, "EditorVisualTheme: '%s' is malformed; using defaults." % path)
|
||||||
|
return false
|
||||||
|
_config = _deep_merge(_config, parsed as Dictionary)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func raw_config() -> Dictionary:
|
||||||
|
return _config.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func chrome_color(role: StringName, editor: StringName = &"") -> Color:
|
||||||
|
var role_key: String = String(role)
|
||||||
|
if not COLOR_ROLE_SECTIONS.has(role_key):
|
||||||
|
_warn_once("color:" + role_key, "EditorVisualTheme: unknown color role '%s'." % role_key)
|
||||||
|
return Color.MAGENTA
|
||||||
|
var section: String = COLOR_ROLE_SECTIONS[role_key]
|
||||||
|
var raw: Variant = _resolved_value(section, role_key, editor)
|
||||||
|
var fallback_config: Dictionary = DEFAULT_CONFIG["chrome"][section]
|
||||||
|
var fallback: Color = Color.from_string(String(fallback_config[role_key]), Color.WHITE)
|
||||||
|
if not raw is String:
|
||||||
|
_warn_once("color:" + role_key, "EditorVisualTheme: invalid value for color role '%s'." % role_key)
|
||||||
|
return fallback
|
||||||
|
var hex: String = String(raw)
|
||||||
|
if not hex.begins_with("#") or (hex.length() != 7 and hex.length() != 9):
|
||||||
|
_warn_once("color:" + role_key, "EditorVisualTheme: invalid color at '%s'." % role_key)
|
||||||
|
return fallback
|
||||||
|
return Color.from_string(hex, fallback)
|
||||||
|
|
||||||
|
|
||||||
|
func chrome_px(role: StringName, editor: StringName = &"") -> int:
|
||||||
|
var role_key: String = String(role)
|
||||||
|
if not SIZE_ROLE_SECTIONS.has(role_key):
|
||||||
|
_warn_once("size:" + role_key, "EditorVisualTheme: unknown size role '%s'." % role_key)
|
||||||
|
return 0
|
||||||
|
var section: String = SIZE_ROLE_SECTIONS[role_key]
|
||||||
|
var config_key: String = String(CONFIG_SIZE_KEYS.get(role_key, role_key))
|
||||||
|
var raw: Variant = _resolved_value(section, config_key, editor)
|
||||||
|
var fallback: int = int(DEFAULT_CONFIG["chrome"][section][config_key])
|
||||||
|
if not raw is int and not raw is float:
|
||||||
|
_warn_once("size:" + role_key, "EditorVisualTheme: invalid value for size role '%s'." % role_key)
|
||||||
|
return fallback
|
||||||
|
return clampi(int(round(float(raw))), 0, 256)
|
||||||
|
|
||||||
|
|
||||||
|
func stickman_canvas_color(role: StringName) -> Color:
|
||||||
|
var role_key: String = String(role)
|
||||||
|
var fallback_text: String = String(DEFAULT_CONFIG["stickman_canvas"].get(role_key, "#FFFFFF"))
|
||||||
|
var fallback: Color = Color.from_string(fallback_text, Color.WHITE)
|
||||||
|
var raw: Variant = _get_nested(_config, ["stickman_canvas", role_key], fallback_text)
|
||||||
|
if not raw is String:
|
||||||
|
_warn_once("stickman_canvas:" + role_key, "EditorVisualTheme: invalid Stickman canvas color '%s'." % role_key)
|
||||||
|
return fallback
|
||||||
|
var hex: String = String(raw)
|
||||||
|
if not hex.begins_with("#") or (hex.length() != 7 and hex.length() != 9):
|
||||||
|
_warn_once("stickman_canvas:" + role_key, "EditorVisualTheme: invalid Stickman canvas color '%s'." % role_key)
|
||||||
|
return fallback
|
||||||
|
return Color.from_string(hex, fallback)
|
||||||
|
|
||||||
|
|
||||||
|
func control_theme(editor: StringName) -> Theme:
|
||||||
|
var theme: Theme = Theme.new()
|
||||||
|
var surface: Color = chrome_color(&"surface", editor)
|
||||||
|
var background: Color = chrome_color(&"background", editor)
|
||||||
|
var hover: Color = chrome_color(&"surface_hover", editor)
|
||||||
|
var text: Color = chrome_color(&"text", editor)
|
||||||
|
var muted: Color = chrome_color(&"text_muted", editor)
|
||||||
|
var edit: Color = chrome_color(&"edit", editor)
|
||||||
|
var focus: Color = chrome_color(&"focus", editor)
|
||||||
|
var border: Color = chrome_color(&"border", editor)
|
||||||
|
var radius: int = chrome_px(&"control", editor)
|
||||||
|
var normal_width: int = chrome_px(&"normal_border", editor)
|
||||||
|
var focus_width: int = maxi(2, chrome_px(&"focus_border", editor))
|
||||||
|
var normal_box: StyleBoxFlat = _make_box(surface, border, radius, normal_width)
|
||||||
|
var hover_box: StyleBoxFlat = _make_box(hover, edit, radius, normal_width)
|
||||||
|
var pressed_box: StyleBoxFlat = _make_box(surface.darkened(0.12), edit, radius, focus_width)
|
||||||
|
var focus_box: StyleBoxFlat = _make_box(Color(0, 0, 0, 0), focus, radius, focus_width)
|
||||||
|
var disabled_box: StyleBoxFlat = _make_box(background.darkened(0.1), border, radius, normal_width)
|
||||||
|
for type_name: String in ["Button", "OptionButton", "ColorPickerButton", "CheckBox"]:
|
||||||
|
theme.set_stylebox("normal", type_name, normal_box.duplicate())
|
||||||
|
theme.set_stylebox("hover", type_name, hover_box.duplicate())
|
||||||
|
theme.set_stylebox("pressed", type_name, pressed_box.duplicate())
|
||||||
|
theme.set_stylebox("focus", type_name, focus_box.duplicate())
|
||||||
|
theme.set_stylebox("disabled", type_name, disabled_box.duplicate())
|
||||||
|
theme.set_color("font_color", type_name, text)
|
||||||
|
theme.set_color("font_hover_color", type_name, text)
|
||||||
|
theme.set_color("font_pressed_color", type_name, edit)
|
||||||
|
theme.set_color("font_disabled_color", type_name, muted)
|
||||||
|
theme.set_font_size("font_size", type_name, chrome_px(&"body", editor))
|
||||||
|
var input_box: StyleBoxFlat = _make_box(background, border, radius, normal_width)
|
||||||
|
var input_focus: StyleBoxFlat = _make_box(background, focus, radius, focus_width)
|
||||||
|
for type_name: String in ["LineEdit", "SpinBox", "TextEdit"]:
|
||||||
|
theme.set_stylebox("normal", type_name, input_box.duplicate())
|
||||||
|
theme.set_stylebox("focus", type_name, input_focus.duplicate())
|
||||||
|
theme.set_color("font_color", type_name, text)
|
||||||
|
theme.set_color("font_placeholder_color", type_name, muted)
|
||||||
|
theme.set_font_size("font_size", type_name, chrome_px(&"body", editor))
|
||||||
|
var panel_box: StyleBoxFlat = _make_box(surface, border, chrome_px(&"panel", editor), normal_width)
|
||||||
|
theme.set_stylebox("panel", "PanelContainer", panel_box)
|
||||||
|
theme.set_stylebox("panel", "MenuBar", panel_box.duplicate())
|
||||||
|
theme.set_font_size("font_size", "Label", chrome_px(&"body", editor))
|
||||||
|
theme.set_color("font_color", "Label", text)
|
||||||
|
theme.set_color("font_hover_color", "Label", text)
|
||||||
|
theme.set_font_size("font_size", "PopupMenu", chrome_px(&"menu_popup", editor))
|
||||||
|
theme.set_font_size("font_size", "MenuBar", chrome_px(&"menu_bar", editor))
|
||||||
|
theme.set_color("font_color", "MenuBar", text)
|
||||||
|
return theme
|
||||||
|
|
||||||
|
|
||||||
|
func style_button_role(button: Button, role: StringName, editor: StringName) -> void:
|
||||||
|
button.custom_minimum_size.y = maxf(button.custom_minimum_size.y, float(chrome_px(&"button_min_height", editor)))
|
||||||
|
if role != &"primary" and role != &"destructive":
|
||||||
|
return
|
||||||
|
var fill_role: StringName = &"action_primary" if role == &"primary" else &"destructive"
|
||||||
|
var fill: Color = chrome_color(fill_role, editor)
|
||||||
|
var focus: Color = chrome_color(&"focus", editor)
|
||||||
|
var radius: int = chrome_px(&"control", editor)
|
||||||
|
var border_width: int = chrome_px(&"normal_border", editor)
|
||||||
|
var focus_width: int = maxi(2, chrome_px(&"focus_border", editor))
|
||||||
|
button.add_theme_stylebox_override("normal", _make_box(fill, fill.lightened(0.12), radius, border_width))
|
||||||
|
button.add_theme_stylebox_override("hover", _make_box(fill.lightened(0.12), focus, radius, border_width))
|
||||||
|
button.add_theme_stylebox_override("pressed", _make_box(fill.darkened(0.12), focus, radius, focus_width))
|
||||||
|
button.add_theme_stylebox_override("focus", _make_box(Color(0.0, 0.0, 0.0, 0.0), focus, radius, focus_width))
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
func _resolved_value(section: String, key: String, editor: StringName) -> Variant:
|
||||||
|
if not editor.is_empty():
|
||||||
|
var editor_value: Variant = _get_nested(_config, ["editors", String(editor), section, key], null)
|
||||||
|
if editor_value != null:
|
||||||
|
return editor_value
|
||||||
|
return _get_nested(_config, ["chrome", section, key], null)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_nested(source: Dictionary, keys: Array[String], fallback: Variant) -> Variant:
|
||||||
|
var cursor: Variant = source
|
||||||
|
for key: String in keys:
|
||||||
|
if not cursor is Dictionary or not (cursor as Dictionary).has(key):
|
||||||
|
return fallback
|
||||||
|
cursor = (cursor as Dictionary)[key]
|
||||||
|
return cursor
|
||||||
|
|
||||||
|
|
||||||
|
func _make_box(fill: Color, outline: Color, radius: int, width: int) -> StyleBoxFlat:
|
||||||
|
var box: StyleBoxFlat = StyleBoxFlat.new()
|
||||||
|
box.bg_color = fill
|
||||||
|
box.border_color = outline
|
||||||
|
box.set_border_width_all(width)
|
||||||
|
box.set_corner_radius_all(radius)
|
||||||
|
box.content_margin_left = 8
|
||||||
|
box.content_margin_right = 8
|
||||||
|
box.content_margin_top = 6
|
||||||
|
box.content_margin_bottom = 6
|
||||||
|
return box
|
||||||
|
|
||||||
|
|
||||||
|
func _deep_merge(base: Dictionary, overrides: Dictionary) -> Dictionary:
|
||||||
|
var merged: Dictionary = base.duplicate(true)
|
||||||
|
for key: Variant in overrides:
|
||||||
|
if merged.get(key) is Dictionary and overrides[key] is Dictionary:
|
||||||
|
merged[key] = _deep_merge(merged[key] as Dictionary, overrides[key] as Dictionary)
|
||||||
|
else:
|
||||||
|
merged[key] = overrides[key]
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
func _warn_once(key: String, message: String) -> void:
|
||||||
|
if not _warned.has(key):
|
||||||
|
_warned[key] = true
|
||||||
|
push_warning(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c2mnpqu565na
|
||||||
@@ -0,0 +1,688 @@
|
|||||||
|
class_name StageDialogStyler
|
||||||
|
extends RefCounted
|
||||||
|
## Renders child-facing stage and editor dialogs as a shared card while preserving
|
||||||
|
## the native AcceptDialog/ConfirmationDialog signals used by their workflows.
|
||||||
|
|
||||||
|
const THEME_DEFAULTS := preload("res://scripts/stage/sandbox_theme.gd")
|
||||||
|
const OVERLAY_META := &"stage_dialog_overlay"
|
||||||
|
const BODY_META := &"stage_dialog_body"
|
||||||
|
const BODY_SCROLL_META := &"stage_dialog_body_scroll"
|
||||||
|
const NOTE_META := &"stage_dialog_note"
|
||||||
|
const DETAIL_META := &"stage_dialog_detail"
|
||||||
|
const ACTIONS_META := &"stage_dialog_actions"
|
||||||
|
const EXTRAS_META := &"stage_dialog_extra_controls"
|
||||||
|
const CARD_META := &"stage_dialog_card"
|
||||||
|
const CARD_WIDTH_META := &"stage_dialog_card_width"
|
||||||
|
const MAX_HEIGHT_RATIO_META := &"stage_dialog_max_height_ratio"
|
||||||
|
const SCREEN_MARGIN_META := &"stage_dialog_screen_margin"
|
||||||
|
const TITLE_META := &"stage_dialog_title"
|
||||||
|
const ICON_META := &"stage_dialog_icon"
|
||||||
|
const ICON_LABEL_META := &"stage_dialog_icon_label"
|
||||||
|
const ICON_BADGE_META := &"stage_dialog_icon_badge"
|
||||||
|
const PRIMARY_META := &"stage_dialog_primary_button"
|
||||||
|
const SECONDARY_META := &"stage_dialog_secondary_buttons"
|
||||||
|
const DESTRUCTIVE_META := &"stage_dialog_destructive_buttons"
|
||||||
|
const DEFAULT_PRIMARY_META := &"stage_dialog_default_primary"
|
||||||
|
const OPEN_META := &"stage_dialog_open"
|
||||||
|
|
||||||
|
|
||||||
|
static func apply(
|
||||||
|
dialog: Window,
|
||||||
|
stage_theme: SandboxTheme,
|
||||||
|
ui_font: Font,
|
||||||
|
primary_button: Button = null,
|
||||||
|
destructive_buttons: Array = [],
|
||||||
|
secondary_buttons: Array = [],
|
||||||
|
default_ok_is_primary: bool = true
|
||||||
|
) -> void:
|
||||||
|
if dialog == null:
|
||||||
|
return
|
||||||
|
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
|
||||||
|
if not dialog.has_meta(OVERLAY_META):
|
||||||
|
_install_dialog_surface(dialog, active_theme, ui_font)
|
||||||
|
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||||
|
var primary: Button = primary_button
|
||||||
|
if primary == null and default_ok_is_primary and not _contains_button(destructive_buttons, ok_button):
|
||||||
|
primary = ok_button
|
||||||
|
dialog.set_meta(PRIMARY_META, primary)
|
||||||
|
dialog.set_meta(SECONDARY_META, secondary_buttons.duplicate())
|
||||||
|
dialog.set_meta(DESTRUCTIVE_META, destructive_buttons.duplicate())
|
||||||
|
dialog.set_meta(DEFAULT_PRIMARY_META, default_ok_is_primary)
|
||||||
|
_refresh_dialog_surface(dialog, active_theme, ui_font, primary, destructive_buttons, secondary_buttons, default_ok_is_primary)
|
||||||
|
|
||||||
|
|
||||||
|
static func popup(dialog: Window) -> void:
|
||||||
|
if dialog == null:
|
||||||
|
return
|
||||||
|
var active_theme: SandboxTheme = _theme_for(dialog)
|
||||||
|
var font: Font = dialog.get_meta(&"stage_dialog_font") as Font if dialog.has_meta(&"stage_dialog_font") else null
|
||||||
|
var primary: Button = dialog.get_meta(PRIMARY_META) as Button if dialog.has_meta(PRIMARY_META) else null
|
||||||
|
var secondary_buttons: Array = dialog.get_meta(SECONDARY_META, []) as Array
|
||||||
|
var destructive_buttons: Array = dialog.get_meta(DESTRUCTIVE_META, []) as Array
|
||||||
|
var default_ok_is_primary: bool = bool(dialog.get_meta(DEFAULT_PRIMARY_META, true))
|
||||||
|
_refresh_dialog_surface(dialog, active_theme, font, primary, destructive_buttons, secondary_buttons, default_ok_is_primary)
|
||||||
|
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||||
|
if overlay == null:
|
||||||
|
return
|
||||||
|
dialog.hide()
|
||||||
|
dialog.set_meta(OPEN_META, true)
|
||||||
|
overlay.show()
|
||||||
|
refit(dialog)
|
||||||
|
var tree: SceneTree = dialog.get_tree()
|
||||||
|
if tree != null:
|
||||||
|
var fit_after_final_layout := func() -> void:
|
||||||
|
if not is_instance_valid(dialog) or not dialog.is_inside_tree() or not is_open(dialog):
|
||||||
|
return
|
||||||
|
refit(dialog)
|
||||||
|
var fit_after_layout := func() -> void:
|
||||||
|
if is_instance_valid(tree):
|
||||||
|
tree.process_frame.connect(fit_after_final_layout, Object.CONNECT_ONE_SHOT)
|
||||||
|
var defer_fit_one_frame := func() -> void:
|
||||||
|
if is_instance_valid(tree):
|
||||||
|
tree.process_frame.connect(fit_after_layout, Object.CONNECT_ONE_SHOT)
|
||||||
|
tree.process_frame.connect(defer_fit_one_frame, Object.CONNECT_ONE_SHOT)
|
||||||
|
var focus_primary: Button = dialog.get_meta(PRIMARY_META) as Button if dialog.has_meta(PRIMARY_META) else null
|
||||||
|
var focus_target: Button = visual_button(focus_primary)
|
||||||
|
if focus_target == null and dialog is ConfirmationDialog:
|
||||||
|
focus_target = visual_button((dialog as ConfirmationDialog).get_cancel_button())
|
||||||
|
if focus_target != null and focus_target.visible:
|
||||||
|
focus_target.call_deferred("grab_focus")
|
||||||
|
|
||||||
|
|
||||||
|
static func is_open(dialog: Window) -> bool:
|
||||||
|
if dialog == null or not is_instance_valid(dialog):
|
||||||
|
return false
|
||||||
|
if dialog.has_meta(OVERLAY_META):
|
||||||
|
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||||
|
return overlay != null and is_instance_valid(overlay) and overlay.visible
|
||||||
|
return dialog.visible
|
||||||
|
|
||||||
|
|
||||||
|
static func refit(dialog: Window) -> void:
|
||||||
|
if dialog == null or not is_instance_valid(dialog) or not dialog.has_meta(OVERLAY_META):
|
||||||
|
return
|
||||||
|
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||||
|
if overlay == null or not is_instance_valid(overlay):
|
||||||
|
return
|
||||||
|
var host_viewport: Viewport = overlay.get_viewport()
|
||||||
|
if host_viewport == null:
|
||||||
|
return
|
||||||
|
var active_theme: SandboxTheme = _theme_for(dialog)
|
||||||
|
var viewport_size: Vector2i = Vector2i(host_viewport.get_visible_rect().size)
|
||||||
|
var card: PanelContainer = dialog.get_meta(CARD_META) as PanelContainer
|
||||||
|
if card != null:
|
||||||
|
var screen_margin: int = int(dialog.get_meta(SCREEN_MARGIN_META, 18))
|
||||||
|
var default_width: int = active_theme.integer("popups.confirmation.card_width", 560, 320, 720)
|
||||||
|
var card_width: int = int(dialog.get_meta(CARD_WIDTH_META, default_width))
|
||||||
|
var available_width: float = maxf(0.0, float(viewport_size.x - screen_margin * 2))
|
||||||
|
card.custom_minimum_size.x = minf(float(card_width), available_width)
|
||||||
|
_fit_body_scroll(dialog, viewport_size, active_theme)
|
||||||
|
|
||||||
|
|
||||||
|
static func is_styled(dialog: Window) -> bool:
|
||||||
|
return dialog != null and is_instance_valid(dialog) and dialog.has_meta(OVERLAY_META)
|
||||||
|
|
||||||
|
|
||||||
|
static func hide(dialog: Window) -> void:
|
||||||
|
if dialog == null or not is_instance_valid(dialog):
|
||||||
|
return
|
||||||
|
dialog.set_meta(OPEN_META, false)
|
||||||
|
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control if dialog.has_meta(OVERLAY_META) else null
|
||||||
|
if overlay != null and is_instance_valid(overlay):
|
||||||
|
overlay.hide()
|
||||||
|
if dialog.visible:
|
||||||
|
dialog.hide()
|
||||||
|
|
||||||
|
|
||||||
|
static func dismiss_on_escape(dialog: Window) -> void:
|
||||||
|
if dialog == null or not is_instance_valid(dialog):
|
||||||
|
return
|
||||||
|
if dialog is ConfirmationDialog:
|
||||||
|
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button()
|
||||||
|
var visible_cancel: Button = visual_button(cancel_button)
|
||||||
|
if visible_cancel != null and visible_cancel.visible:
|
||||||
|
visible_cancel.pressed.emit()
|
||||||
|
return
|
||||||
|
if dialog.has_signal("close_requested"):
|
||||||
|
dialog.emit_signal("close_requested")
|
||||||
|
hide(dialog)
|
||||||
|
|
||||||
|
|
||||||
|
static func visual_button(original_button: Button) -> Button:
|
||||||
|
if original_button == null:
|
||||||
|
return null
|
||||||
|
var visual: Variant = original_button.get_meta(&"stage_dialog_visual_button", null)
|
||||||
|
return visual as Button if visual is Button else original_button
|
||||||
|
|
||||||
|
|
||||||
|
static func panel_style(stage_theme: SandboxTheme, include_content_padding: bool = true) -> StyleBoxFlat:
|
||||||
|
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = active_theme.color("popups.confirmation.surface_color", Color("#202633FA"))
|
||||||
|
style.border_color = active_theme.color("popups.confirmation.border_color", Color("#667386"))
|
||||||
|
style.set_border_width_all(active_theme.integer("popups.confirmation.border_width", 2, 1, 4))
|
||||||
|
style.set_corner_radius_all(active_theme.integer("popups.confirmation.corner_radius", 18, 10, 28))
|
||||||
|
if include_content_padding:
|
||||||
|
var padding: int = active_theme.integer("popups.confirmation.padding", 22, 12, 36)
|
||||||
|
style.content_margin_left = padding
|
||||||
|
style.content_margin_right = padding
|
||||||
|
style.content_margin_top = padding
|
||||||
|
style.content_margin_bottom = padding
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
static func style_button(button: Button, role: StringName, stage_theme: SandboxTheme, ui_font: Font = null) -> void:
|
||||||
|
if button == null:
|
||||||
|
return
|
||||||
|
var active_theme: SandboxTheme = stage_theme if stage_theme != null else THEME_DEFAULTS.new()
|
||||||
|
var action: Color = active_theme.color("popups.confirmation.action_color", Color("#087DBA"))
|
||||||
|
var accent: Color = active_theme.color("popups.confirmation.accent_color", Color("#8BD9FF"))
|
||||||
|
var danger: Color = active_theme.color("popups.confirmation.danger_color", Color("#D94A56"))
|
||||||
|
var secondary: Color = active_theme.color("popups.confirmation.secondary_surface", Color("#293242"))
|
||||||
|
var secondary_border: Color = active_theme.color("popups.confirmation.secondary_border", Color("#718096"))
|
||||||
|
var normal: Color = action
|
||||||
|
var hover: Color = action.lightened(0.12)
|
||||||
|
var pressed: Color = action.darkened(0.12)
|
||||||
|
var border: Color = accent
|
||||||
|
var label_color: Color = Color("#F5F7FA")
|
||||||
|
match role:
|
||||||
|
&"primary":
|
||||||
|
normal = action
|
||||||
|
hover = active_theme.color("stage_panels.action.primary_hover_color", action.lightened(0.12))
|
||||||
|
pressed = active_theme.color("stage_panels.action.primary_pressed_color", action.darkened(0.12))
|
||||||
|
border = accent
|
||||||
|
&"danger":
|
||||||
|
normal = active_theme.color("popups.confirmation.danger_surface", Color("#44262D"))
|
||||||
|
hover = active_theme.color("popups.confirmation.danger_hover_surface", Color("#3B252C"))
|
||||||
|
pressed = active_theme.color("popups.confirmation.danger_pressed_surface", Color("#4A252D"))
|
||||||
|
border = danger
|
||||||
|
label_color = Color("#FFE6E8")
|
||||||
|
_:
|
||||||
|
normal = secondary
|
||||||
|
hover = active_theme.color("stage_panels.action.hover_surface", Color("#303A4B"))
|
||||||
|
pressed = active_theme.color("stage_panels.action.selected_surface", Color("#12394A"))
|
||||||
|
border = secondary_border
|
||||||
|
var border_width: int = active_theme.integer("popups.confirmation.button_border_width", 2, 1, 5)
|
||||||
|
var radius: int = active_theme.integer("popups.confirmation.button_radius", 12, 8, 24)
|
||||||
|
button.add_theme_stylebox_override("normal", _button_box(normal, border, border_width, radius))
|
||||||
|
button.add_theme_stylebox_override("hover", _button_box(hover, border.lightened(0.12), border_width + 1, radius))
|
||||||
|
button.add_theme_stylebox_override("pressed", _button_box(pressed, border, border_width + 1, radius))
|
||||||
|
button.add_theme_stylebox_override("hover_pressed", _button_box(pressed.lightened(0.05), border, border_width + 1, radius))
|
||||||
|
button.add_theme_stylebox_override("disabled", _button_box(secondary.darkened(0.18), secondary_border.darkened(0.35), 1, radius))
|
||||||
|
button.add_theme_stylebox_override("focus", _button_box(normal, active_theme.color("popups.confirmation.focus_color", Color("#8BD9FF")), border_width + 1, radius))
|
||||||
|
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_color_override("font_disabled_color", Color("#AAB2C0"))
|
||||||
|
button.add_theme_font_size_override("font_size", active_theme.integer("popups.confirmation.button_font_size", 20, 16, 32))
|
||||||
|
var min_height: int = active_theme.integer("popups.confirmation.button_height", 52, 44, 80)
|
||||||
|
var min_width: int = active_theme.integer("popups.confirmation.button_min_width", 112, 88, 240)
|
||||||
|
button.custom_minimum_size = Vector2(maxf(button.custom_minimum_size.x, float(min_width)), maxf(button.custom_minimum_size.y, float(min_height)))
|
||||||
|
if ui_font != null:
|
||||||
|
button.add_theme_font_override("font", _strong_font(ui_font) if role == &"primary" else ui_font)
|
||||||
|
button.focus_mode = Control.FOCUS_ALL
|
||||||
|
|
||||||
|
|
||||||
|
static func _install_dialog_surface(dialog: Window, stage_theme: SandboxTheme, ui_font: Font) -> void:
|
||||||
|
dialog.set_meta(&"stage_dialog_theme", stage_theme)
|
||||||
|
if ui_font != null:
|
||||||
|
dialog.set_meta(&"stage_dialog_font", ui_font)
|
||||||
|
dialog.theme = stage_theme.control_theme()
|
||||||
|
dialog.borderless = true
|
||||||
|
dialog.transparent = true
|
||||||
|
dialog.transparent_bg = true
|
||||||
|
dialog.unresizable = true
|
||||||
|
dialog.wrap_controls = false
|
||||||
|
dialog.add_theme_color_override("title_color", Color.TRANSPARENT)
|
||||||
|
dialog.add_theme_font_size_override("title_font_size", 1)
|
||||||
|
if ui_font != null:
|
||||||
|
dialog.add_theme_font_override("font", ui_font)
|
||||||
|
|
||||||
|
var overlay_host: Node = _find_overlay_host(dialog)
|
||||||
|
if overlay_host == null:
|
||||||
|
return
|
||||||
|
var overlay := Control.new()
|
||||||
|
overlay.name = "StageDialogOverlay"
|
||||||
|
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
overlay.theme = stage_theme.control_theme()
|
||||||
|
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
overlay.z_index = 100
|
||||||
|
overlay.z_as_relative = false
|
||||||
|
overlay.visible = false
|
||||||
|
overlay.gui_input.connect(_on_overlay_input.bind(dialog))
|
||||||
|
overlay_host.add_child(overlay)
|
||||||
|
dialog.set_meta(OVERLAY_META, overlay)
|
||||||
|
dialog.set_meta(OPEN_META, false)
|
||||||
|
dialog.set_meta(&"stage_dialog_overlay_host", overlay_host)
|
||||||
|
dialog.tree_exiting.connect(_on_dialog_tree_exiting.bind(dialog, overlay), CONNECT_ONE_SHOT)
|
||||||
|
if dialog.has_signal("confirmed"):
|
||||||
|
dialog.connect("confirmed", _on_native_dialog_closed.bind(dialog))
|
||||||
|
if dialog.has_signal("canceled"):
|
||||||
|
dialog.connect("canceled", _on_native_dialog_closed.bind(dialog))
|
||||||
|
if dialog.has_signal("custom_action"):
|
||||||
|
dialog.connect("custom_action", _on_native_dialog_custom_action.bind(dialog))
|
||||||
|
if dialog.has_signal("close_requested"):
|
||||||
|
dialog.connect("close_requested", _on_native_dialog_closed.bind(dialog))
|
||||||
|
|
||||||
|
var backdrop := ColorRect.new()
|
||||||
|
backdrop.name = "Backdrop"
|
||||||
|
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
backdrop.color = stage_theme.color("popups.confirmation.backdrop_color", Color("#080D15B8"))
|
||||||
|
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
overlay.add_child(backdrop)
|
||||||
|
|
||||||
|
var center := CenterContainer.new()
|
||||||
|
center.name = "CardCenter"
|
||||||
|
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
overlay.add_child(center)
|
||||||
|
|
||||||
|
var card := PanelContainer.new()
|
||||||
|
card.name = "DialogCard"
|
||||||
|
card.custom_minimum_size = Vector2(560.0, 0.0)
|
||||||
|
card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
card.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
card.add_theme_stylebox_override("panel", panel_style(stage_theme))
|
||||||
|
center.add_child(card)
|
||||||
|
dialog.set_meta(CARD_META, card)
|
||||||
|
|
||||||
|
var content := VBoxContainer.new()
|
||||||
|
content.name = "DialogContent"
|
||||||
|
content.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24))
|
||||||
|
card.add_child(content)
|
||||||
|
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
header.name = "DialogHeader"
|
||||||
|
header.add_theme_constant_override("separation", 12)
|
||||||
|
content.add_child(header)
|
||||||
|
|
||||||
|
var icon_badge := PanelContainer.new()
|
||||||
|
icon_badge.name = "IconBadge"
|
||||||
|
icon_badge.custom_minimum_size = Vector2(40.0, 40.0)
|
||||||
|
icon_badge.add_theme_stylebox_override("panel", _icon_box(Color("#173A4B"), Color("#4DB9FF")))
|
||||||
|
header.add_child(icon_badge)
|
||||||
|
dialog.set_meta(ICON_BADGE_META, icon_badge)
|
||||||
|
|
||||||
|
var icon_label := Label.new()
|
||||||
|
icon_label.name = "DialogIcon"
|
||||||
|
icon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
icon_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
icon_label.add_theme_font_size_override("font_size", 22)
|
||||||
|
icon_badge.add_child(icon_label)
|
||||||
|
dialog.set_meta(ICON_LABEL_META, icon_label)
|
||||||
|
|
||||||
|
var title_label := Label.new()
|
||||||
|
title_label.name = "DialogTitle"
|
||||||
|
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
title_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
title_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40))
|
||||||
|
if ui_font != null:
|
||||||
|
title_label.add_theme_font_override("font", _strong_font(ui_font))
|
||||||
|
header.add_child(title_label)
|
||||||
|
dialog.set_meta(TITLE_META, title_label)
|
||||||
|
|
||||||
|
var body := VBoxContainer.new()
|
||||||
|
body.name = "DialogBody"
|
||||||
|
body.add_theme_constant_override("separation", 10)
|
||||||
|
body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
var body_scroll := ScrollContainer.new()
|
||||||
|
body_scroll.name = "DialogBodyScroll"
|
||||||
|
body_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
body_scroll.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||||
|
body_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
content.add_child(body_scroll)
|
||||||
|
body_scroll.add_child(body)
|
||||||
|
dialog.set_meta(BODY_META, body)
|
||||||
|
dialog.set_meta(BODY_SCROLL_META, body_scroll)
|
||||||
|
|
||||||
|
var body_label := Label.new()
|
||||||
|
body_label.name = "DialogMessage"
|
||||||
|
body_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
body_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
body_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
body_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
body_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
|
||||||
|
body.add_child(body_label)
|
||||||
|
dialog.set_meta(&"stage_dialog_message", body_label)
|
||||||
|
|
||||||
|
var detail_box := PanelContainer.new()
|
||||||
|
detail_box.name = "DialogDetails"
|
||||||
|
detail_box.add_theme_stylebox_override("panel", _detail_box(stage_theme))
|
||||||
|
var detail_label := Label.new()
|
||||||
|
detail_label.name = "DetailsText"
|
||||||
|
detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
detail_label.add_theme_font_size_override("font_size", stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
|
||||||
|
detail_box.add_child(detail_label)
|
||||||
|
body.add_child(detail_box)
|
||||||
|
dialog.set_meta(DETAIL_META, detail_box)
|
||||||
|
|
||||||
|
var note := Label.new()
|
||||||
|
note.name = "DialogNote"
|
||||||
|
note.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
note.add_theme_color_override("font_color", Color("#B8C7D8"))
|
||||||
|
note.add_theme_font_size_override("font_size", 17)
|
||||||
|
body.add_child(note)
|
||||||
|
dialog.set_meta(NOTE_META, note)
|
||||||
|
|
||||||
|
var action_row := HBoxContainer.new()
|
||||||
|
action_row.name = "DialogActions"
|
||||||
|
action_row.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
action_row.add_theme_constant_override("separation", stage_theme.integer("popups.confirmation.footer_gap", 12, 8, 24))
|
||||||
|
content.add_child(action_row)
|
||||||
|
dialog.set_meta(ACTIONS_META, action_row)
|
||||||
|
|
||||||
|
var extras: Array[Control] = []
|
||||||
|
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||||
|
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null
|
||||||
|
var default_label: Label = (dialog as AcceptDialog).get_label() if dialog is AcceptDialog else null
|
||||||
|
if default_label != null:
|
||||||
|
default_label.visible = false
|
||||||
|
if ok_button != null:
|
||||||
|
ok_button.visible = false
|
||||||
|
if cancel_button != null:
|
||||||
|
cancel_button.visible = false
|
||||||
|
for child: Node in dialog.get_children():
|
||||||
|
if child is Control and not child is Button and child != default_label and child != ok_button and child != cancel_button:
|
||||||
|
var control: Control = child as Control
|
||||||
|
control.reparent(body)
|
||||||
|
extras.append(control)
|
||||||
|
dialog.set_meta(EXTRAS_META, extras)
|
||||||
|
dialog.hide()
|
||||||
|
|
||||||
|
|
||||||
|
static func _refresh_dialog_surface(
|
||||||
|
dialog: Window,
|
||||||
|
stage_theme: SandboxTheme,
|
||||||
|
ui_font: Font,
|
||||||
|
primary_button: Button = null,
|
||||||
|
destructive_buttons: Array = [],
|
||||||
|
secondary_buttons: Array = [],
|
||||||
|
default_ok_is_primary: bool = true
|
||||||
|
) -> void:
|
||||||
|
if not dialog.has_meta(OVERLAY_META):
|
||||||
|
return
|
||||||
|
dialog.set_meta(&"stage_dialog_theme", stage_theme)
|
||||||
|
if ui_font != null:
|
||||||
|
dialog.set_meta(&"stage_dialog_font", ui_font)
|
||||||
|
var ok_button: Button = (dialog as AcceptDialog).get_ok_button() if dialog is AcceptDialog else null
|
||||||
|
var cancel_button: Button = (dialog as ConfirmationDialog).get_cancel_button() if dialog is ConfirmationDialog else null
|
||||||
|
var original_message: String = String((dialog as AcceptDialog).dialog_text) if dialog is AcceptDialog else ""
|
||||||
|
var body_label: Label = dialog.get_meta(&"stage_dialog_message") as Label
|
||||||
|
var detail_box: PanelContainer = dialog.get_meta(DETAIL_META) as PanelContainer
|
||||||
|
var detail_label: Label = detail_box.get_child(0) as Label if detail_box != null and detail_box.get_child_count() > 0 else null
|
||||||
|
var note_label: Label = dialog.get_meta(NOTE_META) as Label
|
||||||
|
var body_text: String = original_message
|
||||||
|
var note_text: String = ""
|
||||||
|
var note_marker: int = body_text.find("\n\nℹ️")
|
||||||
|
if note_marker >= 0:
|
||||||
|
note_text = body_text.substr(note_marker + 2)
|
||||||
|
body_text = body_text.substr(0, note_marker)
|
||||||
|
var detail_marker: int = body_text.find("\n\n•")
|
||||||
|
var detail_text: String = ""
|
||||||
|
if detail_marker >= 0:
|
||||||
|
detail_text = body_text.substr(detail_marker + 2)
|
||||||
|
body_text = body_text.substr(0, detail_marker)
|
||||||
|
if body_label != null:
|
||||||
|
body_label.text = body_text
|
||||||
|
body_label.visible = not body_text.is_empty()
|
||||||
|
if detail_label != null and detail_box != null:
|
||||||
|
detail_label.text = detail_text
|
||||||
|
detail_box.visible = not detail_text.is_empty()
|
||||||
|
if note_label != null:
|
||||||
|
note_label.text = note_text
|
||||||
|
note_label.visible = not note_text.is_empty()
|
||||||
|
var heading: Dictionary = _split_heading(dialog.title, String(dialog.get_meta(ICON_META, _icon_for_dialog(dialog))))
|
||||||
|
var title_label: Label = dialog.get_meta(TITLE_META) as Label
|
||||||
|
var icon_label: Label = dialog.get_meta(ICON_LABEL_META) as Label
|
||||||
|
if title_label != null:
|
||||||
|
title_label.text = String(heading.get("title", dialog.title))
|
||||||
|
var font_size: int = stage_theme.integer("popups.confirmation.title_font_size", 28, 20, 40)
|
||||||
|
title_label.add_theme_font_size_override("font_size", font_size)
|
||||||
|
title_label.add_theme_color_override("font_color", stage_theme.color("global.text_color", Color("#F5F7FA")))
|
||||||
|
if ui_font != null:
|
||||||
|
title_label.add_theme_font_override("font", _strong_font(ui_font))
|
||||||
|
if icon_label != null:
|
||||||
|
icon_label.text = String(heading.get("icon", "❔"))
|
||||||
|
var icon_badge: PanelContainer = dialog.get_meta(ICON_BADGE_META) as PanelContainer
|
||||||
|
if icon_badge != null:
|
||||||
|
var heading_title: String = String(heading.get("title", "")).to_lower()
|
||||||
|
var badge_background: Color = Color("#173A4B")
|
||||||
|
var badge_border: Color = Color("#4DB9FF")
|
||||||
|
if String(heading.get("icon", "")).contains("⚠"):
|
||||||
|
badge_background = Color("#493A13")
|
||||||
|
badge_border = Color("#D7AE28")
|
||||||
|
elif String(heading.get("icon", "")).contains("🗑") or "erase" in heading_title or "discard" in heading_title:
|
||||||
|
badge_background = Color("#48262D")
|
||||||
|
badge_border = Color("#E66B75")
|
||||||
|
icon_badge.add_theme_stylebox_override("panel", _icon_box(badge_background, badge_border))
|
||||||
|
var text_color: Color = stage_theme.color("global.text_color", Color("#F5F7FA"))
|
||||||
|
if body_label != null:
|
||||||
|
body_label.add_theme_color_override("font_color", text_color)
|
||||||
|
if detail_label != null:
|
||||||
|
detail_label.add_theme_color_override("font_color", text_color)
|
||||||
|
var extras: Array = dialog.get_meta(EXTRAS_META, []) as Array
|
||||||
|
for extra: Variant in extras:
|
||||||
|
if extra is Control:
|
||||||
|
_apply_control_font(extra as Control, ui_font, stage_theme.integer("popups.confirmation.message_font_size", 20, 16, 32))
|
||||||
|
|
||||||
|
var action_row: HBoxContainer = dialog.get_meta(ACTIONS_META) as HBoxContainer
|
||||||
|
if action_row != null:
|
||||||
|
for old_button: Node in action_row.get_children():
|
||||||
|
action_row.remove_child(old_button)
|
||||||
|
old_button.queue_free()
|
||||||
|
var primary: Button = primary_button
|
||||||
|
if primary == null and default_ok_is_primary and not _contains_button(destructive_buttons, ok_button):
|
||||||
|
primary = ok_button
|
||||||
|
if ok_button != null:
|
||||||
|
ok_button.visible = false
|
||||||
|
if cancel_button != null:
|
||||||
|
cancel_button.visible = false
|
||||||
|
for raw_secondary: Variant in secondary_buttons:
|
||||||
|
if raw_secondary is Button:
|
||||||
|
(raw_secondary as Button).visible = false
|
||||||
|
for raw_button: Variant in destructive_buttons:
|
||||||
|
if raw_button is Button:
|
||||||
|
(raw_button as Button).visible = false
|
||||||
|
var ordered: Array[Button] = []
|
||||||
|
_append_unique_button(ordered, primary)
|
||||||
|
for raw_secondary: Variant in secondary_buttons:
|
||||||
|
if raw_secondary is Button:
|
||||||
|
_append_unique_button(ordered, raw_secondary as Button)
|
||||||
|
for raw_destructive: Variant in destructive_buttons:
|
||||||
|
if raw_destructive is Button:
|
||||||
|
_append_unique_button(ordered, raw_destructive as Button)
|
||||||
|
_append_unique_button(ordered, cancel_button)
|
||||||
|
_append_unique_button(ordered, ok_button)
|
||||||
|
for original: Button in ordered:
|
||||||
|
var visual: Button = Button.new()
|
||||||
|
visual.text = original.text
|
||||||
|
visual.disabled = original.disabled
|
||||||
|
visual.tooltip_text = original.tooltip_text
|
||||||
|
visual.name = "Action_%s" % String(original.name)
|
||||||
|
visual.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||||
|
var role: StringName = &"secondary"
|
||||||
|
if original == primary:
|
||||||
|
role = &"primary"
|
||||||
|
elif _contains_button(destructive_buttons, original):
|
||||||
|
role = &"danger"
|
||||||
|
style_button(visual, role, stage_theme, ui_font)
|
||||||
|
visual.pressed.connect(func() -> void: original.pressed.emit())
|
||||||
|
visual.set_meta(&"stage_dialog_original_button", original)
|
||||||
|
original.set_meta(&"stage_dialog_visual_button", visual)
|
||||||
|
if action_row != null:
|
||||||
|
action_row.add_child(visual)
|
||||||
|
|
||||||
|
|
||||||
|
static func _on_overlay_input(event: InputEvent, dialog: Window) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
|
||||||
|
dismiss_on_escape(dialog)
|
||||||
|
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||||
|
if overlay != null and overlay.get_viewport() != null:
|
||||||
|
overlay.get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
static func _fit_body_scroll(dialog: Window, viewport_size: Vector2i, stage_theme: SandboxTheme) -> void:
|
||||||
|
var body_scroll: ScrollContainer = dialog.get_meta(BODY_SCROLL_META) as ScrollContainer
|
||||||
|
var body: Control = dialog.get_meta(BODY_META) as Control
|
||||||
|
var overlay: Control = dialog.get_meta(OVERLAY_META) as Control
|
||||||
|
var header: Control = overlay.get_node_or_null("CardCenter/DialogCard/DialogContent/DialogHeader") as Control if overlay != null else null
|
||||||
|
var actions: Control = dialog.get_meta(ACTIONS_META) as Control
|
||||||
|
var card: Control = dialog.get_meta(CARD_META) as Control
|
||||||
|
if body_scroll == null or body == null or header == null or actions == null or card == null:
|
||||||
|
return
|
||||||
|
var card_width: float = card.custom_minimum_size.x
|
||||||
|
var card_padding: float = float(stage_theme.integer("popups.confirmation.padding", 22, 12, 36))
|
||||||
|
var content_gap: float = float(stage_theme.integer("popups.confirmation.body_gap", 12, 4, 24))
|
||||||
|
var body_width: float = maxf(0.0, card_width - card_padding * 2.0)
|
||||||
|
var preferred_body_height: float = body.get_combined_minimum_size().y
|
||||||
|
var fixed_height: float = header.get_combined_minimum_size().y + actions.get_combined_minimum_size().y + card_padding * 2.0 + content_gap * 2.0
|
||||||
|
var screen_margin: int = int(dialog.get_meta(SCREEN_MARGIN_META, 18))
|
||||||
|
var max_height_ratio: float = clampf(float(dialog.get_meta(MAX_HEIGHT_RATIO_META, 1.0)), 0.35, 1.0)
|
||||||
|
var available_height: float = maxf(0.0, minf(float(viewport_size.y - screen_margin * 2), float(viewport_size.y) * max_height_ratio))
|
||||||
|
var max_body_height: float = maxf(48.0, available_height - fixed_height)
|
||||||
|
if preferred_body_height > max_body_height:
|
||||||
|
var vertical_bar: VScrollBar = body_scroll.get_v_scroll_bar()
|
||||||
|
if vertical_bar != null:
|
||||||
|
body_width = maxf(0.0, body_width - vertical_bar.get_combined_minimum_size().x)
|
||||||
|
body.custom_minimum_size.x = body_width
|
||||||
|
body.size.x = body_width
|
||||||
|
preferred_body_height = body.get_combined_minimum_size().y
|
||||||
|
body_scroll.custom_minimum_size = Vector2(0.0, minf(preferred_body_height, max_body_height))
|
||||||
|
|
||||||
|
|
||||||
|
static func _find_overlay_host(dialog: Window) -> Node:
|
||||||
|
var ancestor: Node = dialog.get_parent()
|
||||||
|
while ancestor != null:
|
||||||
|
if ancestor is CanvasLayer:
|
||||||
|
return ancestor
|
||||||
|
ancestor = ancestor.get_parent()
|
||||||
|
var tree: SceneTree = dialog.get_tree()
|
||||||
|
var scene: Node = tree.current_scene if tree != null and tree.current_scene != null else (tree.root if tree != null else null)
|
||||||
|
if scene != null:
|
||||||
|
var ui_layer: Node = scene.get_node_or_null("UI")
|
||||||
|
if ui_layer is CanvasLayer:
|
||||||
|
return ui_layer
|
||||||
|
var existing: Node = scene.find_child("StageDialogLayer", true, false)
|
||||||
|
if existing is CanvasLayer:
|
||||||
|
return existing
|
||||||
|
var layer := CanvasLayer.new()
|
||||||
|
layer.name = "StageDialogLayer"
|
||||||
|
layer.layer = 100
|
||||||
|
scene.add_child.call_deferred(layer)
|
||||||
|
return layer
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func _on_native_dialog_closed(dialog: Window) -> void:
|
||||||
|
hide(dialog)
|
||||||
|
|
||||||
|
|
||||||
|
static func _on_native_dialog_custom_action(_action: String, dialog: Window) -> void:
|
||||||
|
hide(dialog)
|
||||||
|
|
||||||
|
|
||||||
|
static func _on_dialog_tree_exiting(dialog: Window, overlay: Control) -> void:
|
||||||
|
if dialog != null and is_instance_valid(dialog):
|
||||||
|
dialog.set_meta(OPEN_META, false)
|
||||||
|
if overlay != null and is_instance_valid(overlay):
|
||||||
|
overlay.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
static func _append_unique_button(buttons: Array[Button], button: Button) -> void:
|
||||||
|
if button != null and not buttons.has(button):
|
||||||
|
buttons.append(button)
|
||||||
|
|
||||||
|
|
||||||
|
static func _contains_button(buttons: Array, button: Button) -> bool:
|
||||||
|
for item: Variant in buttons:
|
||||||
|
if item is Button and item == button:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
static func _split_heading(raw_title: String, fallback_icon: String) -> Dictionary:
|
||||||
|
var icon: String = fallback_icon
|
||||||
|
var title: String = raw_title
|
||||||
|
var space: int = raw_title.find(" ")
|
||||||
|
if space > 0 and space < 5:
|
||||||
|
var possible_icon: String = raw_title.substr(0, space)
|
||||||
|
if possible_icon.length() <= 4 and not possible_icon.is_empty():
|
||||||
|
icon = possible_icon
|
||||||
|
title = raw_title.substr(space + 1).strip_edges()
|
||||||
|
if icon.is_empty():
|
||||||
|
icon = "❔"
|
||||||
|
return {"icon": icon, "title": title}
|
||||||
|
|
||||||
|
|
||||||
|
static func _icon_for_dialog(dialog: Window) -> String:
|
||||||
|
var dialog_name: String = dialog.name.to_lower()
|
||||||
|
if "artwork" in dialog_name or "scenery" in dialog_name:
|
||||||
|
return "🖼️"
|
||||||
|
if "asset" in dialog_name or "error" in dialog_name or "warning" in dialog_name:
|
||||||
|
return "⚠️"
|
||||||
|
if "speech" in dialog_name or "speak" in dialog_name:
|
||||||
|
return "🗣️"
|
||||||
|
if "rename" in dialog_name:
|
||||||
|
return "📝"
|
||||||
|
return "❔"
|
||||||
|
|
||||||
|
|
||||||
|
static func _theme_for(dialog: Window) -> SandboxTheme:
|
||||||
|
var raw: Variant = dialog.get_meta(&"stage_dialog_theme", null)
|
||||||
|
return raw as SandboxTheme if raw is SandboxTheme else THEME_DEFAULTS.new()
|
||||||
|
|
||||||
|
|
||||||
|
static func _apply_control_font(control: Control, ui_font: Font, size: int) -> void:
|
||||||
|
control.add_theme_font_size_override("font_size", size)
|
||||||
|
if ui_font != null:
|
||||||
|
control.add_theme_font_override("font", ui_font)
|
||||||
|
for child: Node in control.get_children():
|
||||||
|
if child is Control:
|
||||||
|
_apply_control_font(child as Control, ui_font, size)
|
||||||
|
|
||||||
|
|
||||||
|
static func _icon_box(background: Color, border: Color) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = background
|
||||||
|
style.border_color = border
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(9)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
static func _detail_box(stage_theme: SandboxTheme) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color("#151C28")
|
||||||
|
style.border_color = Color("#39465B")
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(9)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
static func _button_box(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
|
||||||
|
var style := 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
|
||||||
|
|
||||||
|
|
||||||
|
static func _strong_font(base_font: Font) -> Font:
|
||||||
|
if base_font == null:
|
||||||
|
return ThemeDB.fallback_font
|
||||||
|
var strong_font := FontVariation.new()
|
||||||
|
strong_font.base_font = base_font
|
||||||
|
strong_font.variation_embolden = 0.55
|
||||||
|
return strong_font
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6iflwn2v1lkg
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
class_name StageGroundFinishChooser
|
||||||
|
extends ConfirmationDialog
|
||||||
|
## Modal single-choice gallery for choosing the look of newly painted Ground.
|
||||||
|
|
||||||
|
const DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
|
const GROUND_PREVIEW := preload("res://scripts/shared/stage_ui/stage_ground_finish_preview.gd")
|
||||||
|
const DEFAULT_THEME := preload("res://scripts/stage/sandbox_theme.gd")
|
||||||
|
|
||||||
|
const DEFAULT_PAINT_TINT: Color = Color(0.25, 0.55, 0.25, 1.0)
|
||||||
|
|
||||||
|
var _stage_theme: SandboxTheme
|
||||||
|
var _ui_font: Font
|
||||||
|
var _finish_ids: Array[StringName] = []
|
||||||
|
var _finish_labels: Array[String] = []
|
||||||
|
var _selected_finish: StringName = &""
|
||||||
|
var _button_group: ButtonGroup
|
||||||
|
var _choice_buttons: Dictionary = {}
|
||||||
|
var _choice_previews: Dictionary = {}
|
||||||
|
var _choice_labels: Dictionary = {}
|
||||||
|
var _content_root: VBoxContainer
|
||||||
|
var _layout_row: HBoxContainer
|
||||||
|
var _choice_grid: GridContainer
|
||||||
|
var _preview_panel: PanelContainer
|
||||||
|
var _selected_preview: StageGroundFinishPreview
|
||||||
|
var _choice_height: int = 96
|
||||||
|
var _choice_preview_height: int = 48
|
||||||
|
var _choice_font_size: int = 18
|
||||||
|
var _preview_height: int = 128
|
||||||
|
var _compact_preview_height: int = 88
|
||||||
|
var _preview_min_width: int = 224
|
||||||
|
var _choice_min_width: int = 104
|
||||||
|
var _grid_gap: int = 8
|
||||||
|
var _compact_choice_height: int = 54
|
||||||
|
var _compact_choice_preview_height: int = 22
|
||||||
|
var _compact_choice_font_size: int = 16
|
||||||
|
var _compact_grid_gap: int = 6
|
||||||
|
var _compact_layout_max_width: int = 600
|
||||||
|
var _compact_layout_max_height: int = 460
|
||||||
|
var _wide_layout_min_width: int = 760
|
||||||
|
var _screen_margin: int = 24
|
||||||
|
var _is_wide_layout: bool = true
|
||||||
|
var _is_compact_layout: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
stage_theme: SandboxTheme,
|
||||||
|
ui_font: Font,
|
||||||
|
finish_ids: Array[StringName],
|
||||||
|
finish_labels: Array[String]
|
||||||
|
) -> void:
|
||||||
|
_stage_theme = stage_theme if stage_theme != null else DEFAULT_THEME.new() as SandboxTheme
|
||||||
|
_ui_font = ui_font
|
||||||
|
_finish_ids.assign(finish_ids)
|
||||||
|
_finish_labels.assign(finish_labels)
|
||||||
|
if not _finish_ids.is_empty():
|
||||||
|
_selected_finish = _finish_ids[0]
|
||||||
|
_configure_theme_values()
|
||||||
|
|
||||||
|
|
||||||
|
func open_for_finish(finish_id: StringName) -> void:
|
||||||
|
_select_finish(finish_id)
|
||||||
|
_apply_responsive_layout()
|
||||||
|
DIALOG_STYLER.popup(self)
|
||||||
|
call_deferred("_refit_dialog")
|
||||||
|
call_deferred("_focus_selected_choice")
|
||||||
|
|
||||||
|
|
||||||
|
func selected_finish() -> StringName:
|
||||||
|
return _selected_finish
|
||||||
|
|
||||||
|
|
||||||
|
func choice_buttons() -> Dictionary:
|
||||||
|
return _choice_buttons.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func selected_preview_finish() -> StringName:
|
||||||
|
return &"" if _selected_preview == null else _selected_preview.finish_id
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if _stage_theme == null:
|
||||||
|
_stage_theme = DEFAULT_THEME.new() as SandboxTheme
|
||||||
|
_configure_theme_values()
|
||||||
|
title = "Choose Ground"
|
||||||
|
dialog_text = "Stickmen walk here"
|
||||||
|
cancel_button_text = "Close"
|
||||||
|
ok_button_text = "Draw Grass Ground"
|
||||||
|
theme = _stage_theme.control_theme()
|
||||||
|
set_meta(DIALOG_STYLER.CARD_WIDTH_META, _theme_int("card_width", 880, 420, 1200))
|
||||||
|
set_meta(DIALOG_STYLER.MAX_HEIGHT_RATIO_META, _theme_number("max_height_ratio", 0.86, 0.55, 0.96))
|
||||||
|
set_meta(DIALOG_STYLER.SCREEN_MARGIN_META, _screen_margin)
|
||||||
|
_build_content()
|
||||||
|
DIALOG_STYLER.apply(self, _stage_theme, _ui_font, get_ok_button())
|
||||||
|
var icon_badge: Control = get_meta(DIALOG_STYLER.ICON_BADGE_META, null) as Control
|
||||||
|
if icon_badge != null:
|
||||||
|
icon_badge.hide()
|
||||||
|
_apply_responsive_layout()
|
||||||
|
var viewport: Viewport = _stage_viewport()
|
||||||
|
if viewport != null:
|
||||||
|
viewport.size_changed.connect(_on_viewport_size_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _configure_theme_values() -> void:
|
||||||
|
_choice_font_size = _theme_int("choice_font_size", 18, 14, 26)
|
||||||
|
_choice_height = _theme_int("choice_height", 96, 72, 132)
|
||||||
|
_choice_preview_height = _theme_int("choice_preview_height", 48, 32, 72)
|
||||||
|
_compact_choice_height = _theme_int("compact_choice_height", 50, 48, 72)
|
||||||
|
_compact_choice_preview_height = _theme_int("compact_choice_preview_height", 22, 16, 36)
|
||||||
|
_compact_choice_font_size = _theme_int("compact_choice_font_size", 16, 14, 20)
|
||||||
|
_compact_grid_gap = _theme_int("compact_grid_gap", 6, 4, 12)
|
||||||
|
_compact_layout_max_width = _theme_int("compact_layout_max_width", 600, 440, 760)
|
||||||
|
_compact_layout_max_height = _theme_int("compact_layout_max_height", 460, 360, 640)
|
||||||
|
_preview_height = _theme_int("preview_height", 128, 88, 220)
|
||||||
|
_compact_preview_height = _theme_int("compact_preview_height", 88, 64, 140)
|
||||||
|
_preview_min_width = _theme_int("preview_min_width", 224, 160, 360)
|
||||||
|
_choice_min_width = _theme_int("choice_min_width", 104, 88, 160)
|
||||||
|
_grid_gap = _theme_int("grid_gap", 8, 4, 24)
|
||||||
|
_wide_layout_min_width = _theme_int("wide_layout_min_width", 760, 600, 1100)
|
||||||
|
_screen_margin = _theme_int("screen_margin", 24, 12, 64)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_content() -> void:
|
||||||
|
_button_group = ButtonGroup.new()
|
||||||
|
_button_group.allow_unpress = false
|
||||||
|
_content_root = VBoxContainer.new()
|
||||||
|
_content_root.name = "GroundFinishGallery"
|
||||||
|
_content_root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_content_root.add_theme_constant_override("separation", _grid_gap)
|
||||||
|
_layout_row = HBoxContainer.new()
|
||||||
|
_layout_row.name = "GalleryAndPreview"
|
||||||
|
_layout_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_layout_row.add_theme_constant_override("separation", 16)
|
||||||
|
_choice_grid = GridContainer.new()
|
||||||
|
_choice_grid.name = "GroundFinishChoices"
|
||||||
|
_choice_grid.columns = 3
|
||||||
|
_choice_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_choice_grid.add_theme_constant_override("h_separation", _grid_gap)
|
||||||
|
_choice_grid.add_theme_constant_override("v_separation", _grid_gap)
|
||||||
|
_build_choice_buttons()
|
||||||
|
_preview_panel = _build_selected_preview_panel()
|
||||||
|
_layout_row.add_child(_choice_grid)
|
||||||
|
_layout_row.add_child(_preview_panel)
|
||||||
|
_content_root.add_child(_layout_row)
|
||||||
|
add_child(_content_root)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_choice_buttons() -> void:
|
||||||
|
for index: int in mini(_finish_ids.size(), _finish_labels.size()):
|
||||||
|
var finish_id: StringName = _finish_ids[index]
|
||||||
|
var finish_label: String = _finish_labels[index]
|
||||||
|
var button := Button.new()
|
||||||
|
button.name = "GroundFinish_%s" % String(finish_id).trim_prefix("finish_ground_")
|
||||||
|
button.toggle_mode = true
|
||||||
|
button.button_group = _button_group
|
||||||
|
button.text = ""
|
||||||
|
button.custom_minimum_size = Vector2(float(_choice_min_width), float(_choice_height))
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.focus_mode = Control.FOCUS_ALL
|
||||||
|
_apply_choice_style(button)
|
||||||
|
var contents := VBoxContainer.new()
|
||||||
|
contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
contents.add_theme_constant_override("separation", 4)
|
||||||
|
contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var sample := GROUND_PREVIEW.new() as StageGroundFinishPreview
|
||||||
|
sample.name = "FinishSample"
|
||||||
|
sample.custom_minimum_size.y = float(_choice_preview_height)
|
||||||
|
sample.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
sample.configure(finish_id, DEFAULT_PAINT_TINT)
|
||||||
|
contents.add_child(sample)
|
||||||
|
var label := Label.new()
|
||||||
|
label.name = "FinishLabel"
|
||||||
|
label.text = finish_label
|
||||||
|
label.custom_minimum_size.y = 20.0
|
||||||
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
label.add_theme_font_size_override("font_size", _choice_font_size)
|
||||||
|
label.add_theme_color_override("font_color", _theme_color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||||
|
if _ui_font != null:
|
||||||
|
label.add_theme_font_override("font", _ui_font)
|
||||||
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
contents.add_child(label)
|
||||||
|
button.add_child(contents)
|
||||||
|
button.pressed.connect(_on_choice_pressed.bind(finish_id))
|
||||||
|
_choice_grid.add_child(button)
|
||||||
|
_choice_buttons[finish_id] = button
|
||||||
|
_choice_previews[finish_id] = sample
|
||||||
|
_choice_labels[finish_id] = label
|
||||||
|
|
||||||
|
|
||||||
|
func _build_selected_preview_panel() -> PanelContainer:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.name = "SelectedGroundPreview"
|
||||||
|
panel.custom_minimum_size = Vector2(float(_preview_min_width), 0.0)
|
||||||
|
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.add_theme_stylebox_override("panel", _preview_panel_style())
|
||||||
|
var stack := VBoxContainer.new()
|
||||||
|
stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
stack.add_theme_constant_override("separation", 10)
|
||||||
|
stack.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var heading := Label.new()
|
||||||
|
heading.name = "GroundPreviewHeading"
|
||||||
|
heading.text = "New Ground will look like this"
|
||||||
|
heading.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
heading.add_theme_font_size_override("font_size", 18)
|
||||||
|
heading.add_theme_color_override("font_color", _theme_color("global.text_color", Color("#F5F7FA")))
|
||||||
|
if _ui_font != null:
|
||||||
|
heading.add_theme_font_override("font", _ui_font)
|
||||||
|
stack.add_child(heading)
|
||||||
|
_selected_preview = GROUND_PREVIEW.new() as StageGroundFinishPreview
|
||||||
|
_selected_preview.name = "SelectedGroundFinishSample"
|
||||||
|
_selected_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_selected_preview.custom_minimum_size.y = float(_preview_height)
|
||||||
|
_selected_preview.configure(_selected_finish if not _selected_finish.is_empty() else &"finish_ground_grass", DEFAULT_PAINT_TINT)
|
||||||
|
stack.add_child(_selected_preview)
|
||||||
|
panel.add_child(stack)
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
func _select_finish(finish_id: StringName) -> void:
|
||||||
|
if not _finish_ids.has(finish_id):
|
||||||
|
if _finish_ids.is_empty():
|
||||||
|
return
|
||||||
|
finish_id = _finish_ids[0]
|
||||||
|
_selected_finish = finish_id
|
||||||
|
for key: Variant in _choice_buttons:
|
||||||
|
var button: Button = _choice_buttons[key] as Button
|
||||||
|
button.set_pressed_no_signal(StringName(key) == finish_id)
|
||||||
|
if _selected_preview != null:
|
||||||
|
_selected_preview.configure(finish_id, DEFAULT_PAINT_TINT)
|
||||||
|
_update_primary_button_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_primary_button_text() -> void:
|
||||||
|
var label: String = "Ground"
|
||||||
|
var index: int = _finish_ids.find(_selected_finish)
|
||||||
|
if index >= 0 and index < _finish_labels.size():
|
||||||
|
label = _finish_labels[index]
|
||||||
|
var button: Button = get_ok_button()
|
||||||
|
if button == null:
|
||||||
|
return
|
||||||
|
button.text = "Draw %s Ground" % label
|
||||||
|
var visual_button: Button = DIALOG_STYLER.visual_button(button)
|
||||||
|
if visual_button != null and visual_button != button:
|
||||||
|
visual_button.text = button.text
|
||||||
|
|
||||||
|
|
||||||
|
func _on_choice_pressed(finish_id: StringName) -> void:
|
||||||
|
_select_finish(finish_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_viewport_size_changed() -> void:
|
||||||
|
call_deferred("_apply_responsive_layout")
|
||||||
|
call_deferred("_refit_dialog")
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_responsive_layout() -> void:
|
||||||
|
if _content_root == null or _choice_grid == null or _preview_panel == null or _layout_row == null:
|
||||||
|
return
|
||||||
|
var viewport: Viewport = _stage_viewport()
|
||||||
|
if viewport == null:
|
||||||
|
return
|
||||||
|
var viewport_size: Vector2 = viewport.get_visible_rect().size
|
||||||
|
var viewport_width: float = viewport_size.x
|
||||||
|
var card_width: float = minf(
|
||||||
|
float(_theme_int("card_width", 880, 420, 1200)),
|
||||||
|
maxf(0.0, viewport_width - float(_screen_margin * 2))
|
||||||
|
)
|
||||||
|
var body_width: float = maxf(0.0, card_width - 64.0)
|
||||||
|
var wide: bool = viewport_width >= float(_wide_layout_min_width) and body_width >= float(_preview_min_width + _choice_min_width * 3 + _grid_gap * 4)
|
||||||
|
var compact: bool = viewport_width < float(_compact_layout_max_width) or viewport_size.y < float(_compact_layout_max_height)
|
||||||
|
var columns: int = 3 if body_width >= float(_choice_min_width * 3 + _grid_gap * 2) else 2
|
||||||
|
_choice_grid.columns = columns
|
||||||
|
_is_wide_layout = wide
|
||||||
|
_is_compact_layout = compact
|
||||||
|
var card: Control = get_meta(DIALOG_STYLER.CARD_META) as Control
|
||||||
|
if card != null:
|
||||||
|
set_meta(DIALOG_STYLER.MAX_HEIGHT_RATIO_META, _theme_number("compact_height_ratio", 0.89, 0.70, 0.89) if compact else _theme_number("max_height_ratio", 0.86, 0.55, 0.86))
|
||||||
|
_apply_choice_density(compact)
|
||||||
|
if wide:
|
||||||
|
if _choice_grid.get_parent() != _layout_row:
|
||||||
|
if _choice_grid.get_parent() == _content_root:
|
||||||
|
_content_root.remove_child(_choice_grid)
|
||||||
|
if _preview_panel.get_parent() == _content_root:
|
||||||
|
_content_root.remove_child(_preview_panel)
|
||||||
|
_layout_row.add_child(_choice_grid)
|
||||||
|
_layout_row.add_child(_preview_panel)
|
||||||
|
_content_root.add_child(_layout_row)
|
||||||
|
_preview_panel.visible = true
|
||||||
|
_preview_panel.custom_minimum_size.x = float(_preview_min_width)
|
||||||
|
_selected_preview.custom_minimum_size.y = float(_preview_height)
|
||||||
|
else:
|
||||||
|
if _choice_grid.get_parent() != _content_root:
|
||||||
|
if _choice_grid.get_parent() == _layout_row:
|
||||||
|
_layout_row.remove_child(_choice_grid)
|
||||||
|
if _preview_panel.get_parent() == _layout_row:
|
||||||
|
_layout_row.remove_child(_preview_panel)
|
||||||
|
if _layout_row.get_parent() == _content_root:
|
||||||
|
_content_root.remove_child(_layout_row)
|
||||||
|
_content_root.add_child(_preview_panel)
|
||||||
|
_content_root.add_child(_choice_grid)
|
||||||
|
_preview_panel.visible = not compact
|
||||||
|
_preview_panel.custom_minimum_size.x = 0.0
|
||||||
|
_selected_preview.custom_minimum_size.y = float(_compact_preview_height if compact else _preview_height)
|
||||||
|
_preview_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_choice_grid.add_theme_constant_override("v_separation", _compact_grid_gap if compact else _grid_gap)
|
||||||
|
_content_root.queue_sort()
|
||||||
|
|
||||||
|
|
||||||
|
func _refit_dialog() -> void:
|
||||||
|
if DIALOG_STYLER.is_open(self):
|
||||||
|
DIALOG_STYLER.refit(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _stage_viewport() -> Viewport:
|
||||||
|
var overlay: Control = get_meta(DIALOG_STYLER.OVERLAY_META, null) as Control
|
||||||
|
if overlay != null and is_instance_valid(overlay):
|
||||||
|
return overlay.get_viewport()
|
||||||
|
return get_viewport()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_choice_density(compact: bool) -> void:
|
||||||
|
var target_height: float = float(_compact_choice_height if compact else _choice_height)
|
||||||
|
var target_preview_height: float = float(_compact_choice_preview_height if compact else _choice_preview_height)
|
||||||
|
var target_font_size: int = _compact_choice_font_size if compact else _choice_font_size
|
||||||
|
for finish_id: StringName in _choice_buttons:
|
||||||
|
var button: Button = _choice_buttons[finish_id] as Button
|
||||||
|
button.custom_minimum_size.y = target_height
|
||||||
|
_apply_choice_style(button)
|
||||||
|
var sample: StageGroundFinishPreview = _choice_previews[finish_id] as StageGroundFinishPreview
|
||||||
|
if sample != null:
|
||||||
|
sample.custom_minimum_size.y = target_preview_height
|
||||||
|
var label: Label = _choice_labels[finish_id] as Label
|
||||||
|
if label != null:
|
||||||
|
label.add_theme_font_size_override("font_size", target_font_size)
|
||||||
|
label.custom_minimum_size.y = 18.0 if compact else 20.0
|
||||||
|
|
||||||
|
|
||||||
|
func _focus_selected_choice() -> void:
|
||||||
|
if not is_inside_tree() or not DIALOG_STYLER.is_open(self):
|
||||||
|
return
|
||||||
|
var button: Button = _choice_buttons.get(_selected_finish) as Button
|
||||||
|
if button != null and button.visible:
|
||||||
|
button.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_choice_style(button: Button) -> void:
|
||||||
|
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"))
|
||||||
|
var accent: Color = _theme_color("mode_colors.edit", Color("#22C6FF"))
|
||||||
|
button.add_theme_stylebox_override("normal", _choice_box(neutral, border, 2))
|
||||||
|
button.add_theme_stylebox_override("hover", _choice_box(hover, accent, 2))
|
||||||
|
button.add_theme_stylebox_override("pressed", _choice_box(selected, accent, 3))
|
||||||
|
button.add_theme_stylebox_override("hover_pressed", _choice_box(selected.lightened(0.06), accent, 3))
|
||||||
|
button.add_theme_stylebox_override("focus", _choice_box(Color.TRANSPARENT, Color("#F5F7FA"), 3))
|
||||||
|
|
||||||
|
|
||||||
|
func _choice_box(background: Color, border: Color, border_width: 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(_theme_int("choice_corner_radius", 12, 8, 24))
|
||||||
|
style.content_margin_left = 8.0
|
||||||
|
style.content_margin_right = 8.0
|
||||||
|
var vertical_padding: float = 2.0 if _is_compact_layout else 6.0
|
||||||
|
style.content_margin_top = vertical_padding
|
||||||
|
style.content_margin_bottom = vertical_padding
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _preview_panel_style() -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = _theme_color("preview_surface", Color("#151C28"))
|
||||||
|
style.border_color = _theme_color("preview_border", Color("#39465B"))
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(_theme_int("preview_corner_radius", 12, 8, 24))
|
||||||
|
style.content_margin_left = 12.0
|
||||||
|
style.content_margin_right = 12.0
|
||||||
|
style.content_margin_top = 12.0
|
||||||
|
style.content_margin_bottom = 12.0
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _theme_int(key: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||||
|
return fallback if _stage_theme == null else _stage_theme.integer("popups.ground_finish_chooser." + key, fallback, minimum, maximum)
|
||||||
|
|
||||||
|
|
||||||
|
func _theme_number(key: String, fallback: float, minimum: float, maximum: float) -> float:
|
||||||
|
return fallback if _stage_theme == null else _stage_theme.number("popups.ground_finish_chooser." + key, fallback, minimum, maximum)
|
||||||
|
|
||||||
|
|
||||||
|
func _theme_color(key: String, fallback: Color) -> Color:
|
||||||
|
return fallback if _stage_theme == null else _stage_theme.color("popups.ground_finish_chooser." + key, fallback)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dfo24vsra2cye
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class_name StageGroundFinishPreview
|
||||||
|
extends Control
|
||||||
|
## Render-only Ground finish sample using the same motifs as Stage terrain.
|
||||||
|
|
||||||
|
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||||
|
|
||||||
|
const DEFAULT_PAINT_TINT: Color = Color(0.25, 0.55, 0.25, 1.0)
|
||||||
|
|
||||||
|
var finish_id: StringName = &"finish_ground_grass"
|
||||||
|
var paint_tint: Color = DEFAULT_PAINT_TINT
|
||||||
|
|
||||||
|
|
||||||
|
func configure(p_finish_id: StringName, p_paint_tint: Color = DEFAULT_PAINT_TINT) -> void:
|
||||||
|
finish_id = p_finish_id
|
||||||
|
paint_tint = p_paint_tint
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var preview_top: float = 2.0
|
||||||
|
var motif_scale := Vector2.ONE
|
||||||
|
if finish_id == &"finish_ground_grass":
|
||||||
|
preview_top = minf(12.0, maxf(2.0, size.y * 0.48))
|
||||||
|
motif_scale.y = maxf(1.0, 12.0 / maxf(1.0, preview_top - 1.0))
|
||||||
|
var rect := Rect2(Vector2(2.0, preview_top), Vector2(size.x - 4.0, size.y - preview_top - 2.0))
|
||||||
|
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||||
|
return
|
||||||
|
SURFACE_RENDERER.draw_ground(self, rect, finish_id, paint_tint, Vector2.ZERO, motif_scale)
|
||||||
|
draw_rect(rect, Color("#111821"), false, 2.0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cgh4be2rdultd
|
||||||
@@ -150,8 +150,13 @@ func open(
|
|||||||
entries: Array[Dictionary],
|
entries: Array[Dictionary],
|
||||||
current_sky_id: String = "",
|
current_sky_id: String = "",
|
||||||
current_background_kind: String = "sky",
|
current_background_kind: String = "sky",
|
||||||
plain_color: Color = Color("#F5EFE0")
|
plain_color: Color = Color("#F5EFE0"),
|
||||||
|
browse_state: Dictionary = {}
|
||||||
) -> void:
|
) -> void:
|
||||||
|
var restore_scenery_browse_state: bool = p_kind == "scenery" and not browse_state.is_empty()
|
||||||
|
var restored_gallery_tab: String = String(browse_state.get("gallery_tab", "backgrounds")) if restore_scenery_browse_state else "backgrounds"
|
||||||
|
var restored_filter_id: String = String(browse_state.get("tag_id", "all")) if restore_scenery_browse_state else "all"
|
||||||
|
var restored_scroll: int = int(browse_state.get("scroll_vertical", 0)) if restore_scenery_browse_state else 0
|
||||||
_cancel_plain_picker_preview(false)
|
_cancel_plain_picker_preview(false)
|
||||||
kind = p_kind
|
kind = p_kind
|
||||||
_setup_kind = &""
|
_setup_kind = &""
|
||||||
@@ -164,28 +169,62 @@ func open(
|
|||||||
_current_sky_id = current_sky_id if kind == "scenery" else ""
|
_current_sky_id = current_sky_id if kind == "scenery" else ""
|
||||||
_current_background_kind = &"plain" if current_background_kind == "plain" else &"sky"
|
_current_background_kind = &"plain" if current_background_kind == "plain" else &"sky"
|
||||||
_current_plain_color = _opaque_color(plain_color)
|
_current_plain_color = _opaque_color(plain_color)
|
||||||
_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"))
|
_title_label.text = "Choose a Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop"))
|
||||||
_backgrounds_back_button.visible = false
|
_backgrounds_back_button.visible = false
|
||||||
_apply_chooser_visual_style()
|
_apply_chooser_visual_style()
|
||||||
_tabs.visible = kind == "scenery"
|
_tabs.visible = kind == "scenery"
|
||||||
_filter_chips.visible = false
|
_filter_chips.visible = false
|
||||||
_make_artwork_button.visible = false
|
_make_artwork_button.visible = false
|
||||||
_edit_scenery_button.visible = false
|
_edit_scenery_button.visible = false
|
||||||
_browse_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery"
|
_browse_button.visible = false
|
||||||
_refresh_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery"
|
_refresh_button.visible = false
|
||||||
_empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props found. Browse for a .prop file." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen found."))
|
_empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props in the library yet." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen in the library yet."))
|
||||||
_gallery_tab = "backgrounds"
|
_gallery_tab = restored_gallery_tab
|
||||||
_tag_id = "all"
|
_tag_id = restored_filter_id
|
||||||
_backgrounds_tab.set_pressed_no_signal(true)
|
_backgrounds_tab.set_pressed_no_signal(_gallery_tab == "backgrounds")
|
||||||
_artwork_tab.set_pressed_no_signal(false)
|
_artwork_tab.set_pressed_no_signal(_gallery_tab == "artwork")
|
||||||
set_entries(entries)
|
set_entries(entries)
|
||||||
if kind == "scenery":
|
if kind == "scenery":
|
||||||
_set_gallery_tab("backgrounds")
|
_set_gallery_tab(_gallery_tab)
|
||||||
popup_centered(size)
|
popup_centered(size)
|
||||||
|
call_deferred("_settle_popup_after_open")
|
||||||
|
if restore_scenery_browse_state:
|
||||||
|
call_deferred("_restore_browse_scroll_after_open", restored_scroll, restored_gallery_tab)
|
||||||
|
|
||||||
|
|
||||||
|
func capture_browse_state() -> Dictionary:
|
||||||
|
if kind != "scenery":
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"gallery_tab": _gallery_tab,
|
||||||
|
"tag_id": _tag_id,
|
||||||
|
"scroll_vertical": _grid_scroll.scroll_vertical if _grid_scroll != null else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _restore_browse_scroll_after_open(scroll_vertical: int, gallery_tab: String) -> void:
|
||||||
|
await get_tree().process_frame
|
||||||
|
if not is_inside_tree() or not visible or kind != "scenery" or _gallery_tab != gallery_tab:
|
||||||
|
return
|
||||||
|
_grid_scroll.scroll_vertical = maxi(0, scroll_vertical)
|
||||||
|
|
||||||
|
|
||||||
|
func _settle_popup_after_open() -> void:
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
if is_inside_tree() and visible:
|
||||||
|
# PopupPanel can resize after the first native layout pass. Reapply the
|
||||||
|
# content-sized width once child minimum sizes and DPI scaling have settled.
|
||||||
|
var size_before_settle: Vector2i = size
|
||||||
|
_update_popup_layout(true)
|
||||||
|
if size != size_before_settle:
|
||||||
|
popup_centered(size)
|
||||||
|
|
||||||
|
|
||||||
func set_entries(entries: Array[Dictionary]) -> void:
|
func set_entries(entries: Array[Dictionary]) -> void:
|
||||||
_entries = entries
|
_entries = entries
|
||||||
|
if _grid_scroll != null:
|
||||||
|
_grid_scroll.scroll_vertical = 0
|
||||||
if kind == "scenery":
|
if kind == "scenery":
|
||||||
_rebuild_artwork_filter_chips()
|
_rebuild_artwork_filter_chips()
|
||||||
_thumbnails.clear()
|
_thumbnails.clear()
|
||||||
@@ -881,8 +920,8 @@ func _set_gallery_tab(tab: String) -> void:
|
|||||||
_filter_chips.visible = kind == "scenery" and tab == "artwork"
|
_filter_chips.visible = kind == "scenery" and tab == "artwork"
|
||||||
_make_artwork_button.visible = kind == "scenery" and tab == "artwork"
|
_make_artwork_button.visible = kind == "scenery" and tab == "artwork"
|
||||||
_edit_scenery_button.visible = kind == "scenery"
|
_edit_scenery_button.visible = kind == "scenery"
|
||||||
_browse_button.visible = kind == "stickman" or kind == "prop"
|
_browse_button.visible = false
|
||||||
_refresh_button.visible = kind == "stickman" or kind == "prop"
|
_refresh_button.visible = false
|
||||||
_empty_label.text = "No artwork here yet." if tab == "artwork" else "Choose a Sky look or wall finish."
|
_empty_label.text = "No artwork here yet." if tab == "artwork" else "Choose a Sky look or wall finish."
|
||||||
_page = 0
|
_page = 0
|
||||||
_update_popup_layout(true)
|
_update_popup_layout(true)
|
||||||
@@ -953,6 +992,7 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
|
|||||||
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
|
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
|
||||||
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
|
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
|
||||||
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
|
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
|
||||||
|
var asset_scrolling_gallery: bool = _uses_scrolling_asset_gallery() and not in_subview
|
||||||
var filtered: Array[Dictionary] = _filtered_entries()
|
var filtered: Array[Dictionary] = _filtered_entries()
|
||||||
_tabs.visible = kind == "scenery" and not in_subview
|
_tabs.visible = kind == "scenery" and not in_subview
|
||||||
_filter_chips.visible = scenery_artwork_gallery
|
_filter_chips.visible = scenery_artwork_gallery
|
||||||
@@ -972,17 +1012,17 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
|
|||||||
_empty_label.visible = filtered.is_empty() and not in_subview
|
_empty_label.visible = filtered.is_empty() and not in_subview
|
||||||
_grid.visible = not filtered.is_empty() and not in_subview
|
_grid.visible = not filtered.is_empty() and not in_subview
|
||||||
_grid_scroll.visible = not filtered.is_empty() or in_subview
|
_grid_scroll.visible = not filtered.is_empty() or in_subview
|
||||||
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery or in_subview else BoxContainer.ALIGNMENT_CENTER
|
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery or asset_scrolling_gallery or in_subview else BoxContainer.ALIGNMENT_CENTER
|
||||||
_footer.add_theme_constant_override(
|
_footer.add_theme_constant_override(
|
||||||
"separation",
|
"separation",
|
||||||
_theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_subview or kind == "scenery" else 8
|
_theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_subview or kind == "scenery" else 8
|
||||||
)
|
)
|
||||||
var available: Vector2 = _usable_screen_size()
|
var available: Vector2 = _usable_screen_size()
|
||||||
var width_ratio: float = 0.98 if in_subview else (0.96 if kind == "starters" else 0.94)
|
var width_ratio: float = 0.98 if in_subview or asset_scrolling_gallery else (0.96 if kind == "starters" else 0.94)
|
||||||
var max_width: float = maxf(1.0, minf(available.x * width_ratio, available.x - POPUP_SAFE_SCREEN_MARGIN))
|
var max_width: float = maxf(1.0, minf(available.x * width_ratio, available.x - POPUP_SAFE_SCREEN_MARGIN))
|
||||||
var height_ratio: float = _theme_number("popups.asset_selector.starter_viewport_height_ratio", 0.96, 0.7, 0.98) if kind == "starters" else 0.86
|
var height_ratio: float = _theme_number("popups.asset_selector.starter_viewport_height_ratio", 0.96, 0.7, 0.98) if kind == "starters" else 0.86
|
||||||
var max_height: float = maxf(1.0, minf(available.y * height_ratio, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0))
|
var max_height: float = maxf(1.0, minf(available.y * height_ratio, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0))
|
||||||
var preferred_width: float = CELL_MIN_SIZE.x * (2 if kind == "starters" else COLUMNS) + GRID_H_SEPARATION * ((2 if kind == "starters" else COLUMNS) - 1) + POPUP_SIDE_PADDING
|
var preferred_width: float = _cell_min_size.x * (2 if kind == "starters" else COLUMNS) + GRID_H_SEPARATION * ((2 if kind == "starters" else COLUMNS) - 1) + POPUP_SIDE_PADDING
|
||||||
var preferred_height: float = max_height
|
var preferred_height: float = max_height
|
||||||
if kind == "starters":
|
if kind == "starters":
|
||||||
preferred_width = _theme_number("popups.asset_selector.starter_dialog_width", 1160.0, 440.0, 2400.0)
|
preferred_width = _theme_number("popups.asset_selector.starter_dialog_width", 1160.0, 440.0, 2400.0)
|
||||||
@@ -993,14 +1033,14 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
|
|||||||
var popup_width: float = minf(preferred_width, max_width)
|
var popup_width: float = minf(preferred_width, max_width)
|
||||||
_position_footer(popup_width)
|
_position_footer(popup_width)
|
||||||
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
|
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
|
||||||
var minimum_card_width: float = CELL_MIN_SIZE.x
|
var minimum_card_width: float = _cell_min_size.x
|
||||||
if kind == "starters":
|
if kind == "starters":
|
||||||
minimum_card_width = _theme_number("popups.asset_selector.starter_card_min_width", 340.0, 220.0, 800.0)
|
minimum_card_width = _theme_number("popups.asset_selector.starter_card_min_width", 340.0, 220.0, 800.0)
|
||||||
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (minimum_card_width + GRID_H_SEPARATION)), 1, 2 if kind == "starters" else COLUMNS)
|
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (minimum_card_width + GRID_H_SEPARATION)), 1, 2 if kind == "starters" else COLUMNS)
|
||||||
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery or kind == "starters" else _active_columns * ROWS
|
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery or asset_scrolling_gallery or kind == "starters" else _active_columns * ROWS
|
||||||
_grid.columns = _active_columns
|
_grid.columns = _active_columns
|
||||||
var available_card_width: float = (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns)
|
var available_card_width: float = (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns)
|
||||||
var card_width: float = available_card_width if kind == "starters" else minf(CELL_MIN_SIZE.x, available_card_width)
|
var card_width: float = available_card_width if kind == "starters" else minf(_cell_min_size.x, available_card_width)
|
||||||
var card_height_limit: float = 248.0 if kind != "starters" else INF
|
var card_height_limit: float = 248.0 if kind != "starters" else INF
|
||||||
var card_height: float
|
var card_height: float
|
||||||
if kind == "starters":
|
if kind == "starters":
|
||||||
@@ -1128,6 +1168,7 @@ func _rebuild() -> void:
|
|||||||
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
|
var in_subview: bool = in_setup or in_color_picker or in_plain_choices
|
||||||
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
|
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_subview
|
||||||
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
|
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
|
||||||
|
var asset_scrolling_gallery: bool = _uses_scrolling_asset_gallery() and not in_subview
|
||||||
_position_footer(float(size.x))
|
_position_footer(float(size.x))
|
||||||
_sync_stage_preview_dimming()
|
_sync_stage_preview_dimming()
|
||||||
_close_button.visible = not in_setup and not in_color_picker
|
_close_button.visible = not in_setup and not in_color_picker
|
||||||
@@ -1144,8 +1185,8 @@ func _rebuild() -> void:
|
|||||||
_color_use_button.visible = in_color_picker
|
_color_use_button.visible = in_color_picker
|
||||||
_edit_scenery_button.visible = kind == "scenery" and not in_subview
|
_edit_scenery_button.visible = kind == "scenery" and not in_subview
|
||||||
_make_artwork_button.visible = scenery_artwork_gallery
|
_make_artwork_button.visible = scenery_artwork_gallery
|
||||||
_browse_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
|
_browse_button.visible = false
|
||||||
_refresh_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
|
_refresh_button.visible = false
|
||||||
_footer_spacer.visible = scenery_artwork_gallery
|
_footer_spacer.visible = scenery_artwork_gallery
|
||||||
_empty_label.visible = filtered.is_empty() and not in_subview
|
_empty_label.visible = filtered.is_empty() and not in_subview
|
||||||
_grid.visible = not filtered.is_empty() and not in_subview
|
_grid.visible = not filtered.is_empty() and not in_subview
|
||||||
@@ -1255,6 +1296,10 @@ func _is_scenery_artwork_gallery() -> bool:
|
|||||||
return kind == "scenery" and _gallery_tab == "artwork"
|
return kind == "scenery" and _gallery_tab == "artwork"
|
||||||
|
|
||||||
|
|
||||||
|
func _uses_scrolling_asset_gallery() -> bool:
|
||||||
|
return kind == "prop" or kind == "stickman"
|
||||||
|
|
||||||
|
|
||||||
func _count_scene_background_entries(entries: Array[Dictionary]) -> int:
|
func _count_scene_background_entries(entries: Array[Dictionary]) -> int:
|
||||||
var count: int = 0
|
var count: int = 0
|
||||||
for entry: Dictionary in entries:
|
for entry: Dictionary in entries:
|
||||||
@@ -1324,7 +1369,7 @@ func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> C
|
|||||||
card_payload["status"] = "Current background"
|
card_payload["status"] = "Current background"
|
||||||
card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF"))
|
card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF"))
|
||||||
cell.configure(card_payload, _thumbnails.get(key, null), null)
|
cell.configure(card_payload, _thumbnails.get(key, null), null)
|
||||||
if kind == "scenery" or kind == "starters":
|
if kind == "scenery" or kind == "starters" or _uses_scrolling_asset_gallery():
|
||||||
var card_button: Button = cell as Button
|
var card_button: Button = cell as Button
|
||||||
var card_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5)
|
var card_border_width: int = _theme_integer("popups.asset_selector.button_border_width", 2, 1, 5)
|
||||||
var card_corner_radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
|
var card_corner_radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
|
||||||
@@ -1351,13 +1396,12 @@ func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> C
|
|||||||
card_hover_border_width,
|
card_hover_border_width,
|
||||||
card_corner_radius
|
card_corner_radius
|
||||||
))
|
))
|
||||||
if kind == "starters":
|
card_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(
|
||||||
card_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(
|
card_selected_surface.lightened(0.05),
|
||||||
card_selected_surface.lightened(0.05),
|
_theme_color("mode_colors.edit", Color("#22C6FF")),
|
||||||
_theme_color("mode_colors.edit", Color("#22C6FF")),
|
card_hover_border_width,
|
||||||
card_hover_border_width,
|
card_corner_radius
|
||||||
card_corner_radius
|
))
|
||||||
))
|
|
||||||
var focus_color: Color = _theme_color("stage_panels.type.text_color", Color("#F5F7FA")) if kind == "starters" else _theme_color("mode_colors.edit", Color("#22C6FF"))
|
var focus_color: Color = _theme_color("stage_panels.type.text_color", Color("#F5F7FA")) if kind == "starters" else _theme_color("mode_colors.edit", Color("#22C6FF"))
|
||||||
var focus_style: StyleBoxFlat = _make_rounded_style(
|
var focus_style: StyleBoxFlat = _make_rounded_style(
|
||||||
Color(0, 0, 0, 0),
|
Color(0, 0, 0, 0),
|
||||||
@@ -1447,7 +1491,7 @@ func _apply_chooser_visual_style() -> void:
|
|||||||
frame_style.set_corner_radius_all(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32))
|
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)
|
preview_frame.add_theme_stylebox_override("panel", frame_style)
|
||||||
if _title_label != null:
|
if _title_label != null:
|
||||||
var title_size: int = _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48) if kind == "scenery" else (_theme_integer("popups.asset_selector.starter_title_size", 30, 22, 48) if kind == "starters" else 24)
|
var title_size: int = _theme_integer("popups.asset_selector.starter_title_size", 30, 22, 48) if kind == "starters" else _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48)
|
||||||
_title_label.add_theme_font_size_override("font_size", title_size)
|
_title_label.add_theme_font_size_override("font_size", title_size)
|
||||||
_title_label.add_theme_color_override("font_color", text_color)
|
_title_label.add_theme_color_override("font_color", text_color)
|
||||||
_apply_emphasis_font(_title_label)
|
_apply_emphasis_font(_title_label)
|
||||||
|
|||||||
@@ -6,24 +6,31 @@ const SCHEMA_VERSION: String = "2.0"
|
|||||||
const DEFAULT_PATH: String = "res://sandbox_theme.json"
|
const DEFAULT_PATH: String = "res://sandbox_theme.json"
|
||||||
const CONFIRMATION_DEFAULTS: Dictionary = {
|
const CONFIRMATION_DEFAULTS: Dictionary = {
|
||||||
"scenery_erase_size": [520, 208],
|
"scenery_erase_size": [520, 208],
|
||||||
"padding": 18,
|
"padding": 22,
|
||||||
"body_gap": 10,
|
"body_gap": 12,
|
||||||
"footer_gap": 12,
|
"footer_gap": 12,
|
||||||
"corner_radius": 16,
|
"corner_radius": 18,
|
||||||
"border_width": 2,
|
"border_width": 2,
|
||||||
"title_font_size": 26,
|
"title_font_size": 28,
|
||||||
"message_font_size": 20,
|
"message_font_size": 20,
|
||||||
"button_font_size": 20,
|
"button_font_size": 20,
|
||||||
"button_height": 48,
|
"button_height": 52,
|
||||||
|
"button_min_width": 112,
|
||||||
|
"card_width": 560,
|
||||||
"button_radius": 12,
|
"button_radius": 12,
|
||||||
"button_border_width": 2,
|
"button_border_width": 2,
|
||||||
"remove_button_width": 170,
|
"remove_button_width": 170,
|
||||||
"keep_button_width": 132,
|
"keep_button_width": 160,
|
||||||
"surface_color": "#202633FA",
|
"surface_color": "#202633FA",
|
||||||
"border_color": "#22C6FF",
|
"border_color": "#667386",
|
||||||
"accent_color": "#22C6FF",
|
"secondary_surface": "#293242",
|
||||||
|
"secondary_border": "#718096",
|
||||||
|
"focus_color": "#8BD9FF",
|
||||||
|
"backdrop_color": "#080D15B8",
|
||||||
|
"accent_color": "#8BD9FF",
|
||||||
"action_color": "#087DBA",
|
"action_color": "#087DBA",
|
||||||
"danger_color": "#FF626B",
|
"danger_color": "#E66B75",
|
||||||
|
"danger_surface": "#44262D",
|
||||||
"danger_hover_surface": "#3B252C",
|
"danger_hover_surface": "#3B252C",
|
||||||
"danger_pressed_surface": "#4A252D"
|
"danger_pressed_surface": "#4A252D"
|
||||||
}
|
}
|
||||||
@@ -35,7 +42,7 @@ const DEFAULT_CONFIG: Dictionary = {
|
|||||||
"mode_colors": {"edit": "#22C6FF", "direct": "#FFB300", "play": "#33DD77", "guide": "#22C6FF"},
|
"mode_colors": {"edit": "#22C6FF", "direct": "#FFB300", "play": "#33DD77", "guide": "#22C6FF"},
|
||||||
"effects": {"impact_wobble": {"duration": 0.48, "maximum_angle_degrees": 7.0, "sideways_offset": 4.0}},
|
"effects": {"impact_wobble": {"duration": 0.48, "maximum_angle_degrees": 7.0, "sideways_offset": 4.0}},
|
||||||
"grid": {"snap_size": 15.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}},
|
"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":[560,300], "font_size":22, "message_font_size":20}},
|
||||||
"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"}},
|
"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},
|
"palette": {"style_defaults": {}, "dock_width": 116, "card_size": [96, 94], "icon_size": 30, "drag_threshold": 8, "selected_border_width": 3},
|
||||||
"stage_panels": {
|
"stage_panels": {
|
||||||
@@ -118,6 +125,36 @@ func _apply_phase19_defaults() -> void:
|
|||||||
if not asset_selector.has(key):
|
if not asset_selector.has(key):
|
||||||
asset_selector[key] = starter_defaults[key]
|
asset_selector[key] = starter_defaults[key]
|
||||||
popups["asset_selector"] = asset_selector
|
popups["asset_selector"] = asset_selector
|
||||||
|
var ground_finish_defaults: Dictionary = {
|
||||||
|
"card_width": 880,
|
||||||
|
"max_height_ratio": 0.86,
|
||||||
|
"screen_margin": 24,
|
||||||
|
"choice_height": 96,
|
||||||
|
"choice_preview_height": 48,
|
||||||
|
"choice_font_size": 18,
|
||||||
|
"choice_min_width": 104,
|
||||||
|
"choice_corner_radius": 12,
|
||||||
|
"compact_choice_height": 50,
|
||||||
|
"compact_choice_preview_height": 22,
|
||||||
|
"compact_choice_font_size": 16,
|
||||||
|
"compact_grid_gap": 6,
|
||||||
|
"compact_layout_max_width": 600,
|
||||||
|
"compact_layout_max_height": 460,
|
||||||
|
"compact_height_ratio": 0.89,
|
||||||
|
"preview_height": 128,
|
||||||
|
"compact_preview_height": 88,
|
||||||
|
"preview_min_width": 224,
|
||||||
|
"preview_corner_radius": 12,
|
||||||
|
"preview_surface": "#151C28",
|
||||||
|
"preview_border": "#39465B",
|
||||||
|
"grid_gap": 8,
|
||||||
|
"wide_layout_min_width": 760,
|
||||||
|
}
|
||||||
|
var ground_finish: Dictionary = popups.get("ground_finish_chooser", {}).duplicate(true)
|
||||||
|
for key: String in ground_finish_defaults:
|
||||||
|
if not ground_finish.has(key):
|
||||||
|
ground_finish[key] = ground_finish_defaults[key]
|
||||||
|
popups["ground_finish_chooser"] = ground_finish
|
||||||
_config["popups"] = popups
|
_config["popups"] = popups
|
||||||
var overlays: Dictionary = _config.get("overlays", {}).duplicate(true)
|
var overlays: Dictionary = _config.get("overlays", {}).duplicate(true)
|
||||||
var gizmos: Dictionary = overlays.get("gizmos", {}).duplicate(true)
|
var gizmos: Dictionary = overlays.get("gizmos", {}).duplicate(true)
|
||||||
@@ -136,7 +173,9 @@ func _apply_phase19_defaults() -> void:
|
|||||||
|
|
||||||
func _apply_confirmation_defaults() -> void:
|
func _apply_confirmation_defaults() -> void:
|
||||||
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
|
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
|
||||||
popups["confirmation"] = CONFIRMATION_DEFAULTS.duplicate(true)
|
var configured: Variant = popups.get("confirmation", {})
|
||||||
|
var overrides: Dictionary = configured as Dictionary if configured is Dictionary else {}
|
||||||
|
popups["confirmation"] = _deep_merge(CONFIRMATION_DEFAULTS, overrides)
|
||||||
_config["popups"] = popups
|
_config["popups"] = popups
|
||||||
|
|
||||||
func raw_config() -> Dictionary:
|
func raw_config() -> Dictionary:
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const EDITOR_VISUAL_THEME := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||||
|
const EDITOR_CONTEXT_MENU := preload("res://scripts/shared/editor_ui/editor_context_menu.gd")
|
||||||
|
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
|
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||||
|
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
|
||||||
|
|
||||||
|
var _checks: int = 0
|
||||||
|
var _failures: int = 0
|
||||||
|
var _last_pressed_id: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1280, 800)
|
||||||
|
root.gui_embed_subwindows = true
|
||||||
|
var visual_theme: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||||
|
check("shared editor theme loads", visual_theme.load_from(EDITOR_VISUAL_THEME.DEFAULT_PATH))
|
||||||
|
|
||||||
|
var menu: EditorContextMenu = EDITOR_CONTEXT_MENU.new()
|
||||||
|
root.add_child(menu)
|
||||||
|
menu.configure(visual_theme, &"prop")
|
||||||
|
menu.add_item("Copy Shape", 1)
|
||||||
|
menu.add_separator()
|
||||||
|
menu.add_destructive_item("Erase Shape", 2)
|
||||||
|
menu.id_pressed.connect(_on_menu_item_pressed)
|
||||||
|
var copy_button: Button = menu.get_row_button(1)
|
||||||
|
var erase_button: Button = menu.get_row_button(2)
|
||||||
|
check("normal and destructive rows are created", copy_button != null and erase_button != null)
|
||||||
|
check("rows meet the Studio List target height", copy_button != null and copy_button.custom_minimum_size.y == 48.0)
|
||||||
|
check("menu rows do not show a keyboard selector on popup", copy_button != null and copy_button.focus_mode == Control.FOCUS_NONE and not copy_button.has_focus())
|
||||||
|
check("destructive label uses semantic danger color", erase_button != null and erase_button.get_theme_color("font_color").is_equal_approx(visual_theme.chrome_color(&"destructive", &"prop")))
|
||||||
|
var panel_style: StyleBoxFlat = menu.get_theme_stylebox("panel") as StyleBoxFlat
|
||||||
|
var hover_style: StyleBoxFlat = copy_button.get_theme_stylebox("hover") as StyleBoxFlat
|
||||||
|
var focus_style: StyleBoxFlat = copy_button.get_theme_stylebox("focus") as StyleBoxFlat
|
||||||
|
check("popup shell uses shared raised surface", panel_style != null and panel_style.bg_color.is_equal_approx(visual_theme.chrome_color(&"surface", &"prop")))
|
||||||
|
check("hover row uses a cyan side cue", hover_style != null and hover_style.border_width_left == 3 and hover_style.border_color.is_equal_approx(visual_theme.chrome_color(&"edit", &"prop")))
|
||||||
|
check("keyboard focus has a distinct focus outline", focus_style != null and focus_style.border_width_left == 2 and focus_style.border_color.is_equal_approx(visual_theme.chrome_color(&"focus", &"prop")))
|
||||||
|
|
||||||
|
root.size = Vector2i(900, 600)
|
||||||
|
menu.popup_at(Vector2(root.size) - Vector2(4.0, 4.0))
|
||||||
|
await process_frame
|
||||||
|
check("popup opens at the requested edge", menu.visible)
|
||||||
|
check("popup stays inside the viewport", menu.position.x >= 0 and menu.position.y >= 0 and menu.position.x + menu.size.x <= root.size.x and menu.position.y + menu.size.y <= root.size.y)
|
||||||
|
var down_event := InputEventKey.new()
|
||||||
|
down_event.keycode = KEY_DOWN
|
||||||
|
down_event.pressed = true
|
||||||
|
menu.call("_input", down_event)
|
||||||
|
await process_frame
|
||||||
|
check("Down reveals focus on the first row", copy_button.has_focus())
|
||||||
|
var up_event := InputEventKey.new()
|
||||||
|
up_event.keycode = KEY_UP
|
||||||
|
up_event.pressed = true
|
||||||
|
menu.call("_input", up_event)
|
||||||
|
await process_frame
|
||||||
|
check("Up moves focus to the last row", erase_button.has_focus())
|
||||||
|
menu.call("_on_row_mouse_entered", copy_button)
|
||||||
|
await process_frame
|
||||||
|
check("pointer entry hides the keyboard selector", not copy_button.has_focus() and not erase_button.has_focus() and copy_button.focus_mode == Control.FOCUS_NONE)
|
||||||
|
menu.hide()
|
||||||
|
var avoid_rect := Rect2(340.0, 240.0, 220.0, 120.0)
|
||||||
|
menu.popup_at(avoid_rect.get_center(), avoid_rect)
|
||||||
|
await process_frame
|
||||||
|
check("popup keeps the clicked target unobstructed when room is available", not Rect2(Vector2(menu.position), Vector2(menu.size)).intersects(avoid_rect))
|
||||||
|
erase_button.emit_signal("pressed")
|
||||||
|
await process_frame
|
||||||
|
check("activating an item emits its id and closes the popup", _last_pressed_id == 2 and not menu.visible)
|
||||||
|
menu.queue_free()
|
||||||
|
await process_frame
|
||||||
|
root.size = Vector2i(1280, 800)
|
||||||
|
await _test_prop_context_menu()
|
||||||
|
await _test_stickman_context_menus()
|
||||||
|
|
||||||
|
print("----------------------------------------")
|
||||||
|
if _failures == 0:
|
||||||
|
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_prop_context_menu() -> void:
|
||||||
|
var editor: Control = PROP_SCENE.instantiate() as Control
|
||||||
|
root.add_child(editor)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
_dismiss_fixture_recovery_prompt(editor)
|
||||||
|
var canvas: PropCanvas = editor.get("_canvas") as PropCanvas
|
||||||
|
canvas.add_primitive("box", Vector2.ZERO)
|
||||||
|
var shape: Dictionary = (canvas.get("shapes") as Array)[0] as Dictionary
|
||||||
|
var bounds: Rect2 = canvas.call("_shape_bbox", shape) as Rect2
|
||||||
|
var center: Vector2 = bounds.get_center()
|
||||||
|
var zoom: float = float(canvas.get("_zoom"))
|
||||||
|
var pan: Vector2 = canvas.get("_pan_offset") as Vector2
|
||||||
|
var click := InputEventMouseButton.new()
|
||||||
|
click.global_position = canvas.global_position + center * zoom + pan
|
||||||
|
canvas.call("_handle_transform_right_click", click, center)
|
||||||
|
await process_frame
|
||||||
|
var menu: EditorContextMenu = canvas.get("_context_menu") as EditorContextMenu
|
||||||
|
check("Prop opens the shared Studio List menu", menu.visible)
|
||||||
|
check("Prop uses child-readable shape action labels", menu.get_row_button(3) != null and menu.get_row_button(3).text == "Copy Shape" and menu.get_row_button(5).text == "Erase Shape")
|
||||||
|
check("Prop destructive action uses danger color", menu.get_row_button(5) != null and menu.get_row_button(5).get_theme_color("font_color").is_equal_approx((editor.get("_editor_visual_theme") as EditorVisualTheme).chrome_color(&"destructive", &"prop")))
|
||||||
|
check("Prop menu remains inside the viewport", menu.position.x >= 0 and menu.position.y >= 0 and menu.position.x + menu.size.x <= root.size.x and menu.position.y + menu.size.y <= root.size.y)
|
||||||
|
var prop_target: Rect2 = canvas.call("_global_shape_target_rect", 0) as Rect2
|
||||||
|
check("Prop menu leaves the clicked shape visible", not Rect2(Vector2(menu.position), Vector2(menu.size)).intersects(prop_target))
|
||||||
|
check("Prop menu avoids an unnecessary scrollbar", not (menu.get_node("MenuShell/MenuScroll") as ScrollContainer).get_v_scroll_bar().visible)
|
||||||
|
await _save_capture("context_menu_prop.png")
|
||||||
|
menu.hide()
|
||||||
|
editor.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _test_stickman_context_menus() -> void:
|
||||||
|
var main_scene: Control = STICKMAN_SCENE.instantiate() as Control
|
||||||
|
root.add_child(main_scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var editor: Control = main_scene.find_child("StickmanEditor", true, false) as Control
|
||||||
|
var panels: Dictionary = editor.get("_body_part_panels") as Dictionary
|
||||||
|
var head: BodyPartPanel = panels.get("head") as BodyPartPanel
|
||||||
|
head.add_shape(&"rectangle", head.call("drawing_center_world"))
|
||||||
|
var shape: Dictionary = (head.get("shapes") as Array)[0] as Dictionary
|
||||||
|
var points: PackedVector2Array = shape.get("points", PackedVector2Array()) as PackedVector2Array
|
||||||
|
var center := Vector2.ZERO
|
||||||
|
for point: Vector2 in points:
|
||||||
|
center += point
|
||||||
|
if not points.is_empty():
|
||||||
|
center /= float(points.size())
|
||||||
|
var click := InputEventMouseButton.new()
|
||||||
|
var drawing_area: Control = head.get("drawing_area") as Control
|
||||||
|
var head_zoom: float = float(head.get("_zoom"))
|
||||||
|
var head_pan: Vector2 = head.get("_pan_offset") as Vector2
|
||||||
|
click.global_position = drawing_area.global_position + head_pan + center * head_zoom
|
||||||
|
head.call("_handle_right_click", click, center)
|
||||||
|
await process_frame
|
||||||
|
var part_menu: EditorContextMenu = head.get("context_menu") as EditorContextMenu
|
||||||
|
check("Stickman body-part panels open the shared Studio List menu", part_menu.visible)
|
||||||
|
check("Stickman shape menu uses the shared shape wording", part_menu.get_row_button(9) != null and part_menu.get_row_button(9).text == "Copy Shape" and part_menu.get_row_button(5).text == "Erase Shape")
|
||||||
|
check("Stickman Erase Shape is coral", part_menu.get_row_button(5) != null and part_menu.get_row_button(5).get_theme_color("font_color").is_equal_approx((editor.get("_editor_visual_theme") as EditorVisualTheme).chrome_color(&"destructive", &"stickman")))
|
||||||
|
var part_target: Rect2 = head.call("_global_shape_target_rect", shape) as Rect2
|
||||||
|
check("Stickman part menu leaves the clicked shape visible", not Rect2(Vector2(part_menu.position), Vector2(part_menu.size)).intersects(part_target))
|
||||||
|
check("Stickman part menu avoids an unnecessary scrollbar", not (part_menu.get_node("MenuShell/MenuScroll") as ScrollContainer).get_v_scroll_bar().visible)
|
||||||
|
await _save_capture("context_menu_stickman_part.png")
|
||||||
|
part_menu.hide()
|
||||||
|
|
||||||
|
var preview: WholeStickmanPreview = editor.find_child("WholeStickmanPreview", true, false) as WholeStickmanPreview
|
||||||
|
var preview_area: Control = preview.get("preview_area") as Control
|
||||||
|
await process_frame
|
||||||
|
var part_bounds: Dictionary = preview.get("_part_bounds") as Dictionary
|
||||||
|
var head_bounds: Rect2 = part_bounds.get("head", Rect2()) as Rect2
|
||||||
|
var preview_zoom: float = float(preview.get("_zoom"))
|
||||||
|
var preview_pan: Vector2 = preview.get("_pan_offset") as Vector2
|
||||||
|
var preview_click := InputEventMouseButton.new()
|
||||||
|
preview_click.position = head_bounds.get_center() * preview_zoom + preview_pan
|
||||||
|
preview_click.global_position = preview_area.global_position + preview_click.position
|
||||||
|
preview.call("_handle_right_click", preview_click)
|
||||||
|
await process_frame
|
||||||
|
var preview_menu: EditorContextMenu = preview.get("_context_menu") as EditorContextMenu
|
||||||
|
check("Whole Stickman preview opens the shared Studio List menu", preview_menu.visible)
|
||||||
|
check("Whole Stickman uses child-readable flip labels", preview_menu.get_row_button(2) != null and preview_menu.get_row_button(2).text == "Flip Left-Right" and preview_menu.get_row_button(3).text == "Flip Up-Down")
|
||||||
|
var preview_target := Rect2(preview_area.global_position + preview_pan + head_bounds.position * preview_zoom, head_bounds.size * preview_zoom).grow(4.0)
|
||||||
|
check("Whole Stickman menu leaves the clicked part visible", not Rect2(Vector2(preview_menu.position), Vector2(preview_menu.size)).intersects(preview_target))
|
||||||
|
await _save_capture("context_menu_stickman_preview.png")
|
||||||
|
preview_menu.hide()
|
||||||
|
main_scene.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _save_capture(file_name: String) -> void:
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
print("SKIP visual capture under headless renderer: %s" % file_name)
|
||||||
|
return
|
||||||
|
var viewport_texture: ViewportTexture = root.get_texture()
|
||||||
|
if viewport_texture == null:
|
||||||
|
check("visual capture texture exists: %s" % file_name, false)
|
||||||
|
return
|
||||||
|
var image: Image = viewport_texture.get_image()
|
||||||
|
if image == null or image.is_empty():
|
||||||
|
check("visual capture image is available: %s" % file_name, false)
|
||||||
|
return
|
||||||
|
var path: String = ProjectSettings.globalize_path("res://.godot/" + file_name)
|
||||||
|
var error: Error = image.save_png(path)
|
||||||
|
check("visual capture saved: %s" % file_name, error == OK and not image.is_empty())
|
||||||
|
|
||||||
|
|
||||||
|
func _dismiss_fixture_recovery_prompt(editor: Control) -> void:
|
||||||
|
# A real user autosave may be present in user://. Hide the prompt for clean
|
||||||
|
# visual captures without choosing Restore or Discard or touching that file.
|
||||||
|
for child: Node in editor.get_children():
|
||||||
|
if child is ConfirmationDialog and (child as ConfirmationDialog).title == "Restore Prop?":
|
||||||
|
STAGE_DIALOG_STYLER.hide(child as ConfirmationDialog)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_menu_item_pressed(id: int) -> void:
|
||||||
|
_last_pressed_id = id
|
||||||
|
|
||||||
|
|
||||||
|
func check(label: String, condition: bool) -> void:
|
||||||
|
_checks += 1
|
||||||
|
if condition:
|
||||||
|
print("PASS: %s" % label)
|
||||||
|
else:
|
||||||
|
_failures += 1
|
||||||
|
push_error("FAIL: %s" % label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://crigocq28ory
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const STICKMAN_SCENE := preload("res://scenes/main.tscn")
|
||||||
|
const PROP_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||||
|
const STAGE_DIALOG_STYLER := preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
|
|
||||||
|
var _checks: int = 0
|
||||||
|
var _failures: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
print("\n=== EDITOR VISUAL HARMONIZATION TEST ===")
|
||||||
|
root.size = Vector2i(1280, 800)
|
||||||
|
await _test_stickman_workspace_and_transactions()
|
||||||
|
await _test_prop_reshape_and_chrome()
|
||||||
|
print("----------------------------------------")
|
||||||
|
if _failures == 0:
|
||||||
|
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func check(label: String, condition: bool) -> void:
|
||||||
|
_checks += 1
|
||||||
|
if condition:
|
||||||
|
print("PASS: %s" % label)
|
||||||
|
else:
|
||||||
|
_failures += 1
|
||||||
|
push_error("FAIL: %s" % label)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_stickman_workspace_and_transactions() -> void:
|
||||||
|
var main_scene: Control = STICKMAN_SCENE.instantiate() as Control
|
||||||
|
root.add_child(main_scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var editor: Control = main_scene.find_child("StickmanEditor", true, false) as Control
|
||||||
|
var panels: Dictionary = editor.get("_body_part_panels") as Dictionary
|
||||||
|
var preview: Control = editor.find_child("WholeStickmanPreview", true, false) as Control
|
||||||
|
var menu_bar: Control = editor.get_node("ChromeRoot/MenuBar") as Control
|
||||||
|
var top_bar: Control = editor.get_node("ChromeRoot/TopBar") as Control
|
||||||
|
var workspace_toolbar: Control = editor.get_node("ChromeRoot/WorkspaceToolbar") as Control
|
||||||
|
var workspace_row: Control = editor.get_node("ChromeRoot/WorkspaceRow") as Control
|
||||||
|
var status_bar: Control = editor.get_node("ChromeRoot/StatusBar") as Control
|
||||||
|
var stickman_save_status: Label = editor.get_node("ChromeRoot/StatusBar/SaveStatus") as Label
|
||||||
|
check("Stickman keeps ten live BodyPartPanel instances", panels.size() == 10)
|
||||||
|
check("one permanent Whole Stickman preview exists", preview != null)
|
||||||
|
check("wide layout adapts to Overview", int(editor.call("get_workspace_view")) == 0)
|
||||||
|
check("Stickman chrome rows and workspace do not overlap", top_bar.position.y >= menu_bar.position.y + menu_bar.size.y - 1.0 and workspace_toolbar.position.y >= top_bar.position.y + top_bar.size.y - 1.0 and workspace_row.position.y >= workspace_toolbar.position.y + workspace_toolbar.size.y - 1.0 and status_bar.position.y >= workspace_row.position.y + workspace_row.size.y - 1.0)
|
||||||
|
check("Overview body panels begin below the chrome", (panels.get("head") as Control).get_global_rect().position.y >= workspace_row.get_global_rect().position.y - 1.0)
|
||||||
|
check("clean new Stickman shows explicit unsaved status", stickman_save_status.text == "Not saved yet")
|
||||||
|
|
||||||
|
var head: Control = panels.get("head") as Control
|
||||||
|
var head_id: int = head.get_instance_id()
|
||||||
|
check("head part supports authored shapes", bool(head.call("add_shape", &"rectangle", head.call("drawing_center_world"))))
|
||||||
|
check("shape creation marks the document dirty", bool(editor.call("has_unsaved_changes")))
|
||||||
|
check("Stickman dirty transition shows explicit status", stickman_save_status.text == "Unsaved changes")
|
||||||
|
check("new shape is selected", int(head.call("selected_shape_index")) == 0)
|
||||||
|
|
||||||
|
var zoom: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
zoom.button_index = MOUSE_BUTTON_WHEEL_UP
|
||||||
|
zoom.pressed = true
|
||||||
|
zoom.position = Vector2(100.0, 80.0)
|
||||||
|
head.call("_on_drawing_area_gui_input", zoom)
|
||||||
|
var pan_down: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
pan_down.button_index = MOUSE_BUTTON_MIDDLE
|
||||||
|
pan_down.pressed = true
|
||||||
|
pan_down.position = Vector2(100.0, 80.0)
|
||||||
|
head.call("_on_drawing_area_gui_input", pan_down)
|
||||||
|
var pan_motion: InputEventMouseMotion = InputEventMouseMotion.new()
|
||||||
|
pan_motion.position = Vector2(112.0, 91.0)
|
||||||
|
head.call("_on_drawing_area_gui_input", pan_motion)
|
||||||
|
var pan_up: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
pan_up.button_index = MOUSE_BUTTON_MIDDLE
|
||||||
|
pan_up.pressed = false
|
||||||
|
pan_up.position = Vector2(112.0, 91.0)
|
||||||
|
head.call("_on_drawing_area_gui_input", pan_up)
|
||||||
|
var saved_view: Dictionary = head.call("get_view_context") as Dictionary
|
||||||
|
var saved_pan: Vector2 = saved_view.get("pan_offset", Vector2.ZERO) as Vector2
|
||||||
|
check("panel zoom and pan are active before moving it", float(saved_view.get("zoom", 1.0)) > 1.0 and saved_pan.length() > 0.0)
|
||||||
|
|
||||||
|
var stickman_path: String = "user://editor_visual_harmonization_test.stk"
|
||||||
|
check("Stickman saves through its document API", bool(editor.call("save_to_path", stickman_path)))
|
||||||
|
check("successful save clears dirty state", not bool(editor.call("has_unsaved_changes")))
|
||||||
|
check("saved Stickman shows explicit saved status", stickman_save_status.text == "Saved")
|
||||||
|
var stickman_file: FileAccess = FileAccess.open(stickman_path, FileAccess.READ)
|
||||||
|
var stickman_json: Variant = JSON.parse_string(stickman_file.get_as_text())
|
||||||
|
stickman_file.close()
|
||||||
|
var saved_parts: Dictionary = (stickman_json as Dictionary).get("body_parts", {}) as Dictionary
|
||||||
|
var saved_head: Dictionary = saved_parts.get("head", {}) as Dictionary
|
||||||
|
check("save preserves the existing 1.5 format", str((stickman_json as Dictionary).get("version", "")) == "1.5")
|
||||||
|
check("save includes authored head shapes", (saved_head.get("shapes", []) as Array).size() == 1)
|
||||||
|
check("save still writes guide offset metadata", saved_head.has("guide_offset"))
|
||||||
|
|
||||||
|
var before_switch_view: Dictionary = head.call("get_view_context") as Dictionary
|
||||||
|
editor.call("request_workspace_view", 1, true)
|
||||||
|
check("Focus view keeps the same panel instance", head.get_instance_id() == head_id and int(editor.call("get_workspace_view")) == 1)
|
||||||
|
check("Focus view preserves panel zoom and pan", _view_contexts_equal(before_switch_view, head.call("get_view_context") as Dictionary))
|
||||||
|
check("view choice does not dirty authored content", not bool(editor.call("has_unsaved_changes")))
|
||||||
|
root.size = Vector2i(520, 800)
|
||||||
|
await process_frame
|
||||||
|
editor.call("_refresh_responsive_workspace")
|
||||||
|
check("manual Focus choice survives a narrow resize", int(editor.call("get_workspace_view")) == 1)
|
||||||
|
var whole_stickman_button: Button = editor.get_node("ChromeRoot/WorkspaceToolbar/WholeStickmanButton") as Button
|
||||||
|
var back_to_part_button: Button = editor.get_node("ChromeRoot/WorkspaceToolbar/BackToPartButton") as Button
|
||||||
|
var body_part_workspace: Control = editor.get_node("ChromeRoot/WorkspaceRow/BodyPartWorkspace") as Control
|
||||||
|
check("narrow Focus keeps part controls and Whole Stickman access visible", body_part_workspace.visible and whole_stickman_button.visible and not preview.visible)
|
||||||
|
whole_stickman_button.emit_signal("pressed")
|
||||||
|
await process_frame
|
||||||
|
check("narrow Whole Stickman control opens the permanent preview", not body_part_workspace.visible and preview.visible and back_to_part_button.visible and int(editor.call("get_workspace_view")) == 1)
|
||||||
|
back_to_part_button.emit_signal("pressed")
|
||||||
|
await process_frame
|
||||||
|
check("Back to Part restores the same live panel at narrow width", body_part_workspace.visible and not preview.visible and whole_stickman_button.visible and head.get_instance_id() == head_id)
|
||||||
|
root.size = Vector2i(1280, 800)
|
||||||
|
await process_frame
|
||||||
|
editor.call("_refresh_responsive_workspace")
|
||||||
|
|
||||||
|
editor.call("request_active_part", "torso")
|
||||||
|
check("part picker switches the active focus panel", str(editor.call("get_active_part_name")) == "torso")
|
||||||
|
editor.call("request_active_part", "head")
|
||||||
|
check("switching back restores the original head instance", panels.get("head") == head and head.get_instance_id() == head_id)
|
||||||
|
check("switching parts preserves head geometry and selection", int(head.call("selected_shape_index")) == 0 and (head.get("shapes") as Array).size() == 1)
|
||||||
|
editor.call("request_workspace_view", 0, true)
|
||||||
|
check("Overview restores the same authored panel", int(editor.call("get_workspace_view")) == 0 and head.get_instance_id() == head_id)
|
||||||
|
|
||||||
|
head.call("open_selected_color_picker")
|
||||||
|
var picker: PopupPanel = head.get("color_picker_popup") as PopupPanel
|
||||||
|
check("open color popup blocks presentation switching", picker.visible and not bool(head.call("can_switch_presentation")))
|
||||||
|
editor.call("request_active_part", "torso")
|
||||||
|
editor.call("request_workspace_view", 1, true)
|
||||||
|
check("deferred view switch waits while popup is open", int(editor.call("get_workspace_view")) == 0 and str(editor.call("get_active_part_name")) == "head")
|
||||||
|
picker.hide()
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
check("deferred part and view switch resumes after popup closes", int(editor.call("get_workspace_view")) == 1 and str(editor.call("get_active_part_name")) == "torso")
|
||||||
|
check("popup and workspace changes remain clean", not bool(editor.call("has_unsaved_changes")))
|
||||||
|
editor.call("request_active_part", "head")
|
||||||
|
var preview_area: Control = preview.get("preview_area") as Control
|
||||||
|
await process_frame
|
||||||
|
var raw_points: PackedVector2Array = preview.call("_collect_all_raw_points", "head") as PackedVector2Array
|
||||||
|
var part_center: Vector2 = preview.call("_compute_center", raw_points) as Vector2
|
||||||
|
var initial_part_position: Vector2 = preview.call("get_part_position", "head") as Vector2
|
||||||
|
var mouse_down: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
mouse_down.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
mouse_down.pressed = true
|
||||||
|
mouse_down.position = part_center
|
||||||
|
preview.call("_on_preview_gui_input", mouse_down)
|
||||||
|
var drag: InputEventMouseMotion = InputEventMouseMotion.new()
|
||||||
|
drag.position = part_center + Vector2(16.0, 12.0)
|
||||||
|
preview.call("_on_preview_gui_input", drag)
|
||||||
|
var mouse_up: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
mouse_up.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
mouse_up.pressed = false
|
||||||
|
mouse_up.position = drag.position
|
||||||
|
preview.call("_on_preview_gui_input", mouse_up)
|
||||||
|
check("preview drag commits an authored part transform", not (preview.call("get_part_position", "head") as Vector2).is_equal_approx(initial_part_position))
|
||||||
|
check("committed preview transform marks document dirty", bool(editor.call("has_unsaved_changes")))
|
||||||
|
check("preview input uses the live preview area", preview_area != null and preview_area.size.x > 0.0)
|
||||||
|
check("save after transform keeps the existing format", bool(editor.call("save_to_path", stickman_path)) and not bool(editor.call("has_unsaved_changes")))
|
||||||
|
preview.call("_mirror_part_x")
|
||||||
|
check("mirror transform marks document dirty", bool(editor.call("has_unsaved_changes")) and (preview.call("get_part_scale", "head") as Vector2).x < 0.0)
|
||||||
|
check("mirror transform can be saved", bool(editor.call("save_to_path", stickman_path)) and not bool(editor.call("has_unsaved_changes")))
|
||||||
|
preview.call("_bring_forward_part")
|
||||||
|
check("part order edit marks document dirty", bool(editor.call("has_unsaved_changes")))
|
||||||
|
|
||||||
|
check("second shape can be authored", bool(head.call("add_shape", &"line", head.call("drawing_center_world"))))
|
||||||
|
editor.call("_on_file_menu_id_pressed", 2)
|
||||||
|
var unsaved_dialog: ConfirmationDialog = editor.get("_unsaved_dialog") as ConfirmationDialog
|
||||||
|
check("Start Over shows a save/discard/cancel transaction", STAGE_DIALOG_STYLER.is_open(unsaved_dialog))
|
||||||
|
unsaved_dialog.get_cancel_button().emit_signal("pressed")
|
||||||
|
await process_frame
|
||||||
|
check("Cancel leaves dirty content in place", bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).size() == 2)
|
||||||
|
editor.call("_on_file_menu_id_pressed", 2)
|
||||||
|
unsaved_dialog.emit_signal("custom_action", "discard")
|
||||||
|
await process_frame
|
||||||
|
check("Discard clears authored parts and dirty state", not bool(editor.call("has_unsaved_changes")) and (head.get("shapes") as Array).is_empty())
|
||||||
|
check("clean new Stickman returns to explicit unsaved status", stickman_save_status.text == "Not saved yet")
|
||||||
|
check("Start Over keeps the selected workspace view", int(editor.call("get_workspace_view")) == 1)
|
||||||
|
DirAccess.remove_absolute(stickman_path)
|
||||||
|
main_scene.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _test_prop_reshape_and_chrome() -> void:
|
||||||
|
var editor: Control = PROP_SCENE.instantiate() as Control
|
||||||
|
root.add_child(editor)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var canvas: Control = editor.get("_canvas") as Control
|
||||||
|
var save_status: Label = editor.get("_save_status") as Label
|
||||||
|
check("Prop chrome uses wrapping top and mode rows", editor.get("_top_right") is FlowContainer and editor.get("_mode_bar") is FlowContainer)
|
||||||
|
check("Prop action target meets 44 px minimum", (editor.get("_create_point_button") as Button).custom_minimum_size.y >= 44.0)
|
||||||
|
check("new Prop has an explicit clean save status", str(save_status.text).ends_with("Not saved yet"))
|
||||||
|
check("whole-prop action uses approved discoverable wording", str((editor.get("_whole_prop_resize_button") as Button).text) == "Move, Turn, or Resize Prop")
|
||||||
|
check("material physics tile uses child-facing grip language", str((editor.get("_friction_label") as Label).text) == "Surface grip")
|
||||||
|
var preset_option: PropInspectorDropdown = editor.get("_preset_option") as PropInspectorDropdown
|
||||||
|
check("material choices show colored swatches in field and popup", preset_option.item_count == 5 and preset_option.get_item_icon(0) != null and preset_option.get_item_icon(4) != null and preset_option.icon != null)
|
||||||
|
var material_selected_row: StyleBoxFlat = preset_option.get_popup().get_theme_stylebox("hover") as StyleBoxFlat
|
||||||
|
var material_items_plain: bool = true
|
||||||
|
for index: int in preset_option.item_count:
|
||||||
|
material_items_plain = material_items_plain and not preset_option.get_popup().is_item_checkable(index)
|
||||||
|
check("material popup uses the full-row selection highlight without radio items", material_items_plain and material_selected_row.bg_color.is_equal_approx(Color("#103B4D")) and material_selected_row.border_color.is_equal_approx(Color("#22C6FF")))
|
||||||
|
await process_frame
|
||||||
|
preset_option.show_popup()
|
||||||
|
await process_frame
|
||||||
|
check("opening the material popup focuses its current value and spans the field", preset_option.get_popup().get_focused_item() == preset_option.selected and preset_option.get_popup().size.x >= preset_option.size.x)
|
||||||
|
check("dropdown popup icons reserve a 44 px row target", preset_option.get_popup().get_item_icon(0).get_width() >= 44)
|
||||||
|
preset_option.hide_popup()
|
||||||
|
preset_option.get_popup().emit_signal("id_pressed", 1)
|
||||||
|
check("choosing a material updates the field and editor model", preset_option.selected == 1 and int(editor.get("_material_preset")) == 1)
|
||||||
|
preset_option.get_popup().emit_signal("id_pressed", 0)
|
||||||
|
var primitive_option: PropInspectorDropdown = editor.get("_primitive_option") as PropInspectorDropdown
|
||||||
|
check("2D shape choices show flat icons in field and popup", primitive_option.item_count == 5 and primitive_option.get_item_icon(0) != null and primitive_option.get_item_icon(4) != null and primitive_option.icon != null)
|
||||||
|
var shape_items_plain: bool = true
|
||||||
|
for index: int in primitive_option.item_count:
|
||||||
|
shape_items_plain = shape_items_plain and not primitive_option.get_popup().is_item_checkable(index)
|
||||||
|
check("shape popup does not draw radio selectors", shape_items_plain)
|
||||||
|
primitive_option.show_popup()
|
||||||
|
await process_frame
|
||||||
|
check("shape popup highlights its current 2D choice", primitive_option.get_popup().get_focused_item() == primitive_option.selected)
|
||||||
|
primitive_option.hide_popup()
|
||||||
|
primitive_option.get_popup().emit_signal("id_pressed", 2)
|
||||||
|
check("choosing a 2D shape updates its parameters", primitive_option.selected == 2 and (editor.get("_param_spins") as Dictionary).has("base"))
|
||||||
|
primitive_option.get_popup().emit_signal("id_pressed", 0)
|
||||||
|
var tag_buttons: Dictionary = editor.get("_tag_checks") as Dictionary
|
||||||
|
var nature_button: Button = tag_buttons.get("nature") as Button
|
||||||
|
check("artwork tag chip has a colored icon and a wide touch target", nature_button != null and nature_button.icon != null and nature_button.custom_minimum_size.y >= 48.0)
|
||||||
|
var nature_icon: Texture2D = nature_button.icon
|
||||||
|
nature_button.set_pressed_no_signal(true)
|
||||||
|
nature_button.emit_signal("toggled", true)
|
||||||
|
var selected_tag_style := nature_button.get_theme_stylebox("pressed") as StyleBoxFlat
|
||||||
|
check("tag selection updates its model", (editor.get("_tags") as Array).has("nature"))
|
||||||
|
check("selected tag keeps its original colored icon without a badge", nature_button.icon == nature_icon)
|
||||||
|
check("selected tag style uses a cyan-tinted highlight", selected_tag_style.bg_color.is_equal_approx(Color("#103B4D")))
|
||||||
|
nature_button.set_pressed_no_signal(false)
|
||||||
|
nature_button.emit_signal("toggled", false)
|
||||||
|
check("clearing a tag removes it from the tag model", not (editor.get("_tags") as Array).has("nature"))
|
||||||
|
check("inspector sections use themed cards", editor.get("_material_section") is PanelContainer and editor.get("_add_section") is PanelContainer and editor.get("_tag_section") is PanelContainer)
|
||||||
|
check("anchor layer control uses the approved label", (editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) is Label and str(((editor.get("_anchor_z") as SpinBox).get_parent().get_child(0) as Label).text) == "Held prop layer:")
|
||||||
|
|
||||||
|
canvas.call("add_primitive", "box", Vector2(150.0, 150.0))
|
||||||
|
var shapes: Array = canvas.call("get_shape_data") as Array
|
||||||
|
check("prop primitive is selected for editing", int(canvas.call("get_selected_index")) == 0 and shapes.size() == 1)
|
||||||
|
check("Prop edit shows explicit unsaved status", str(save_status.text).ends_with("Unsaved changes"))
|
||||||
|
var prop_path: String = "user://editor_visual_harmonization_test.prop"
|
||||||
|
var save_result: Variant = await editor.call("_save_to", prop_path, false)
|
||||||
|
check("clean Prop save shows explicit saved status", bool(save_result) and str(save_status.text).ends_with("Saved"))
|
||||||
|
editor.call("_on_points_mode_pressed")
|
||||||
|
var create_button: Button = editor.get("_create_point_button") as Button
|
||||||
|
check("Reshape exposes the Create Point action", create_button.visible and create_button.text == "📍 Create Point")
|
||||||
|
create_button.emit_signal("pressed")
|
||||||
|
check("Create Point arms with a visible Esc hint", bool(canvas.call("is_create_point_armed")) and str((editor.get("_create_point_hint") as Label).text).contains("Press Esc"))
|
||||||
|
var escape: InputEventKey = InputEventKey.new()
|
||||||
|
escape.keycode = KEY_ESCAPE
|
||||||
|
escape.pressed = true
|
||||||
|
editor.call("_unhandled_key_input", escape)
|
||||||
|
check("Esc cancels the armed point action", not bool(canvas.call("is_create_point_armed")))
|
||||||
|
create_button.emit_signal("pressed")
|
||||||
|
var internal_shapes: Array = canvas.get("shapes") as Array
|
||||||
|
var first_shape: Dictionary = internal_shapes[0] as Dictionary
|
||||||
|
var original_points: PackedVector2Array = first_shape.get("points", PackedVector2Array()) as PackedVector2Array
|
||||||
|
var edge_midpoint: Vector2 = (original_points[0] + original_points[1]) * 0.5
|
||||||
|
var canvas_zoom: float = float(canvas.get("_zoom"))
|
||||||
|
var canvas_pan: Vector2 = canvas.get("_pan_offset") as Vector2
|
||||||
|
var edge_screen_pos: Vector2 = edge_midpoint * canvas_zoom + canvas_pan
|
||||||
|
check("toolbar click target resolves to the visible shape edge", int(canvas.call("hit_test_segment", edge_midpoint)) == 0)
|
||||||
|
var edge_click: InputEventMouseButton = InputEventMouseButton.new()
|
||||||
|
edge_click.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
edge_click.pressed = true
|
||||||
|
edge_click.position = edge_screen_pos
|
||||||
|
canvas.call("_on_canvas_gui_input", edge_click)
|
||||||
|
shapes = canvas.call("get_shape_data") as Array
|
||||||
|
var reshaped: Dictionary = shapes[0] as Dictionary
|
||||||
|
var vertices: PackedVector2Array = reshaped.get("points", PackedVector2Array()) as PackedVector2Array
|
||||||
|
check("armed edge click inserts a point", vertices.size() == 5)
|
||||||
|
check("inserted point records reshaped state", bool(reshaped.get("reshaped", false)))
|
||||||
|
check("inserted point marks Prop document dirty", bool(editor.get("_dirty")))
|
||||||
|
check("Create Point updates Prop status to unsaved", str(save_status.text).ends_with("Unsaved changes"))
|
||||||
|
check("one click disarms Create Point", not bool(canvas.call("is_create_point_armed")))
|
||||||
|
if FileAccess.file_exists(prop_path):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(prop_path))
|
||||||
|
editor.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _view_contexts_equal(left: Dictionary, right: Dictionary) -> bool:
|
||||||
|
var left_pan: Vector2 = left.get("pan_offset", Vector2.ZERO) as Vector2
|
||||||
|
var right_pan: Vector2 = right.get("pan_offset", Vector2.ZERO) as Vector2
|
||||||
|
return is_equal_approx(float(left.get("zoom", 1.0)), float(right.get("zoom", 1.0))) and left_pan.is_equal_approx(right_pan)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bulw0ym7dnnaj
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const EDITOR_VISUAL_THEME := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||||
|
|
||||||
|
var _checks: int = 0
|
||||||
|
var _failures: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var defaults: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||||
|
check("committed shared theme loads", defaults.load_from(EDITOR_VISUAL_THEME.DEFAULT_PATH))
|
||||||
|
check("default body font is 18", defaults.chrome_px(&"body", &"prop") == 18)
|
||||||
|
check("default touch target is 44 px", defaults.chrome_px(&"button_min_height", &"stickman") == 44)
|
||||||
|
check("selected surface has a shared semantic token", defaults.chrome_color(&"selected_surface", &"prop").is_equal_approx(Color("#103B4D")))
|
||||||
|
check("Studio List rows are 48 px", defaults.chrome_px(&"context_menu_row_height", &"stickman") == 48)
|
||||||
|
check("Studio List minimum width is 208 px", defaults.chrome_px(&"context_menu_min_width", &"prop") == 208)
|
||||||
|
check("preview role preserves alpha", is_equal_approx(defaults.chrome_color(&"preview").a, 0.4))
|
||||||
|
|
||||||
|
var custom_path: String = "user://editor_visual_theme_test.json"
|
||||||
|
var custom_file: FileAccess = FileAccess.open(custom_path, FileAccess.WRITE)
|
||||||
|
custom_file.store_string('{"chrome":{"colors":{"edit":"#22C6FF","focus":"#FF00AA"},"targets":{"button_min_height":48}},"editors":{"prop":{"colors":{"edit":"#FF8800"}}}}')
|
||||||
|
custom_file.close()
|
||||||
|
var custom: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||||
|
check("custom shared theme loads", custom.load_from(custom_path))
|
||||||
|
check("prop semantic override wins", custom.chrome_color(&"edit", &"prop").is_equal_approx(Color("#FF8800")))
|
||||||
|
check("stickman uses common semantic default", custom.chrome_color(&"edit", &"stickman").is_equal_approx(Color("#22C6FF")))
|
||||||
|
check("common touch target override applies", custom.chrome_px(&"button_min_height", &"prop") == 48)
|
||||||
|
check("local focus token overrides default", custom.chrome_color(&"focus").is_equal_approx(Color("#FF00AA")))
|
||||||
|
DirAccess.remove_absolute(custom_path)
|
||||||
|
|
||||||
|
var invalid_path: String = "user://editor_visual_theme_test_invalid.json"
|
||||||
|
var invalid_file: FileAccess = FileAccess.open(invalid_path, FileAccess.WRITE)
|
||||||
|
invalid_file.store_string('{"chrome":{"colors":{"edit":"electric"},"targets":{"button_min_height":"large"}}}')
|
||||||
|
invalid_file.close()
|
||||||
|
var invalid: EditorVisualTheme = EDITOR_VISUAL_THEME.new()
|
||||||
|
invalid.load_from(invalid_path)
|
||||||
|
check("invalid color falls back to semantic default", invalid.chrome_color(&"edit").is_equal_approx(Color("#22C6FF")))
|
||||||
|
check("invalid size falls back to semantic default", invalid.chrome_px(&"button_min_height") == 44)
|
||||||
|
DirAccess.remove_absolute(invalid_path)
|
||||||
|
|
||||||
|
var prop_theme: Theme = defaults.control_theme(&"prop")
|
||||||
|
var stickman_theme: Theme = defaults.control_theme(&"stickman")
|
||||||
|
var prop_focus: StyleBoxFlat = prop_theme.get_stylebox("focus", "Button") as StyleBoxFlat
|
||||||
|
var stickman_focus: StyleBoxFlat = stickman_theme.get_stylebox("focus", "Button") as StyleBoxFlat
|
||||||
|
check("each editor receives a separate local Theme", prop_theme.get_instance_id() != stickman_theme.get_instance_id())
|
||||||
|
check("each editor receives separate focus styles", prop_focus.get_instance_id() != stickman_focus.get_instance_id())
|
||||||
|
check("focus outline uses semantic focus token", prop_focus.border_color.is_equal_approx(defaults.chrome_color(&"focus")))
|
||||||
|
|
||||||
|
var action: Button = Button.new()
|
||||||
|
defaults.style_button_role(action, &"primary", &"prop")
|
||||||
|
var action_box: StyleBoxFlat = action.get_theme_stylebox("normal") as StyleBoxFlat
|
||||||
|
check("primary action uses semantic action color", action_box.bg_color.is_equal_approx(defaults.chrome_color(&"action_primary", &"prop")))
|
||||||
|
check("primary action reaches the minimum target size", action.custom_minimum_size.y >= 44.0)
|
||||||
|
action.free()
|
||||||
|
|
||||||
|
print("----------------------------------------")
|
||||||
|
if _failures == 0:
|
||||||
|
print("RESULT: ALL PASSED (%d assertions)" % _checks)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func check(label: String, condition: bool) -> void:
|
||||||
|
_checks += 1
|
||||||
|
if condition:
|
||||||
|
print("PASS: %s" % label)
|
||||||
|
else:
|
||||||
|
_failures += 1
|
||||||
|
push_error("FAIL: %s" % label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cmve03xjcfcg8
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn")
|
||||||
|
const DIALOG_STYLER: Script = preload("res://scripts/shared/stage_ui/stage_dialog_styler.gd")
|
||||||
|
|
||||||
|
var _checks: int = 0
|
||||||
|
var _failures: int = 0
|
||||||
|
var _stage: SandboxStage
|
||||||
|
var _chooser: StageGroundFinishChooser
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
_stage = SANDBOX_STAGE.instantiate() as SandboxStage
|
||||||
|
root.add_child(_stage)
|
||||||
|
await _wait_frames(3)
|
||||||
|
_open_through_palette_signal()
|
||||||
|
await _wait_frames(3)
|
||||||
|
_test_initial_gallery()
|
||||||
|
await _save_capture("phase19_visual_ground_finish_chooser_1280x720.png")
|
||||||
|
await _test_narrow_viewport_fit()
|
||||||
|
await _save_capture("phase19_visual_ground_finish_chooser_470x417.png")
|
||||||
|
_test_close_keeps_placement_unchanged()
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
await _wait_frames(2)
|
||||||
|
_open_through_palette_signal()
|
||||||
|
await process_frame
|
||||||
|
DIALOG_STYLER.dismiss_on_escape(_chooser)
|
||||||
|
_test_escape_keeps_placement_unchanged()
|
||||||
|
_open_through_palette_signal()
|
||||||
|
await _wait_frames(2)
|
||||||
|
_test_finish_selection_is_a_draft()
|
||||||
|
await _save_capture("phase19_visual_ground_finish_chooser_dirt_selected.png")
|
||||||
|
_test_draw_commits_selected_finish()
|
||||||
|
if DisplayServer.get_name() != "headless":
|
||||||
|
Input.warp_mouse(Vector2i(900, 420))
|
||||||
|
await _wait_frames(3)
|
||||||
|
_test_ground_ghost_is_armed()
|
||||||
|
await _save_capture("phase19_visual_ground_finish_placement.png")
|
||||||
|
await _test_ground_click_commits_selected_finish()
|
||||||
|
_stage.queue_free()
|
||||||
|
await _wait_frames(3)
|
||||||
|
if _failures == 0:
|
||||||
|
print("RESULT: ALL PASSED (%d checks)" % _checks)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("RESULT: %d FAILURE(S) out of %d checks" % [_failures, _checks])
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _open_through_palette_signal() -> void:
|
||||||
|
var buttons: Dictionary = _stage.get("_palette_buttons") as Dictionary
|
||||||
|
var ground_button: AssetCard = buttons.get("ground") as AssetCard
|
||||||
|
_expect(ground_button != null, "Ground palette card exists")
|
||||||
|
if ground_button == null:
|
||||||
|
return
|
||||||
|
ground_button.set_pressed_no_signal(true)
|
||||||
|
ground_button.activated.emit(ground_button.payload())
|
||||||
|
_chooser = _stage.get("_ground_finish_chooser") as StageGroundFinishChooser
|
||||||
|
|
||||||
|
|
||||||
|
func _test_initial_gallery() -> void:
|
||||||
|
_expect(_chooser != null, "Ground finish chooser is built")
|
||||||
|
if _chooser == null:
|
||||||
|
return
|
||||||
|
_expect(DIALOG_STYLER.is_open(_chooser), "Ground card opens the modal chooser through the palette signal")
|
||||||
|
_expect(_chooser.title == "Choose Ground", "Chooser title matches the approved gallery")
|
||||||
|
_expect(_chooser.dialog_text == "Stickmen walk here", "Chooser explains the Ground role")
|
||||||
|
_expect(_chooser.cancel_button_text == "Close", "Footer dismissal action is Close")
|
||||||
|
_expect(_chooser.get_ok_button().text == "Draw Grass Ground", "Primary action names the selected finish")
|
||||||
|
var buttons: Dictionary = _chooser.choice_buttons()
|
||||||
|
_expect(buttons.size() == 7, "Gallery contains seven exclusive Ground finish choices")
|
||||||
|
var grass_button: Button = buttons.get(&"finish_ground_grass") as Button
|
||||||
|
_expect(grass_button != null and grass_button.button_pressed, "Current Grass finish is visibly selected")
|
||||||
|
_expect(_chooser.selected_preview_finish() == &"finish_ground_grass", "Large sample previews the selected finish")
|
||||||
|
var grid: GridContainer = _chooser.get("_choice_grid") as GridContainer
|
||||||
|
var wide_row: HBoxContainer = _chooser.get("_layout_row") as HBoxContainer
|
||||||
|
var preview_panel: Control = _chooser.get("_preview_panel") as Control
|
||||||
|
_expect(bool(_chooser.get("_is_wide_layout")) and grid != null and grid.columns == 3, "1280×720 opens the side-by-side three-column gallery")
|
||||||
|
_expect(grid != null and grid.get_parent() == wide_row and preview_panel != null and preview_panel.visible, "Wide gallery keeps choices beside the selected preview")
|
||||||
|
var card: Control = _chooser.get_meta(DIALOG_STYLER.CARD_META) as Control
|
||||||
|
var viewport_size: Vector2 = root.get_visible_rect().size
|
||||||
|
var card_rect: Rect2 = card.get_global_rect() if card != null else Rect2()
|
||||||
|
_expect(card != null and card_rect.size.x < viewport_size.x * 0.9, "Roomy chooser card leaves visible horizontal Stage margins")
|
||||||
|
_expect(card != null and card_rect.size.y < viewport_size.y * 0.82, "Roomy chooser card stays comfortably below the Stage height")
|
||||||
|
_expect(card != null and card_rect.position.x >= 20.0 and card_rect.end.x <= viewport_size.x - 20.0, "Roomy chooser remains inside the viewport width")
|
||||||
|
_expect(card != null and card_rect.position.y >= 20.0 and card_rect.end.y <= viewport_size.y - 20.0, "Roomy chooser remains inside the viewport height")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_narrow_viewport_fit() -> void:
|
||||||
|
if _chooser == null:
|
||||||
|
return
|
||||||
|
root.size = Vector2i(470, 417)
|
||||||
|
await _wait_frames(2)
|
||||||
|
_chooser.call("_apply_responsive_layout")
|
||||||
|
DIALOG_STYLER.refit(_chooser)
|
||||||
|
await _wait_frames(2)
|
||||||
|
var card: Control = _chooser.get_meta(DIALOG_STYLER.CARD_META) as Control
|
||||||
|
var viewport_size: Vector2 = root.get_visible_rect().size
|
||||||
|
var card_rect: Rect2 = card.get_global_rect() if card != null else Rect2()
|
||||||
|
var grid: GridContainer = _chooser.get("_choice_grid") as GridContainer
|
||||||
|
var preview_panel: Control = _chooser.get("_preview_panel") as Control
|
||||||
|
_expect(card != null and card_rect.position.x >= 12.0 and card_rect.end.x <= viewport_size.x - 12.0, "Narrow chooser clamps within horizontal safe margins")
|
||||||
|
_expect(card != null and card_rect.position.y >= 12.0 and card_rect.end.y <= viewport_size.y - 12.0, "Narrow chooser clamps within vertical safe margins")
|
||||||
|
_expect(card != null and card_rect.size.x < viewport_size.x and card_rect.size.y < viewport_size.y, "Narrow chooser remains a card instead of a full-scene sheet")
|
||||||
|
_expect(bool(_chooser.get("_is_compact_layout")) and grid != null and grid.columns == 3, "470×417 uses the compact three-column gallery")
|
||||||
|
_expect(preview_panel != null and not preview_panel.visible, "Compact gallery hides the redundant large preview")
|
||||||
|
var body_scroll: ScrollContainer = _chooser.get_meta(DIALOG_STYLER.BODY_SCROLL_META) as ScrollContainer
|
||||||
|
var body_rect: Rect2 = body_scroll.get_global_rect() if body_scroll != null else Rect2()
|
||||||
|
var all_choices_visible: bool = grid != null and grid.get_child_count() == 7 and body_scroll != null
|
||||||
|
if all_choices_visible:
|
||||||
|
for child: Node in grid.get_children():
|
||||||
|
var choice: Control = child as Control
|
||||||
|
var choice_rect: Rect2 = choice.get_global_rect()
|
||||||
|
if not choice.visible or choice_rect.position.x < body_rect.position.x - 1.0 or choice_rect.end.x > body_rect.end.x + 1.0 or choice_rect.position.y < body_rect.position.y - 1.0 or choice_rect.end.y > body_rect.end.y + 1.0:
|
||||||
|
all_choices_visible = false
|
||||||
|
break
|
||||||
|
_expect(all_choices_visible, "Compact gallery exposes all seven finish cards without scrolling")
|
||||||
|
var actions: HBoxContainer = _chooser.get_meta(DIALOG_STYLER.ACTIONS_META) as HBoxContainer
|
||||||
|
var close_button: Button = DIALOG_STYLER.visual_button(_chooser.get_cancel_button())
|
||||||
|
var draw_button: Button = DIALOG_STYLER.visual_button(_chooser.get_ok_button())
|
||||||
|
_expect(actions != null and close_button != null and draw_button != null, "Narrow chooser keeps both footer actions in its fixed footer")
|
||||||
|
_expect(close_button != null and close_button.get_global_rect().intersects(card_rect), "Narrow chooser keeps Close reachable inside the card")
|
||||||
|
_expect(draw_button != null and draw_button.get_global_rect().intersects(card_rect), "Narrow chooser keeps Draw Ground reachable inside the card")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_close_keeps_placement_unchanged() -> void:
|
||||||
|
if _chooser == null:
|
||||||
|
return
|
||||||
|
var close_button: Button = DIALOG_STYLER.visual_button(_chooser.get_cancel_button())
|
||||||
|
if close_button != null:
|
||||||
|
close_button.pressed.emit()
|
||||||
|
var placement_id: String = String(_stage.get("_placement_id"))
|
||||||
|
var new_finish: StringName = StringName(_stage.get("_new_ground_finish"))
|
||||||
|
var ground_button: Button = (_stage.get("_palette_buttons") as Dictionary).get("ground") as Button
|
||||||
|
_expect(not DIALOG_STYLER.is_open(_chooser), "Close dismisses the chooser")
|
||||||
|
_expect(placement_id.is_empty(), "Close does not arm Ground placement")
|
||||||
|
_expect(new_finish == &"finish_ground_grass", "Close discards the unconfirmed finish choice")
|
||||||
|
_expect(ground_button != null and not ground_button.button_pressed, "Close restores the Ground card to its inactive state")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_escape_keeps_placement_unchanged() -> void:
|
||||||
|
if _chooser == null:
|
||||||
|
return
|
||||||
|
var placement_id: String = String(_stage.get("_placement_id"))
|
||||||
|
var ground_button: Button = (_stage.get("_palette_buttons") as Dictionary).get("ground") as Button
|
||||||
|
_expect(not DIALOG_STYLER.is_open(_chooser), "Escape dismisses the chooser")
|
||||||
|
_expect(placement_id.is_empty(), "Escape does not arm Ground placement")
|
||||||
|
_expect(ground_button != null and not ground_button.button_pressed, "Escape restores the Ground card to its inactive state")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_finish_selection_is_a_draft() -> void:
|
||||||
|
if _chooser == null:
|
||||||
|
return
|
||||||
|
var dirt_button: Button = _chooser.choice_buttons().get(&"finish_ground_dirt") as Button
|
||||||
|
if dirt_button != null:
|
||||||
|
dirt_button.set_pressed_no_signal(true)
|
||||||
|
dirt_button.pressed.emit()
|
||||||
|
_expect(_chooser.selected_finish() == &"finish_ground_dirt", "Choosing Dirt updates the draft finish")
|
||||||
|
_expect(_chooser.selected_preview_finish() == &"finish_ground_dirt", "Choosing Dirt updates the large preview")
|
||||||
|
_expect(_chooser.get_ok_button().text == "Draw Dirt Ground", "Choosing Dirt updates the primary action")
|
||||||
|
_expect(StringName(_stage.get("_new_ground_finish")) == &"finish_ground_grass", "Draft selection does not mutate the active Ground finish")
|
||||||
|
_expect(String(_stage.get("_placement_id")).is_empty(), "Draft selection does not start placement")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_draw_commits_selected_finish() -> void:
|
||||||
|
if _chooser == null:
|
||||||
|
return
|
||||||
|
var draw_button: Button = DIALOG_STYLER.visual_button(_chooser.get_ok_button())
|
||||||
|
if draw_button != null:
|
||||||
|
draw_button.pressed.emit()
|
||||||
|
var painter: StageTerrainPainter = _stage.get("_terrain_painter") as StageTerrainPainter
|
||||||
|
_expect(not DIALOG_STYLER.is_open(_chooser), "Draw Ground closes the chooser")
|
||||||
|
_expect(StringName(_stage.get("_new_ground_finish")) == &"finish_ground_dirt", "Draw Ground commits the selected finish")
|
||||||
|
_expect(String(_stage.get("_placement_id")) == "ground", "Draw Ground enters the existing Ground placement tool")
|
||||||
|
_expect(painter != null and StringName(painter.get("_ground_finish")) == &"finish_ground_dirt", "Ground painter receives the selected finish")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_ground_ghost_is_armed() -> void:
|
||||||
|
var ghost: Node2D = _stage.get("_ghost") as Node2D
|
||||||
|
_expect(ghost != null and is_instance_valid(ghost), "Draw Ground shows the cursor-following Ground ghost")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_ground_click_commits_selected_finish() -> void:
|
||||||
|
var stage_position := Vector2(900.0, 420.0)
|
||||||
|
var press := InputEventMouseButton.new()
|
||||||
|
press.position = stage_position
|
||||||
|
press.global_position = stage_position
|
||||||
|
press.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
press.pressed = true
|
||||||
|
_stage._unhandled_input(press)
|
||||||
|
var release := InputEventMouseButton.new()
|
||||||
|
release.position = stage_position
|
||||||
|
release.global_position = stage_position
|
||||||
|
release.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
release.pressed = false
|
||||||
|
_stage._unhandled_input(release)
|
||||||
|
await _wait_frames(3)
|
||||||
|
var placed_dirt_ground: bool = false
|
||||||
|
var world: Node2D = _stage.get_node("World") as Node2D
|
||||||
|
if world != null:
|
||||||
|
for child: Node in world.get_children():
|
||||||
|
if child is TerrainBlock and (child as TerrainBlock).spawn_id == "ground" and (child as TerrainBlock).finish_id == &"finish_ground_dirt":
|
||||||
|
placed_dirt_ground = true
|
||||||
|
break
|
||||||
|
_expect(placed_dirt_ground, "Production Ground input places the selected Dirt finish")
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_frames(count: int) -> void:
|
||||||
|
for _frame_index: int in count:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _save_capture(file_name: String) -> void:
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
return
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var capture: Image = root.get_texture().get_image()
|
||||||
|
var path: String = ProjectSettings.globalize_path("res://tests/" + file_name)
|
||||||
|
var error: Error = capture.save_png(path)
|
||||||
|
_expect(error == OK, "render capture saved for visual inspection: %s" % path)
|
||||||
|
print("CAPTURE: %s size=%s result=%s" % [path, str(capture.get_size()), str(error)])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect(condition: bool, description: String) -> void:
|
||||||
|
_checks += 1
|
||||||
|
if condition:
|
||||||
|
print("PASS: %s" % description)
|
||||||
|
else:
|
||||||
|
_failures += 1
|
||||||
|
push_error("FAIL: %s" % description)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1fx16sotrdpp
|
||||||
@@ -20,8 +20,9 @@ func _check(label: String, value: bool) -> void:
|
|||||||
push_error("FAIL: %s" % label)
|
push_error("FAIL: %s" % label)
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1200, 800)
|
||||||
var theme := THEME.new()
|
var theme := THEME.new()
|
||||||
_check("default inspector background is #262C39", theme.inspector_background_color() == Color("#262C39"))
|
_check("default inspector background uses shared surface token", theme.inspector_background_color().is_equal_approx(Color("#252B38FA")))
|
||||||
var preview := PREVIEW_SCENE.instantiate() as Control
|
var preview := PREVIEW_SCENE.instantiate() as Control
|
||||||
root.add_child(preview)
|
root.add_child(preview)
|
||||||
await process_frame
|
await process_frame
|
||||||
@@ -39,7 +40,6 @@ func _run() -> void:
|
|||||||
_check("front pose stops gait", int(preview.get("_motion")) == 0 and bool(preview.get("_walk_paused")))
|
_check("front pose stops gait", int(preview.get("_motion")) == 0 and bool(preview.get("_walk_paused")))
|
||||||
preview.queue_free()
|
preview.queue_free()
|
||||||
var editor := CANVAS_SCENE.instantiate() as Control
|
var editor := CANVAS_SCENE.instantiate() as Control
|
||||||
editor.size = Vector2(1200.0, 800.0)
|
|
||||||
root.add_child(editor)
|
root.add_child(editor)
|
||||||
await process_frame
|
await process_frame
|
||||||
var canvas := editor.get("_canvas") as PropCanvas
|
var canvas := editor.get("_canvas") as PropCanvas
|
||||||
@@ -95,12 +95,12 @@ func _run() -> void:
|
|||||||
var eye_data: Dictionary = PROP_FILE.load("res://content/props/eye.prop")
|
var eye_data: Dictionary = PROP_FILE.load("res://content/props/eye.prop")
|
||||||
_check("eye prop fixture is available", not eye_data.is_empty())
|
_check("eye prop fixture is available", not eye_data.is_empty())
|
||||||
editor.call("_apply_json_data", eye_data)
|
editor.call("_apply_json_data", eye_data)
|
||||||
editor.size = Vector2(1200.0, 600.0)
|
root.size = Vector2i(1200, 600)
|
||||||
canvas.set_edit_mode(PropCanvas.EditMode.ATTACH)
|
canvas.set_edit_mode(PropCanvas.EditMode.ATTACH)
|
||||||
await process_frame
|
await process_frame
|
||||||
await process_frame
|
await process_frame
|
||||||
var top_bar := editor.get_node("TopBar") as Control
|
var top_bar := editor.get_node("ChromeRoot/TopBar") as Control
|
||||||
var status_bar := editor.get_node("StatusBar") as Control
|
var status_bar := editor.get_node("ChromeRoot/StatusBar") as Control
|
||||||
var holding_preview := editor.get("_attach_preview") as Control
|
var holding_preview := editor.get("_attach_preview") as Control
|
||||||
_check("Attach layout stays between name and status bars", main_split.get_global_rect().position.y >= top_bar.get_global_rect().end.y - 1.0 and holding_preview.get_global_rect().end.y <= status_bar.get_global_rect().position.y + 1.0)
|
_check("Attach layout stays between name and status bars", main_split.get_global_rect().position.y >= top_bar.get_global_rect().end.y - 1.0 and holding_preview.get_global_rect().end.y <= status_bar.get_global_rect().position.y + 1.0)
|
||||||
var preview_container := holding_preview.get("_viewport_container") as SubViewportContainer
|
var preview_container := holding_preview.get("_viewport_container") as SubViewportContainer
|
||||||
|
|||||||
@@ -146,25 +146,24 @@ func _test_ui_tokens() -> void:
|
|||||||
var theme: PropTheme = PROP_THEME.new()
|
var theme: PropTheme = PROP_THEME.new()
|
||||||
theme.load_from(PROP_THEME.DEFAULT_PATH)
|
theme.load_from(PROP_THEME.DEFAULT_PATH)
|
||||||
check("font_body 18", theme.font_body() == 18)
|
check("font_body 18", theme.font_body() == 18)
|
||||||
check("font_header 24", theme.font_header() == 24)
|
check("font_header 22", theme.font_header() == 22)
|
||||||
check("font_menu 22", theme.font_menu() == 22)
|
check("font_menu 20", theme.font_menu() == 20)
|
||||||
check("font_menu_bar 22", theme.font_menu_bar() == 22)
|
check("font_menu_bar 20", theme.font_menu_bar() == 20)
|
||||||
check("font_mode 26", theme.font_mode() == 26)
|
check("font_mode 24", theme.font_mode() == 24)
|
||||||
check("font_status 18", theme.font_status() == 18)
|
check("font_status 16", theme.font_status() == 16)
|
||||||
check("font_prop_name 20", theme.font_prop_name() == 20)
|
check("font_prop_name 20", theme.font_prop_name() == 20)
|
||||||
check("font_context_menu 22", theme.font_context_menu() == 22)
|
check("font_context_menu 20", theme.font_context_menu() == 20)
|
||||||
check("button_min_height 36", theme.button_min_height() == 36)
|
check("button_min_height 44", theme.button_min_height() == 44)
|
||||||
check("row_min_height 36", theme.row_min_height() == 36)
|
check("row_min_height 44", theme.row_min_height() == 44)
|
||||||
check("mode_button_min_height 56", theme.mode_button_min_height() == 56)
|
check("mode_button_min_height 56", theme.mode_button_min_height() == 56)
|
||||||
check("text_color token", theme.text_color().is_equal_approx(Color.from_string("#F2F2F2", Color.WHITE)))
|
check("text_color token", theme.text_color().is_equal_approx(Color.from_string("#F5F7FA", Color.WHITE)))
|
||||||
check("text_muted token", theme.text_muted_color().is_equal_approx(Color.from_string("#9A9A9A", Color.WHITE)))
|
check("text_muted token", theme.text_muted_color().is_equal_approx(Color.from_string("#AAB2C0", Color.WHITE)))
|
||||||
check("border token", theme.border_color().is_equal_approx(Color.from_string("#FFFFFF33", Color.WHITE)))
|
check("border token", theme.border_color().is_equal_approx(Color.from_string("#FFFFFF33", Color.WHITE)))
|
||||||
check("accent token", theme.accent_color().is_equal_approx(Color.from_string("#4FA3FF", Color.WHITE)))
|
check("accent token", theme.accent_color().is_equal_approx(Color.from_string("#22C6FF", Color.WHITE)))
|
||||||
var path := "user://prop_theme_test_ui.json"
|
var path := "user://prop_theme_test_ui.json"
|
||||||
_write_user_theme('{"version":"1.0","ui":{"fonts":{"body":24},"colors":{"accent":"#FF8800"}}}', path)
|
_write_user_theme('{"version":"1.0","ui":{"fonts":{"body":24},"colors":{"accent":"#FF8800"}}}', path)
|
||||||
var custom: PropTheme = PROP_THEME.new()
|
var custom: PropTheme = PROP_THEME.new()
|
||||||
custom.load_from(path)
|
custom.load_from(path)
|
||||||
check("ui font override", custom.font_body() == 24)
|
check("legacy PropTheme ui font does not override shared tokens", custom.font_body() == 18)
|
||||||
check("unoverridden ui token falls back", custom.font_header() == 24)
|
check("legacy PropTheme ui accent does not override shared tokens", custom.accent_color().is_equal_approx(Color.from_string("#22C6FF", Color.WHITE)))
|
||||||
check("ui accent override", custom.accent_color().is_equal_approx(Color.from_string("#FF8800", Color.WHITE)))
|
|
||||||
DirAccess.remove_absolute(path)
|
DirAccess.remove_absolute(path)
|
||||||
|
|||||||