Add StageBackgroundView and related tests for plain background functionality

- Implemented StageBackgroundView class for managing sky and plain backgrounds in the stage.
- Added functionality to set background type and color, bind camera, and apply themes.
- Created test_phase19_plain_background.gd to validate plain background behavior and migration.
- Developed test_phase19_starter_chooser_layout.gd to ensure proper layout and functionality of the starter chooser.
- Included visual capture tests for UI elements and interactions.
This commit is contained in:
2026-10-22 16:18:25 -04:00
parent db677dc194
commit 59f1817063
43 changed files with 2403 additions and 160 deletions
+204 -25
View File
@@ -50,7 +50,7 @@ const STAGE_MODAL_VIEWS_SCRIPT := preload("res://scripts/editors/stage/stage_mod
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_SCENERY_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_scenery_controller.gd")
const STAGE_SKY_VIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_sky_view.gd")
const STAGE_BACKGROUND_VIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_background_view.gd")
const STAGE_DRAW_ORDER_SCRIPT := preload("res://scripts/stage/stage_draw_order.gd")
const STAGE_WALL_PANEL_SCRIPT := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
const STAGE_BACKGROUND_GROUND_SCRIPT := preload("res://scripts/stage/scenery/stage_background_ground.gd")
@@ -258,13 +258,21 @@ var _spawner: StageSpawner
var _selection: StageSelection
var _gizmos: StageGizmos
var _stage_input_ready: bool = false
var _startup_scene_applied: bool = false
var _startup_starter_chooser_pending: bool = false
var _startup_starter_chooser_shown: bool = false
var _grid: StageGrid
var _spatial_index: StageSpatialIndex
var _terrain_painter: StageTerrainPainter
var _scenery_controller: StageSceneryController
var _sky_layer: CanvasLayer
var _sky_view: StageSkyView
var _background_view: StageBackgroundView
var _sky_id: StringName = &"sky_neutral"
var _background_kind: StringName = &"sky"
var _plain_color: Color = Color("#F5EFE0")
var _plain_color_draft_active: bool = false
var _plain_color_draft: Color = Color("#F5EFE0")
var _new_ground_finish: StringName = &"finish_ground_grass"
var _hosted_prop_editor: PropEditor
var _hosted_artwork_path: String = ""
@@ -1850,6 +1858,8 @@ func set_mode(mode: StageMode) -> void:
_show_toast("Finish or cancel speech editing first.")
_sync_mode_buttons()
return
if _plain_color_draft_active:
_close_selector()
_cancel_surface_color_preview()
if _scenery_controller != null:
if _scenery_controller.is_group_preview_pending():
@@ -2513,6 +2523,74 @@ func _mark_scene_dirty() -> void:
_paused_edit_autosave_remaining = 1.0
func _refresh_background_view() -> void:
if _background_view != null:
var displayed_color: Color = _plain_color_draft if _plain_color_draft_active else _plain_color
_background_view.set_background(_background_kind, _sky_id, displayed_color)
_sky_view = _background_view.get_sky_view()
_update_grid_background_style()
func _commit_background_kind(kind: StringName, sky_id: StringName = &"") -> void:
var next_kind: StringName = &"plain" if kind == &"plain" else &"sky"
var next_sky: StringName = sky_id if not sky_id.is_empty() else _sky_id
var changed: bool = _background_kind != next_kind or _sky_id != next_sky
_background_kind = next_kind
_sky_id = next_sky
_refresh_background_view()
if _asset_browser != null:
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
if changed:
_mark_scene_dirty()
func _preview_plain_color(color: Color) -> void:
_plain_color_draft = _opaque_stage_color(color)
_plain_color_draft_active = true
_refresh_background_view()
func _cancel_plain_color_preview() -> void:
if not _plain_color_draft_active:
return
_plain_color_draft_active = false
_plain_color_draft = _plain_color
_refresh_background_view()
func _commit_plain_color(color: Color) -> void:
var next_color: Color = _opaque_stage_color(color)
var changed: bool = _canonical_surface_color(_plain_color) != _canonical_surface_color(next_color)
_plain_color = next_color
_plain_color_draft = next_color
_plain_color_draft_active = false
_refresh_background_view()
_record_recent_surface_color(next_color)
if _asset_browser != null:
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
if changed:
_mark_scene_dirty()
func _opaque_stage_color(color: Color) -> Color:
return Color(color.r, color.g, color.b, 1.0)
func _update_grid_background_style() -> void:
if _grid == null:
return
var displayed_color: Color = _plain_color_draft if _plain_color_draft_active else _plain_color
var plain_active: bool = _background_kind == &"plain"
_grid.set_background_treatment(
plain_active,
displayed_color,
_sandbox_theme.color("grid.plain_light_minor", Color("#11182120")) if _sandbox_theme != null else Color("#11182120"),
_sandbox_theme.color("grid.plain_light_major", Color("#11182148")) if _sandbox_theme != null else Color("#11182148"),
_sandbox_theme.color("grid.plain_dark_minor", Color("#FFFFFF25")) if _sandbox_theme != null else Color("#FFFFFF25"),
_sandbox_theme.color("grid.plain_dark_major", Color("#FFFFFF58")) if _sandbox_theme != null else Color("#FFFFFF58")
)
func _set_scene_dirty(value: bool) -> void:
if _document != null:
_document.set_dirty(value)
@@ -2533,6 +2611,8 @@ func _collect_scene_data(is_autosave: bool = false) -> Dictionary:
var document_metadata: Dictionary = {
"scene_name": _scene_name,
"sky_id": String(_sky_id),
"background_kind": String(_background_kind),
"plain_color": _canonical_surface_color(_plain_color),
"metadata": _document.metadata_for_snapshot(is_autosave),
}
return _scene_adapter.collect(_world, _authored, _event_rules, camera_state, document_metadata)
@@ -2613,6 +2693,7 @@ func _validate_queues_for_save() -> String:
func _reset_scene_content() -> void:
set_mode(StageMode.EDIT)
_cancel_plain_color_preview()
_cancel_surface_color_preview()
if _scenery_erase_piece_dialog != null:
_scenery_erase_piece_dialog.hide()
@@ -2622,8 +2703,9 @@ func _reset_scene_content() -> void:
_cancel_scenery_repair_preview()
_scenery_repair_target = null
_sky_id = &"sky_neutral"
if _sky_view != null:
_sky_view.set_sky(_sky_id)
_background_kind = &"sky"
_plain_color = Color("#F5EFE0")
_refresh_background_view()
if _scenery_controller != null:
_scenery_controller.exit_tool()
_scenery_controller.cancel_group()
@@ -2655,11 +2737,19 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
source_path = CONTENT_PATHS.canonicalize(source_path)
if _scene_adapter == null:
return false
if _plain_color_draft_active:
_close_selector()
_cancel_plain_color_preview()
var was_suppressed: bool = _suppress_dirty
_suppress_dirty = true
_reset_scene_content()
var adapter_result: Dictionary = _scene_adapter.apply(data, _world, _spawner)
_sky_id = StringName(String(adapter_result.get("sky_id", "sky_neutral")))
_background_kind = StringName(String(adapter_result.get("background_kind", "sky")))
_plain_color = STAGE_SCENE_IO.color_from_data(
adapter_result.get("plain_color", STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR),
Color("#F5EFE0")
)
var loaded_nodes: Array[Node2D] = adapter_result.get("loaded_nodes", [])
for node: Node2D in loaded_nodes:
_finalize_placed_node(node)
@@ -2683,8 +2773,7 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
var camera_data: Dictionary = raw_camera as Dictionary if raw_camera is Dictionary else {}
_camera.global_position = STAGE_SCENE_IO.data_to_vector(camera_data.get("position"), DEFAULT_CAMERA_POSITION)
_camera.zoom = Vector2.ONE * clampf(float(camera_data.get("zoom", DEFAULT_CAMERA_ZOOM)), min_zoom, max_zoom)
if _sky_view != null:
_sky_view.set_sky(_sky_id)
_refresh_background_view()
var raw_metadata: Variant = data.get("metadata", {})
var metadata: Dictionary = raw_metadata as Dictionary if raw_metadata is Dictionary else {}
if _document != null:
@@ -2712,6 +2801,7 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
_add_recent_scene(source_path)
if _document != null:
_document.remove_autosave()
_startup_scene_applied = true
return true
func _show_scene_load_warnings(warnings: Array[String]) -> void:
@@ -2782,20 +2872,79 @@ func _update_scene_chrome() -> void:
_refresh_status()
func _check_autosave_recovery() -> void:
func _check_autosave_recovery(show_starter_chooser_after_discard: bool = false) -> void:
if _document == null:
if show_starter_chooser_after_discard:
_queue_startup_starter_chooser()
return
var inspection: Dictionary = _document.inspect_autosave()
if not bool(inspection.get("available", false)):
if show_starter_chooser_after_discard:
_queue_startup_starter_chooser()
return
var autosave_data: Dictionary = inspection.get("data", {}) as Dictionary
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.confirmed.connect(func() -> void: _apply_scene_data(autosave_data, AUTOSAVE_PATH, true); dialog.queue_free())
dialog.canceled.connect(func() -> void: _document.remove_autosave(); dialog.queue_free())
dialog.canceled.connect(func() -> void:
_document.remove_autosave()
dialog.queue_free()
if show_starter_chooser_after_discard:
_queue_startup_starter_chooser()
)
get_node(NodePath("UI")).add_child(dialog); _apply_dialog_theme(dialog); dialog.popup_centered()
func _queue_startup_starter_chooser() -> void:
if _startup_starter_chooser_pending or _startup_starter_chooser_shown:
return
_startup_starter_chooser_pending = true
call_deferred("_present_startup_starter_chooser")
func _present_startup_starter_chooser() -> void:
while _startup_starter_chooser_pending and is_inside_tree():
if not _stage_input_ready or _startup_modal_is_visible():
await get_tree().process_frame
continue
if get_tree().current_scene != self or _startup_scene_applied or current_mode != StageMode.EDIT or not _current_scene_path.is_empty() or _hosted_prop_editor != null:
_startup_starter_chooser_pending = false
return
_startup_starter_chooser_pending = false
_startup_starter_chooser_shown = true
_open_selector("starters")
return
_startup_starter_chooser_pending = false
func _startup_modal_is_visible() -> bool:
if _selector_open or _speech_composer_is_open():
return true
var ui_layer: CanvasLayer = _chrome.get_canvas_layer() if _chrome != null else null
if ui_layer != null:
for child: Node in ui_layer.get_children():
var window := child as Window
if window != null and window.visible:
return true
for dialog: Window in [
_cli_error_dialog,
_speech_recovery_dialog,
_speech_save_gate_dialog,
_scene_open_dialog,
_scene_save_dialog,
_unsaved_changes_dialog,
_rename_scene_dialog,
_missing_assets_dialog,
_missing_scenery_dialog,
_scenery_repair_confirm_dialog,
_group_preview_save_dialog,
_confirm_dialog,
]:
if dialog != null and is_instance_valid(dialog) and dialog.visible:
return true
return false
func _handle_cli_startup(args: Array) -> void:
if OS.get_environment("STICKMAN_WALK_DEBUG") == "1":
print("[WalkDebug] CLI_STARTUP args=%s" % [args])
@@ -2806,7 +2955,7 @@ func _handle_cli_startup(args: Array) -> void:
return
var scene_path := String(result.get("scene_path", ""))
if scene_path.is_empty():
_check_autosave_recovery()
_check_autosave_recovery(true)
return
if _document != null and _document.has_autosave():
_document.remove_autosave()
@@ -3085,6 +3234,9 @@ func _build_ui() -> void:
_asset_browser.asset_selected_requested.connect(_on_asset_selected)
_asset_browser.selector_cancel_requested.connect(_on_selector_cancelled)
_asset_browser.plain_color_preview_requested.connect(_preview_plain_color)
_asset_browser.plain_color_commit_requested.connect(_commit_plain_color)
_asset_browser.plain_color_preview_cancel_requested.connect(_cancel_plain_color_preview)
_asset_browser.asset_load_failed.connect(func(kind: String, path: String) -> void:
if _scenery_repair_target != null and is_instance_valid(_scenery_repair_target):
_show_toast("Could not open that artwork. Try another file.")
@@ -3097,6 +3249,7 @@ func _build_ui() -> void:
_asset_browser.edit_scenery_requested.connect(_enter_scenery_edit_mode)
_asset_browser.surface_color_confirmed.connect(_record_recent_surface_color)
_asset_browser.set_surface_color_history(_recent_surface_colors)
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
_build_ground_finish_popup(ui)
_modal_views.build_file_dialogs(ui)
@@ -4510,6 +4663,8 @@ func _build_gizmo_layer() -> void:
_gizmos = STAGE_GIZMOS.new()
_gizmos.name = "GizmoLayer"
_gizmos.camera = _camera
if _sandbox_theme != null:
_gizmos.apply_theme(_sandbox_theme as SandboxTheme)
_gizmos.transform_committed.connect(_on_transform_committed)
_gizmos.transform_started.connect(_on_gizmo_transform_started)
_gizmos.transform_updated.connect(_on_gizmo_transform_updated)
@@ -4522,6 +4677,7 @@ func _build_grid_layer() -> void:
_grid.camera = _camera
add_child(_grid)
move_child(_grid, 0)
_update_grid_background_style()
func _build_ghost_holder() -> void:
@@ -4542,15 +4698,19 @@ func _build_scenery_controller() -> void:
func _build_sky_layer() -> void:
_sky_layer = CanvasLayer.new()
_sky_layer.name = "StageSkyLayer"
_sky_layer.name = "StageBackgroundLayer"
_sky_layer.layer = -1
_sky_view = STAGE_SKY_VIEW_SCRIPT.new() as StageSkyView
_sky_view.name = "StageSkyView"
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_sky_view.set_sky(_sky_id)
_sky_layer.add_child(_sky_view)
_background_view = STAGE_BACKGROUND_VIEW_SCRIPT.new() as StageBackgroundView
_background_view.name = "StageBackgroundView"
_background_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_background_view.set_background(_background_kind, _sky_id, _plain_color)
_sky_layer.add_child(_background_view)
add_child(_sky_layer)
_sky_view.bind_camera(_camera)
_background_view.bind_camera(_camera)
_sky_view = _background_view.get_sky_view()
if _sandbox_theme != null:
_background_view.apply_theme(_sandbox_theme as SandboxTheme)
_update_grid_background_style()
func _build_placement_overlay() -> void:
@@ -4912,7 +5072,13 @@ func _open_selector(kind: String) -> void:
(_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind)
if kind == "scenery":
_asset_browser.set_surface_color_history(_recent_surface_colors)
var selector_state: Dictionary = _asset_browser.open_selector(kind, _spawner.get_selected_stickman_path(), _sky_id)
var selector_state: Dictionary = _asset_browser.open_selector(
kind,
_spawner.get_selected_stickman_path(),
_sky_id,
_background_kind,
_plain_color
)
if kind == "stickman":
var selected_stickman_path: String = String(selector_state.get("selected_stickman_path", _spawner.get_selected_stickman_path()))
if selected_stickman_path != _spawner.get_selected_stickman_path():
@@ -4940,12 +5106,14 @@ func _on_asset_selected(entry: Dictionary) -> void:
return
var background_kind: String = String(entry.get("background_kind", ""))
if background_kind == "sky":
_sky_id = StringName(String(entry.get("id", "sky_neutral")))
_sky_view.set_sky(_sky_id)
var scenery_selector: AssetSelector = _asset_browser.get_selector()
if scenery_selector != null:
scenery_selector.set_current_sky(_sky_id)
_mark_scene_dirty()
_commit_background_kind(&"sky", StringName(String(entry.get("id", "sky_neutral"))))
_refresh_scenery_capture_bar()
return
if background_kind == "plain":
_commit_background_kind(&"plain")
var plain_selector: AssetSelector = _asset_browser.get_selector()
if plain_selector != null:
plain_selector.open_plain_background_choices(_plain_color)
_refresh_scenery_capture_bar()
return
if background_kind == "wall":
@@ -5774,8 +5942,15 @@ func _handle_scenery_edit_motion(world_position: Vector2) -> void:
func _close_selector() -> void:
var closing_kind: String = _selector_kind
_cancel_plain_color_preview()
if _asset_browser != null:
_asset_browser.close()
if closing_kind == "scenery":
# Scenery is a chooser entry; the capture bar communicates active drawing tools.
var scenery_button: Button = _palette_buttons.get("scenery") as Button
if scenery_button != null:
scenery_button.set_pressed_no_signal(false)
func _on_selector_cancelled() -> void:
@@ -5783,7 +5958,7 @@ func _on_selector_cancelled() -> void:
return
var kind: String = _selector_kind
_close_selector()
if kind != "scenery" and kind != "starters":
if kind != "starters":
set_placement_mode("")
@@ -7900,8 +8075,12 @@ func _load_font(path: String) -> Font:
return _theme_controller.load_font(path)
func _apply_theme_to_visuals() -> void:
if _sky_view != null and _sandbox_theme != null:
_sky_view.apply_theme(_sandbox_theme as SandboxTheme)
if _background_view != null and _sandbox_theme != null:
_background_view.apply_theme(_sandbox_theme as SandboxTheme)
if _grid != null:
_update_grid_background_style()
if _gizmos != null and _sandbox_theme != null:
_gizmos.apply_theme(_sandbox_theme as SandboxTheme)
if _director_visuals != null:
_director_visuals.set_style(_theme)
_director_visuals.completed_path_alpha = _sandbox_theme.number("overlays.completed_path_alpha", 0.20, 0.0, 1.0) if _sandbox_theme != null else 0.20
+51 -5
View File
@@ -9,6 +9,9 @@ signal refresh_requested
signal make_artwork_requested
signal edit_scenery_requested
signal surface_color_confirmed(color: Color)
signal plain_color_preview_requested(color: Color)
signal plain_color_commit_requested(color: Color)
signal plain_color_preview_cancel_requested()
const ASSET_SELECTOR_SCENE := preload("res://scenes/stage/asset_selector.tscn")
const STICKMAN_LIBRARY_SCRIPT := preload("res://scripts/characters/stickman/stickman_library.gd")
@@ -32,6 +35,9 @@ var browse_dialog: FileDialog
var thumbnail_queue: Array[Dictionary] = []
var thumbnail_busy: bool = false
var thumbnail_generation: int = 0
var _current_background_kind: StringName = &"sky"
var _current_sky_id: StringName = &"sky_neutral"
var _current_plain_color: Color = Color("#F5EFE0")
func _ready() -> void:
@@ -62,6 +68,10 @@ func build_ui(ui_layer: CanvasLayer, ui_font: Font, emoji_font: Font, sandbox_th
selector.make_artwork_requested.connect(func() -> void: make_artwork_requested.emit())
selector.edit_scenery_requested.connect(func() -> void: edit_scenery_requested.emit())
selector.surface_color_confirmed.connect(func(color: Color) -> void: surface_color_confirmed.emit(color))
selector.plain_color_preview_requested.connect(func(color: Color) -> void: plain_color_preview_requested.emit(color))
selector.plain_color_commit_requested.connect(func(color: Color) -> void: plain_color_commit_requested.emit(color))
selector.plain_color_preview_cancel_requested.connect(func() -> void: plain_color_preview_cancel_requested.emit())
selector.stage_preview_dimming_changed.connect(_on_selector_stage_preview_dimming_changed)
ui_layer.add_child(selector)
# apply_font AFTER add_child so the selector's @onready nodes resolve first.
selector.apply_font(ui_font, emoji_font)
@@ -92,12 +102,28 @@ func set_surface_color_history(colors: Array[Color]) -> void:
selector.set_surface_color_history(colors)
func set_background_state(kind: StringName, sky_id: StringName, plain_color: Color) -> void:
_current_background_kind = &"plain" if kind == &"plain" else &"sky"
_current_sky_id = sky_id
_current_plain_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
if selector != null:
selector.set_background_state(_current_background_kind, _current_sky_id, _current_plain_color)
func _starter_scene_entries() -> Array[Dictionary]:
return [
{
"id": "starter_plain_background",
"name": "Plain Background",
"description": "An empty stage with one color. Choose a new color whenever you like.",
"category": "Make your own",
"path": "res://content/stages/starters/plain_background.scene",
"icon": "🎨",
},
{
"id": "starter_blank_scene",
"name": "Blank Scene",
"description": "Start fresh with an open stage and a clear sky.",
"name": "Open Sky",
"description": "An empty stage with a soft sky. Sun and clouds, ready for your story.",
"category": "Make your own",
"path": "res://content/stages/starters/blank.scene",
"icon": "✨",
@@ -129,7 +155,16 @@ func make_browse_entry(kind: String, path: String) -> Dictionary:
return stickman_library.make_entry(path)
func open_selector(kind: String, current_stickman_path: String, current_sky_id: StringName = &"") -> Dictionary:
func open_selector(
kind: String,
current_stickman_path: String,
current_sky_id: StringName = &"",
background_kind: StringName = &"sky",
plain_color: Color = Color("#F5EFE0")
) -> Dictionary:
_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_plain_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
set_selector_kind(kind)
var entries: Array[Dictionary] = scan_entries(kind)
if kind == "starters":
@@ -142,7 +177,7 @@ func open_selector(kind: String, current_stickman_path: String, current_sky_id:
selected_stickman_path = String(entries[0].get("path", ""))
var auto_selection: Dictionary = entries[0] if kind == "stickman" and entries.size() == 1 else {}
if auto_selection.is_empty():
open(kind, entries, current_sky_id)
open(kind, entries, _current_sky_id)
return {
"entries": entries,
"selected_stickman_path": selected_stickman_path,
@@ -152,6 +187,7 @@ func open_selector(kind: String, current_stickman_path: String, current_sky_id:
func _scenery_background_entries() -> Array[Dictionary]:
return [
{"id": "plain_background", "name": "Plain Background", "category": "Scene background", "gallery_tab": "backgrounds", "background_kind": "plain", "plain_color": _current_plain_color, "icon": "🎨"},
{"id": "sky_clear_day", "name": "Clear Day", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "☀️"},
{"id": "sky_cloudy_day", "name": "Cloudy Day", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "☁️"},
{"id": "sky_sunset", "name": "Sunset", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌅"},
@@ -170,8 +206,9 @@ func open(kind: String, entries: Array[Dictionary], current_sky_id: StringName =
selector_open = true
if selector_dim != null:
selector_dim.visible = true
selector_dim.color = Color(0.0, 0.0, 0.0, SELECTOR_DIM_ALPHA)
if selector != null:
selector.open(kind, entries, String(current_sky_id))
selector.open(kind, entries, String(current_sky_id), String(_current_background_kind), _current_plain_color)
load_or_enqueue_thumbnails(kind, entries)
@@ -206,6 +243,8 @@ func set_entries(entries: Array[Dictionary]) -> void:
func refresh_selector_entries() -> Array[Dictionary]:
var entries: Array[Dictionary] = scan_entries(selector_kind)
if selector_kind == "scenery":
entries.append_array(_scenery_background_entries())
set_entries(entries)
return entries
@@ -282,6 +321,13 @@ func _on_selector_cancelled() -> void:
selector_cancel_requested.emit()
func _on_selector_stage_preview_dimming_changed(should_dim: bool) -> void:
if selector_dim == null:
return
selector_dim.color = Color(0.0, 0.0, 0.0, SELECTOR_DIM_ALPHA if should_dim else 0.0)
selector_dim.visible = selector_open
func _on_selector_popup_hide() -> void:
if not selector_open:
return
@@ -130,6 +130,8 @@ func collect(
"version": STAGE_SCENE_IO.FILE_VERSION,
"scene_name": scene_name,
"sky_id": String(document_metadata.get("sky_id", "sky_neutral")),
"background_kind": String(document_metadata.get("background_kind", "sky")),
"plain_color": String(document_metadata.get("plain_color", STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR)),
"camera": {"position": STAGE_SCENE_IO.vector_to_data(camera_position), "zoom": camera_zoom},
"objects": objects,
"event_rules": rules_result["rules"],
@@ -299,6 +301,8 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
"missing_paths": missing_paths,
"decoded_rules": decoded_rules,
"sky_id": String(data.get("sky_id", "sky_neutral")),
"background_kind": String(data.get("background_kind", "sky")),
"plain_color": String(data.get("plain_color", STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR)),
}
@@ -39,14 +39,18 @@ func configure(selected: Color, recents: Array[Color], presets: Array[Color], st
func set_selected_no_signal(color: Color) -> void:
_selected = color
_refresh_current_color()
_recent_swatches.set_selected(color)
_preset_swatches.set_selected(color)
if _recent_swatches != null:
_recent_swatches.set_selected(color)
if _preset_swatches != null:
_preset_swatches.set_selected(color)
func set_recent_colors(colors: Array[Color]) -> void:
if _recents == colors:
return
_recents = colors.duplicate()
if _recent_empty_label == null or _recent_swatches == null:
return
_recent_empty_label.visible = _recents.is_empty()
_recent_swatches.visible = not _recents.is_empty()
_recent_swatches.configure(_recents, _selected, _stage_theme)
@@ -99,6 +103,7 @@ func _build_current_color_section(parent: Control) -> void:
current_row.add_theme_constant_override("separation", _compact_gap())
parent.add_child(current_row)
_current_chip = Button.new()
_current_chip.name = "CurrentColorChip"
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48, 44, 96))
if _compact_mode:
swatch_size = maxf(swatch_size, 52.0)
@@ -116,7 +121,8 @@ func _build_current_color_section(parent: Control) -> void:
func _build_recent_color_section(parent: Control) -> void:
parent.add_child(_make_heading("Recently used"))
_recent_empty_label = Label.new()
_recent_empty_label.text = "No colors yet" if _compact_mode else "Colors you use will appear here"
_recent_empty_label.name = "RecentColorsEmptyLabel"
_recent_empty_label.text = "No colors yet" if _compact_mode else "Colors you use will appear here."
_recent_empty_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_recent_empty_label.add_theme_color_override("font_color", _color("global.muted_text_color", Color("#AAB2C0")))
if _compact_mode:
@@ -125,7 +131,7 @@ func _build_recent_color_section(parent: Control) -> void:
_recent_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
_recent_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if _compact_mode:
_recent_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8)
_recent_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 44, 44, 52), 8)
_recent_swatches.color_selected.connect(_on_swatch_selected)
parent.add_child(_recent_swatches)
@@ -135,7 +141,7 @@ func _build_palette_section(parent: Control) -> void:
_preset_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
_preset_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL
if _compact_mode:
_preset_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8)
_preset_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 44, 44, 52), 8)
_preset_swatches.color_selected.connect(_on_swatch_selected)
parent.add_child(_preset_swatches)
@@ -22,9 +22,11 @@ var _preferred_preview_height: float = 140.0
var _preferred_picker_min_size: Vector2 = Vector2(320.0, 340.0)
var _built: bool = false
var _suppress_color_signal: bool = false
var _plain_background_mode: bool = false
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
_plain_background_mode = false
_surface_kind = surface_kind
_finish_id = finish
_stage_theme = stage_theme
@@ -35,6 +37,19 @@ func configure(surface_kind: StringName, finish: StringName, color: Color, stage
_apply_preview_heading_theme()
func configure_plain_background(color: Color, stage_theme: SandboxTheme) -> void:
_plain_background_mode = true
_surface_kind = &"plain_background"
_finish_id = &""
_stage_theme = stage_theme
if not _built:
_build()
set_color_no_signal(Color(color.r, color.g, color.b, 1.0))
_color_picker.edit_alpha = false
theme = stage_theme.control_theme() if stage_theme != null else null
_apply_preview_heading_theme()
func get_color() -> Color:
return _color_picker.color if _color_picker != null else Color.WHITE
+53 -3
View File
@@ -15,6 +15,26 @@ var _label: Label = null
var _description: Label = null
var _status_label: Label = null
static var _rounded_preview_shader: Shader
const ROUNDED_PREVIEW_SHADER_CODE := """
shader_type canvas_item;
uniform float corner_radius_px = 12.0;
void fragment() {
vec4 preview_color = texture(TEXTURE, UV);
vec2 preview_size = vec2(1.0) / TEXTURE_PIXEL_SIZE;
vec2 half_size = preview_size * 0.5;
vec2 local_position = UV * preview_size;
vec2 rounded_box = abs(local_position - half_size) - (half_size - vec2(corner_radius_px));
float edge_distance = length(max(rounded_box, vec2(0.0))) + min(max(rounded_box.x, rounded_box.y), 0.0) - corner_radius_px;
float antialias_width = max(fwidth(edge_distance), 1.0);
preview_color.a *= 1.0 - smoothstep(0.0, antialias_width, edge_distance);
COLOR = preview_color;
}
"""
func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Variant = null) -> void:
_payload = payload.duplicate(true)
_drag_threshold = theme.number("palette.drag_threshold", 8.0, 1.0, 128.0) if theme != null else 8.0
@@ -32,7 +52,9 @@ func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Varia
_box.alignment = BoxContainer.ALIGNMENT_CENTER; _box.add_theme_constant_override("separation", 2); add_child(_box)
_icon = TextureRect.new(); _icon.custom_minimum_size = thumbnail_size; _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE; _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED; _icon.texture = icon_texture; _icon.visible = icon_texture != null; _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE; _box.add_child(_icon)
_glyph = Label.new(); _glyph.text = String(_payload.get("icon", "")); _glyph.visible = icon_texture == null; _glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE; _glyph.add_theme_font_size_override("font_size", 30); _box.add_child(_glyph)
_label = Label.new(); _label.text = String(_payload.get("label", "")); _label.custom_minimum_size = Vector2(0, 28); _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; _label.clip_text = true; _label.mouse_filter = Control.MOUSE_FILTER_IGNORE; _box.add_child(_label)
_label = Label.new(); _label.text = String(_payload.get("label", "")); _label.custom_minimum_size = Vector2(0, float(_payload.get("label_min_height", 28.0))); _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER; _label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; _label.clip_text = true; _label.mouse_filter = Control.MOUSE_FILTER_IGNORE; _box.add_child(_label)
if _payload.has("label_font_size"):
_label.add_theme_font_size_override("font_size", int(_payload["label_font_size"]))
var description: String = String(_payload.get("description", ""))
var status: String = String(_payload.get("status", ""))
if not status.is_empty():
@@ -56,6 +78,8 @@ func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Varia
_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_description.max_lines_visible = 2
_description.add_theme_font_size_override("font_size", 14)
if _payload.has("description_font_size"):
_description.add_theme_font_size_override("font_size", int(_payload["description_font_size"]))
_description.mouse_filter = Control.MOUSE_FILTER_IGNORE
_box.add_child(_description)
@@ -75,8 +99,34 @@ func set_preview_control(preview: Control) -> void:
preview.custom_minimum_size = preview_size
preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
_box.add_child(preview)
_box.move_child(preview, 0)
var preview_corner_radius: float = float(_payload.get("preview_corner_radius", 0.0))
if preview_corner_radius > 0.0:
var rounded_material := ShaderMaterial.new()
rounded_material.shader = _get_rounded_preview_shader()
rounded_material.set_shader_parameter("corner_radius_px", preview_corner_radius)
preview.material = rounded_material
var horizontal_inset: int = int(_payload.get("preview_horizontal_inset", 0))
var top_inset: int = int(_payload.get("preview_top_inset", 0))
if horizontal_inset > 0 or top_inset > 0:
var preview_frame := MarginContainer.new()
preview_frame.name = "PreviewInsetFrame"
preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_frame.add_theme_constant_override("margin_left", horizontal_inset)
preview_frame.add_theme_constant_override("margin_right", horizontal_inset)
preview_frame.add_theme_constant_override("margin_top", top_inset)
preview_frame.add_child(preview)
_box.add_child(preview_frame)
_box.move_child(preview_frame, 0)
else:
_box.add_child(preview)
_box.move_child(preview, 0)
func _get_rounded_preview_shader() -> Shader:
if _rounded_preview_shader == null:
_rounded_preview_shader = Shader.new()
_rounded_preview_shader.code = ROUNDED_PREVIEW_SHADER_CODE
return _rounded_preview_shader
func set_thumbnail(texture: Texture2D) -> void:
if _icon != null: _icon.texture = texture; _icon.visible = texture != null
+467 -64
View File
@@ -8,6 +8,10 @@ signal refresh_requested()
signal make_artwork_requested()
signal edit_scenery_requested()
signal surface_color_confirmed(color: Color)
signal plain_color_preview_requested(color: Color)
signal plain_color_commit_requested(color: Color)
signal plain_color_preview_cancel_requested()
signal stage_preview_dimming_changed(should_dim: bool)
const COLUMNS := 4
const ROWS := 3
@@ -49,11 +53,14 @@ var _filter_chip_group: ButtonGroup
var _artwork_filter_buttons: Array[Button] = []
var _tag_id: String = "all"
var _current_sky_id: String = ""
var _current_background_kind: StringName = &"sky"
var _current_plain_color: Color = Color("#F5EFE0")
var _sky_card_group: ButtonGroup
var _active_columns: int = COLUMNS
var _page_capacity: int = PAGE_SIZE
var _effective_cell_size: Vector2 = CELL_MIN_SIZE
var _surface_cell_size: Vector2 = CELL_MIN_SIZE
var _surface_row_heading: Label
var _surface_color_history: Array[Color] = []
var _setup_view: VBoxContainer
var _color_picker_view: StageSurfaceColorPickerView
@@ -69,6 +76,18 @@ var _setup_cancel_button: Button
var _setup_draw_button: Button
var _color_back_button: Button
var _color_use_button: Button
var _plain_color_view: VBoxContainer
var _plain_color_preview: StageSurfacePreview
var _plain_color_controls: StageSurfaceColorControls
var _plain_color_helper: Label
var _plain_color_choices_open: bool = false
var _plain_color_original: Color = Color("#F5EFE0")
var _plain_color_draft: Color = Color("#F5EFE0")
var _color_picker_owner: StringName = &"surface"
var _backgrounds_back_button: Button
var _plain_footer_row: HBoxContainer
var _plain_footer_column: VBoxContainer
var _stage_preview_is_dimmed: bool = true
@onready var _title_label: Label = %TitleLabel
@onready var _grid: GridContainer = %GridContainer
@@ -103,6 +122,8 @@ func _ready() -> void:
tab.toggle_mode = true
tab.button_group = _gallery_tab_group
_build_surface_setup_view()
_build_plain_background_view()
_build_surface_row_heading()
_rebuild_artwork_filter_chips()
_prev_button.pressed.connect(_on_prev)
_next_button.pressed.connect(_on_next)
@@ -113,6 +134,7 @@ func _ready() -> void:
_artwork_tab.pressed.connect(func() -> void: _set_gallery_tab("artwork"))
_make_artwork_button.pressed.connect(func() -> void: make_artwork_requested.emit())
_edit_scenery_button.pressed.connect(func() -> void: edit_scenery_requested.emit())
popup_hide.connect(_on_popup_hide)
get_tree().root.size_changed.connect(_on_root_size_changed)
_apply_chooser_visual_style()
@@ -123,15 +145,27 @@ func _on_root_size_changed() -> void:
_rebuild()
func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void:
func open(
p_kind: String,
entries: Array[Dictionary],
current_sky_id: String = "",
current_background_kind: String = "sky",
plain_color: Color = Color("#F5EFE0")
) -> void:
_cancel_plain_picker_preview(false)
kind = p_kind
_setup_kind = &""
_color_picker_open = false
_plain_color_choices_open = false
_setup_view.visible = false
_plain_color_view.visible = false
if _color_picker_view != null:
_color_picker_view.visible = false
_current_sky_id = current_sky_id if kind == "scenery" else ""
_current_background_kind = &"plain" if current_background_kind == "plain" else &"sky"
_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"))
_backgrounds_back_button.visible = false
_apply_chooser_visual_style()
_tabs.visible = kind == "scenery"
_filter_chips.visible = false
@@ -165,6 +199,47 @@ func set_surface_color_history(colors: Array[Color]) -> void:
_surface_color_history = colors.duplicate()
if _setup_color_controls != null and is_instance_valid(_setup_color_controls):
_setup_color_controls.set_recent_colors(_surface_color_history)
if _plain_color_controls != null and is_instance_valid(_plain_color_controls):
_plain_color_controls.set_recent_colors(_surface_color_history)
func set_background_state(background_kind: StringName, sky_id: StringName, plain_color: Color) -> void:
_current_background_kind = &"plain" if background_kind == &"plain" else &"sky"
_current_sky_id = String(sky_id)
_current_plain_color = _opaque_color(plain_color)
for entry: Dictionary in _entries:
if String(entry.get("background_kind", "")) == "plain":
entry["plain_color"] = _current_plain_color
if not _plain_color_choices_open and not (_color_picker_open and _color_picker_owner == &"plain"):
_plain_color_draft = _current_plain_color
if _plain_color_preview != null:
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
if _plain_color_controls != null:
_plain_color_controls.set_selected_no_signal(_current_plain_color)
if _color_picker_open and _color_picker_owner == &"plain" and _color_picker_view != null:
_color_picker_view.configure_plain_background(_current_plain_color, _sandbox_theme as SandboxTheme)
if kind == "scenery" and _gallery_tab == "backgrounds":
_rebuild()
func open_plain_background_choices(color: Color) -> void:
_current_plain_color = _opaque_color(color)
_plain_color_draft = _current_plain_color
_plain_color_choices_open = true
_color_picker_open = false
_color_picker_owner = &"plain"
_title_label.text = "Choose background color"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.visible = true
_plain_color_view.visible = true
_setup_view.visible = false
_color_picker_view.visible = false
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.configure(_current_plain_color, _surface_color_history, _preset_surface_colors(), _sandbox_theme as SandboxTheme)
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
call_deferred("_focus_plain_color_controls")
func set_thumbnail(entry: Dictionary, tex: Texture2D) -> void:
@@ -198,6 +273,7 @@ func apply_font(ui_font: Font, emoji_font: Font) -> void:
_prev_button, _next_button, _browse_button, _refresh_button, _close_button,
_edit_scenery_button, _make_artwork_button,
_backgrounds_tab, _artwork_tab,
_backgrounds_back_button, _plain_color_helper,
_setup_view.get_node_or_null("SetupHeading") if _setup_view != null else null,
get_meta("setup_helper", null) as Control,
_setup_cancel_button, _setup_draw_button,
@@ -226,8 +302,13 @@ func apply_theme(theme: Variant) -> void:
ground_choices.apply_theme(theme as SandboxTheme)
if _setup_color_controls != null:
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme)
if _plain_color_controls != null:
_plain_color_controls.configure(_current_plain_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme)
if _color_picker_view != null:
_color_picker_view.configure(_setup_kind, _setup_finish, _setup_color, theme as SandboxTheme)
if _color_picker_owner == &"plain":
_color_picker_view.configure_plain_background(_plain_color_draft, theme as SandboxTheme)
else:
_color_picker_view.configure(_setup_kind, _setup_finish, _setup_color, theme as SandboxTheme)
_apply_chooser_visual_style()
@@ -305,7 +386,7 @@ func _build_surface_setup_view() -> void:
_color_picker_view = SURFACE_COLOR_PICKER_VIEW.new() as StageSurfaceColorPickerView
_color_picker_view.name = "SurfaceColorPickerView"
_color_picker_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_color_picker_view.preview_color_changed.connect(_on_setup_color_preview)
_color_picker_view.preview_color_changed.connect(_on_color_picker_preview)
_color_picker_view.visible = false
_content_stack.add_child(_color_picker_view)
_setup_cancel_button = Button.new()
@@ -349,6 +430,77 @@ func _build_surface_setup_view() -> void:
_apply_font_to(choice)
func _build_plain_background_view() -> void:
_plain_color_view = VBoxContainer.new()
_plain_color_view.name = "PlainBackgroundColorChoices"
_plain_color_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_color_view.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.setup_gap", 8, 4, 24))
_plain_color_view.visible = false
_content_stack.add_child(_plain_color_view)
_plain_color_helper = Label.new()
_plain_color_helper.name = "PlainBackgroundHelper"
_plain_color_helper.text = "Tap a color to use it on the Stage."
_plain_color_helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_plain_color_helper.add_theme_color_override("font_color", _theme_color("global.muted_text_color", Color("#AAB2C0")))
_plain_color_view.add_child(_plain_color_helper)
var preview_frame := PanelContainer.new()
preview_frame.name = "PlainBackgroundPreviewFrame"
var preview_size: Vector2 = _theme_vector2("popups.asset_selector.surface_preview_size", Vector2(420.0, 132.0))
preview_size.y = minf(preview_size.y, 132.0)
preview_frame.custom_minimum_size.y = preview_size.y + 6.0
preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_color_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
_plain_color_preview.name = "PlainBackgroundPreview"
_plain_color_preview.set_rounded_frame(
float(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)),
_theme_color("popups.style_defaults.bg_color", Color("#202633FA"))
)
_plain_color_preview.custom_minimum_size = preview_size
_plain_color_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_frame.add_child(_plain_color_preview)
_plain_color_view.add_child(preview_frame)
_plain_color_controls = SURFACE_COLOR_CONTROLS.new() as StageSurfaceColorControls
_plain_color_controls.name = "PlainBackgroundColorControls"
_plain_color_controls.color_selected.connect(_on_plain_color_selected)
_plain_color_controls.custom_color_requested.connect(_open_plain_color_picker)
_plain_color_view.add_child(_plain_color_controls)
_backgrounds_back_button = Button.new()
_backgrounds_back_button.name = "BackgroundsBackButton"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.custom_minimum_size = Vector2(148.0, float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
_backgrounds_back_button.focus_mode = Control.FOCUS_ALL
_backgrounds_back_button.visible = false
_backgrounds_back_button.pressed.connect(_back_to_backgrounds)
_footer.add_child(_backgrounds_back_button)
_plain_footer_row = HBoxContainer.new()
_plain_footer_row.name = "PlainBackgroundFooterRow"
_plain_footer_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_footer_row.alignment = BoxContainer.ALIGNMENT_BEGIN
_plain_footer_row.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.footer_gap", 16, 8, 32))
_plain_footer_row.visible = false
_footer.add_child(_plain_footer_row)
_plain_footer_column = VBoxContainer.new()
_plain_footer_column.name = "PlainBackgroundFooterColumn"
_plain_footer_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_plain_footer_column.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.footer_gap", 16, 8, 32))
_plain_footer_column.visible = false
_footer.add_child(_plain_footer_column)
_apply_font_to(_plain_color_helper)
_apply_font_to(_backgrounds_back_button)
_apply_chooser_visual_style()
func _build_surface_row_heading() -> void:
_surface_row_heading = Label.new()
_surface_row_heading.name = "MoreBackgroundsHeading"
_surface_row_heading.text = "More backgrounds"
_surface_row_heading.visible = false
_surface_row_heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
_content_stack.add_child(_surface_row_heading)
_content_stack.move_child(_surface_row_heading, _surface_cards_row.get_index())
_apply_font_to(_surface_row_heading)
func _make_look_cue(kind: StringName, finish: StringName) -> Control:
var preview: StageSurfacePreview = SURFACE_PREVIEW.new() as StageSurfacePreview
var cue_size: Vector2 = _theme_vector2("popups.asset_selector.look_cue_size", Vector2(80.0, 42.0))
@@ -418,6 +570,14 @@ func _on_setup_color_selected(color: Color) -> void:
surface_color_confirmed.emit(_setup_color)
func _on_plain_color_selected(color: Color) -> void:
_current_plain_color = _opaque_color(color)
_plain_color_draft = _current_plain_color
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.set_selected_no_signal(_current_plain_color)
plain_color_commit_requested.emit(_current_plain_color)
func _update_setup_preview() -> void:
if _setup_preview == null:
return
@@ -437,6 +597,7 @@ func _open_setup_color_view() -> void:
_setup_original_color = _setup_color
_color_picker_view.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme)
_color_picker_open = true
_color_picker_owner = &"surface"
_title_label.text = "Choose color"
_rebuild()
_update_popup_layout()
@@ -444,12 +605,45 @@ func _open_setup_color_view() -> void:
_color_picker_view.focus_picker()
func _open_plain_color_picker() -> void:
if not _plain_color_choices_open:
return
_plain_color_original = _current_plain_color
_plain_color_draft = _current_plain_color
_color_picker_owner = &"plain"
_color_picker_view.configure_plain_background(_plain_color_draft, _sandbox_theme as SandboxTheme)
_color_back_button.text = "‹ Plain Background"
_plain_color_choices_open = false
_color_picker_open = true
_title_label.text = "Choose background color"
_backgrounds_back_button.visible = false
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
_color_picker_view.focus_picker()
func _on_setup_color_preview(color: Color) -> void:
_setup_color = _opaque_color(color)
_update_setup_preview()
func _on_color_picker_preview(color: Color) -> void:
if _color_picker_owner == &"plain":
_plain_color_draft = _opaque_color(color)
plain_color_preview_requested.emit(_plain_color_draft)
return
_on_setup_color_preview(color)
func _use_setup_color() -> void:
if _color_picker_owner == &"plain":
_current_plain_color = _opaque_color(_color_picker_view.get_color())
_plain_color_draft = _current_plain_color
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
plain_color_commit_requested.emit(_current_plain_color)
_close_plain_color_picker()
return
_setup_color = _opaque_color(_color_picker_view.get_color())
_setup_color_chosen = true
_update_setup_preview()
@@ -459,6 +653,9 @@ func _use_setup_color() -> void:
func _back_from_setup_color_view() -> void:
if _color_picker_owner == &"plain":
_cancel_plain_picker_preview(true)
return
_setup_color = _setup_original_color
_setup_color_controls.set_selected_no_signal(_setup_color)
_update_setup_preview()
@@ -475,6 +672,74 @@ func _close_setup_color_view() -> void:
call_deferred("_update_popup_layout_after_layout")
func _close_plain_color_picker() -> void:
_color_picker_open = false
_plain_color_choices_open = true
_title_label.text = "Choose background color"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.visible = true
_plain_color_view.visible = true
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.set_selected_no_signal(_current_plain_color)
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
call_deferred("_focus_plain_color_controls")
func _cancel_plain_picker_preview(return_to_choices: bool = true) -> void:
if not _color_picker_open or _color_picker_owner != &"plain":
return
_plain_color_draft = _current_plain_color
_color_picker_open = false
plain_color_preview_cancel_requested.emit()
if _color_picker_view != null:
_color_picker_view.configure_plain_background(_current_plain_color, _sandbox_theme as SandboxTheme)
if return_to_choices and kind == "scenery":
_plain_color_choices_open = true
_title_label.text = "Choose background color"
_backgrounds_back_button.text = "‹ Backgrounds"
_backgrounds_back_button.visible = true
_plain_color_view.visible = true
_plain_color_preview.configure(&"plain_background", &"", _current_plain_color)
_plain_color_controls.set_selected_no_signal(_current_plain_color)
_update_popup_layout(true)
_rebuild()
call_deferred("_update_popup_layout_after_layout")
call_deferred("_focus_plain_color_controls")
func _back_to_backgrounds() -> void:
if _color_picker_open and _color_picker_owner == &"plain":
_cancel_plain_picker_preview(true)
return
_plain_color_choices_open = false
_plain_color_view.visible = false
_backgrounds_back_button.visible = false
_title_label.text = "Choose the scene background"
_update_popup_layout(true)
_rebuild()
call_deferred("_focus_plain_background_card")
func _focus_plain_color_controls() -> void:
if _plain_color_controls != null and _plain_color_controls.visible:
_plain_color_controls.focus_custom_button()
func _focus_plain_background_card() -> void:
for node: Node in _grid.get_children():
var card: AssetCard = node as AssetCard
if card != null and String(card.get_meta("entry_id", "")) == "plain_background":
card.grab_focus()
return
func _on_popup_hide() -> void:
if _color_picker_open and _color_picker_owner == &"plain":
_cancel_plain_picker_preview(true)
func _opaque_color(color: Color) -> Color:
return Color(color.r, color.g, color.b, 1.0)
@@ -600,11 +865,17 @@ func _has_unsorted_artwork() -> bool:
func _set_gallery_tab(tab: String) -> void:
if _color_picker_open and _color_picker_owner == &"plain":
_cancel_plain_picker_preview(false)
_color_picker_open = false
_plain_color_choices_open = false
_setup_kind = &""
_setup_view.visible = false
_plain_color_view.visible = false
_color_picker_view.visible = false
_backgrounds_back_button.visible = false
_gallery_tab = tab
_title_label.text = "Choose the scene background" if tab == "backgrounds" else "Choose Scenery"
_backgrounds_tab.set_pressed_no_signal(tab == "backgrounds")
_artwork_tab.set_pressed_no_signal(tab == "artwork")
_filter_chips.visible = kind == "scenery" and tab == "artwork"
@@ -677,49 +948,70 @@ func _usable_screen_size() -> Vector2:
func _update_popup_layout(keep_visible_position: bool = false) -> void:
var previous_position: Vector2i = position
var in_color_picker: bool = _color_picker_open
var in_plain_choices: bool = _plain_color_choices_open and not in_color_picker
var in_setup: bool = not _setup_kind.is_empty() and not in_color_picker
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_setup and not in_color_picker
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_setup and not in_color_picker
var 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_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
var filtered: Array[Dictionary] = _filtered_entries()
_tabs.visible = kind == "scenery" and not in_color_picker
_tabs.visible = kind == "scenery" and not in_subview
_filter_chips.visible = scenery_artwork_gallery
_position_footer()
_close_button.visible = not in_setup and not in_color_picker
_setup_cancel_button.visible = in_setup
_setup_draw_button.visible = in_setup
_setup_view.visible = in_setup
_plain_color_view.visible = in_plain_choices
_backgrounds_back_button.visible = in_plain_choices
_color_picker_view.visible = in_color_picker
_surface_cards_row.visible = scenery_background_gallery and _has_surface_entries(filtered)
_surface_row_heading.visible = _surface_cards_row.visible
_color_back_button.visible = in_color_picker
_color_use_button.visible = in_color_picker
_edit_scenery_button.visible = kind == "scenery" and not in_setup and not in_color_picker
_footer_spacer.visible = scenery_artwork_gallery
_empty_label.visible = filtered.is_empty() and not in_setup and not in_color_picker
_grid.visible = not filtered.is_empty() and not in_setup and not in_color_picker
_grid_scroll.visible = not filtered.is_empty() or in_setup or in_color_picker
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery else (BoxContainer.ALIGNMENT_END if in_setup or in_color_picker else BoxContainer.ALIGNMENT_CENTER)
_empty_label.visible = 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
_footer.alignment = BoxContainer.ALIGNMENT_BEGIN if scenery_artwork_gallery or in_subview else BoxContainer.ALIGNMENT_CENTER
_footer.add_theme_constant_override(
"separation",
_theme_integer("popups.asset_selector.footer_gap", 16, 8, 32) if in_setup or in_color_picker or kind == "scenery" else 8
_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 width_ratio: float = 0.98 if in_setup else 0.94
var width_ratio: float = 0.98 if in_subview 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_height: float = maxf(1.0, minf(available.y * 0.86, available.y - POPUP_SAFE_SCREEN_MARGIN * 2.0))
var preferred_width: float = CELL_MIN_SIZE.x * COLUMNS + GRID_H_SEPARATION * (COLUMNS - 1) + POPUP_SIDE_PADDING
var 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 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
if in_color_picker:
if kind == "starters":
preferred_width = _theme_number("popups.asset_selector.starter_dialog_width", 1160.0, 440.0, 2400.0)
if in_color_picker or in_plain_choices:
var color_dialog_size: Vector2 = _theme_vector2("popups.dialog.surface_color_size", Vector2(560.0, 640.0))
preferred_width = color_dialog_size.x
preferred_height = color_dialog_size.y
var popup_width: float = minf(preferred_width, max_width)
_position_footer(popup_width)
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (CELL_MIN_SIZE.x + GRID_H_SEPARATION)), 1, COLUMNS)
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery else _active_columns * ROWS
var minimum_card_width: float = CELL_MIN_SIZE.x
if kind == "starters":
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)
_page_capacity = maxi(1, filtered.size()) if scenery_background_gallery or scenery_artwork_gallery or kind == "starters" else _active_columns * ROWS
_grid.columns = _active_columns
var card_width: float = minf(CELL_MIN_SIZE.x, (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns))
var card_height_limit: float = 248.0 if kind == "starters" else CELL_MIN_SIZE.y
var card_height: float = minf(card_height_limit, card_width + (58.0 if kind == "starters" else 30.0))
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_height_limit: float = 248.0 if kind != "starters" else INF
var card_height: float
if kind == "starters":
var preview_horizontal_inset: float = _theme_number("popups.asset_selector.starter_preview_horizontal_inset", 10.0, 0.0, 32.0)
var preview_aspect: float = _theme_number("popups.asset_selector.starter_preview_aspect", 2.0, 1.2, 2.8)
var preview_top_inset: float = _theme_number("popups.asset_selector.starter_preview_top_inset", 6.0, 0.0, 24.0)
var preview_height: float = maxf(1.0, card_width - preview_horizontal_inset * 2.0) / preview_aspect
var text_and_spacing_height: float = _theme_number("popups.asset_selector.starter_card_text_height", 92.0, 72.0, 144.0)
card_height = preview_height + preview_top_inset + text_and_spacing_height
else:
card_height = minf(card_height_limit, card_width + 30.0)
_effective_cell_size = Vector2(maxf(96.0, card_width), maxf(180.0, card_height))
var surface_width: float = minf(_effective_cell_size.x, maxf(96.0, (grid_width - GRID_H_SEPARATION) * 0.5))
var surface_height: float = maxf(180.0, minf(card_height_limit, surface_width + 30.0))
@@ -729,18 +1021,20 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
_page = clampi(_page, 0, page_count - 1)
var page_start: int = _page * _page_capacity
var page_items: int = clampi(filtered_count - page_start, 0, _page_capacity)
var grid_item_count: int = _count_sky_entries(filtered) if scenery_background_gallery else page_items
var grid_item_count: int = _count_scene_background_entries(filtered) if scenery_background_gallery else page_items
var grid_rows: int = int(ceil(float(grid_item_count) / float(_active_columns))) if grid_item_count > 0 else 0
var body_height: float
if in_color_picker:
_color_picker_view.set_available_size(Vector2(popup_width, minf(preferred_height, max_height)))
body_height = maxf(220.0, _color_picker_view.get_combined_minimum_size().y)
elif in_plain_choices:
body_height = maxf(220.0, _visible_content_minimum_height(_plain_color_view))
elif in_setup:
body_height = maxf(180.0, _visible_content_minimum_height(_setup_view))
else:
body_height = maxf(120.0, float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION)
if scenery_background_gallery and _has_surface_entries(filtered):
body_height += _surface_cell_size.y + GRID_V_SEPARATION
body_height += _surface_cell_size.y + _surface_row_heading.get_combined_minimum_size().y + GRID_V_SEPARATION * 2.0
if page_items == 0:
body_height = maxf(body_height, _empty_label.get_combined_minimum_size().y)
_tabs.custom_minimum_size.x = grid_width
@@ -798,6 +1092,8 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
_color_back_button.custom_minimum_size.x = action_width
_color_use_button.custom_minimum_size.x = action_width
size = Vector2i(maxi(1, roundi(popup_width)), maxi(1, roundi(popup_height)))
_position_footer(float(size.x))
_sync_stage_preview_dimming()
if visible:
if keep_visible_position:
var max_x: int = maxi(0, floori(available.x - float(size.x) - POPUP_SAFE_SCREEN_MARGIN))
@@ -815,7 +1111,7 @@ func _update_popup_layout(keep_visible_position: bool = false) -> void:
func _update_popup_layout_after_layout() -> void:
await get_tree().process_frame
if is_inside_tree():
_update_popup_layout()
_update_popup_layout(visible)
func _rebuild() -> void:
@@ -827,27 +1123,33 @@ func _rebuild() -> void:
_cell_texrects.clear()
var filtered: Array[Dictionary] = _filtered_entries()
var in_color_picker: bool = _color_picker_open
var in_plain_choices: bool = _plain_color_choices_open and not in_color_picker
var in_setup: bool = not _setup_kind.is_empty() and not in_color_picker
var scenery_background_gallery: bool = _is_scenery_background_gallery() and not in_setup and not in_color_picker
var scenery_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_setup and not in_color_picker
_position_footer()
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_artwork_gallery: bool = _is_scenery_artwork_gallery() and not in_subview
_position_footer(float(size.x))
_sync_stage_preview_dimming()
_close_button.visible = not in_setup and not in_color_picker
_setup_view.visible = in_setup
_plain_color_view.visible = in_plain_choices
_backgrounds_back_button.visible = in_plain_choices
_color_picker_view.visible = in_color_picker
_filter_chips.visible = scenery_artwork_gallery
_surface_cards_row.visible = scenery_background_gallery and _has_surface_entries(filtered)
_surface_row_heading.visible = _surface_cards_row.visible
_setup_cancel_button.visible = in_setup
_setup_draw_button.visible = in_setup
_color_back_button.visible = in_color_picker
_color_use_button.visible = in_color_picker
_edit_scenery_button.visible = kind == "scenery" and not in_setup and not in_color_picker
_edit_scenery_button.visible = kind == "scenery" and not in_subview
_make_artwork_button.visible = scenery_artwork_gallery
_browse_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
_refresh_button.visible = (kind == "stickman" or kind == "prop") and not in_setup and not in_color_picker
_footer_spacer.visible = scenery_artwork_gallery
_empty_label.visible = filtered.is_empty() and not in_setup and not in_color_picker
_grid.visible = not filtered.is_empty() and not in_setup and not in_color_picker
_grid_scroll.visible = not filtered.is_empty() or in_setup or in_color_picker
_empty_label.visible = 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
var total := _page_count()
var single := total <= 1
_prev_button.visible = not single
@@ -856,7 +1158,7 @@ func _rebuild() -> void:
_prev_button.disabled = _page <= 0
_next_button.disabled = (_page + 1) * _page_capacity >= filtered.size()
_page_label.text = "Page %d/%d" % [_page + 1, total]
if in_setup or in_color_picker:
if in_subview:
_prev_button.visible = false
_next_button.visible = false
_page_label.visible = false
@@ -869,7 +1171,7 @@ func _rebuild() -> void:
if scenery_background_gallery:
for entry: Dictionary in filtered:
var background_kind: String = String(entry.get("background_kind", ""))
if background_kind == "sky":
if background_kind == "sky" or background_kind == "plain":
_grid.add_child(_build_cell(entry))
elif background_kind == "wall_setup" or background_kind == "background_ground_setup":
_surface_cards_row.add_child(_build_cell(entry, _surface_cell_size))
@@ -879,15 +1181,70 @@ func _rebuild() -> void:
_grid.add_child(_build_cell(filtered[i]))
func _position_footer() -> void:
func _position_footer(target_width: float = -1.0) -> void:
var parent := _footer.get_parent() as VBoxContainer
if parent == null:
return
var target_index: int = parent.get_child_count() - 1
if _footer.get_index() != target_index:
parent.move_child(_footer, target_index)
if _close_button.get_parent() == _footer and _close_button.get_index() != 0:
_footer.move_child(_close_button, 0)
var in_plain_choices: bool = _plain_color_choices_open and not _color_picker_open
if in_plain_choices:
var available_width: float = target_width if target_width >= 0.0 else float(size.x)
var stack_actions: bool = available_width < 380.0
var action_container: Control = _plain_footer_column if stack_actions else _plain_footer_row
_plain_footer_row.visible = not stack_actions
_plain_footer_column.visible = stack_actions
_move_footer_control(_backgrounds_back_button, action_container, 0)
if stack_actions:
_move_footer_control(_close_button, action_container, 1)
_move_footer_control(_footer_spacer, _footer, 1)
_footer_spacer.visible = false
_backgrounds_back_button.custom_minimum_size.x = 0.0
_close_button.custom_minimum_size.x = 0.0
_backgrounds_back_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_close_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
else:
_move_footer_control(_footer_spacer, action_container, 1)
_move_footer_control(_close_button, action_container, 2)
_footer_spacer.visible = true
_backgrounds_back_button.custom_minimum_size.x = 148.0
_close_button.custom_minimum_size.x = 136.0
_backgrounds_back_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_close_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
return
_plain_footer_row.visible = false
_plain_footer_column.visible = false
_move_footer_control(_backgrounds_back_button, _footer, 1)
_move_footer_control(_footer_spacer, _footer, 1)
_move_footer_control(_close_button, _footer, 0)
_backgrounds_back_button.custom_minimum_size.x = 148.0
_close_button.custom_minimum_size.x = 136.0
_backgrounds_back_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_close_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
func _move_footer_control(control: Control, target: Control, index: int) -> void:
if control == null or target == null:
return
if control.get_parent() != target:
var old_parent: Node = control.get_parent()
if old_parent != null:
old_parent.remove_child(control)
target.add_child(control)
var safe_index: int = clampi(index, 0, maxi(0, target.get_child_count() - 1))
if control.get_index() != safe_index:
target.move_child(control, safe_index)
func _sync_stage_preview_dimming() -> void:
var should_dim: bool = not (_plain_color_choices_open and kind == "scenery")
if _color_picker_open and _color_picker_owner == &"plain":
should_dim = false
if should_dim == _stage_preview_is_dimmed:
return
_stage_preview_is_dimmed = should_dim
stage_preview_dimming_changed.emit(should_dim)
func _is_scenery_background_gallery() -> bool:
@@ -898,10 +1255,10 @@ func _is_scenery_artwork_gallery() -> bool:
return kind == "scenery" and _gallery_tab == "artwork"
func _count_sky_entries(entries: Array[Dictionary]) -> int:
func _count_scene_background_entries(entries: Array[Dictionary]) -> int:
var count: int = 0
for entry: Dictionary in entries:
if String(entry.get("background_kind", "")) == "sky":
if String(entry.get("background_kind", "")) in ["sky", "plain"]:
count += 1
return count
@@ -930,8 +1287,8 @@ func _visible_content_minimum_height(container: VBoxContainer) -> float:
func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> Control:
var key := _entry_key(entry)
var cell: Variant = ASSET_CARD.new()
# Give scenery cards rounded resting and interaction states. The selected sky
# state is applied separately below, so opening the gallery stays neutral.
# Scene cards have clear rounded resting and interaction states. Selected
# background state is applied separately below.
var categories: Array = entry.get("tags", []) as Array
var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", "")))
var card_size: Vector2 = _effective_cell_size if requested_size == Vector2.ZERO else requested_size
@@ -940,56 +1297,88 @@ func _build_cell(entry: Dictionary, requested_size: Vector2 = Vector2.ZERO) -> C
var thumb_edge: float = minf(_thumbnail_size.x, minf(card_size.x - 20.0, thumb_height_budget))
var thumb_size := Vector2.ONE * maxf(72.0, thumb_edge)
if kind == "starters":
thumb_size = Vector2.ONE * minf(128.0, maxf(72.0, thumb_edge))
var preview_horizontal_inset: float = _theme_number("popups.asset_selector.starter_preview_horizontal_inset", 10.0, 0.0, 32.0)
var preview_aspect: float = _theme_number("popups.asset_selector.starter_preview_aspect", 2.0, 1.2, 2.8)
var preview_width: float = maxf(72.0, card_size.x - preview_horizontal_inset * 2.0)
thumb_size = Vector2(preview_width, preview_width / preview_aspect)
var description: String = "" if is_background_ground_setup else String(entry.get("description", ""))
var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": description, "thumbnail_size": thumb_size}
if kind == "starters":
card_payload["label_font_size"] = _theme_integer("popups.asset_selector.starter_label_size", 22, 18, 32)
card_payload["label_min_height"] = _theme_integer("popups.asset_selector.starter_label_height", 34, 28, 52)
card_payload["description_font_size"] = _theme_integer("popups.asset_selector.starter_description_size", 20, 16, 30)
card_payload["description_min_height"] = _theme_integer("popups.asset_selector.starter_description_height", 48, 36, 80)
card_payload["preview_corner_radius"] = _theme_integer("popups.asset_selector.starter_preview_corner_radius", 12, 6, 24)
card_payload["preview_horizontal_inset"] = roundi(_theme_number("popups.asset_selector.starter_preview_horizontal_inset", 10.0, 0.0, 32.0))
card_payload["preview_top_inset"] = _theme_integer("popups.asset_selector.starter_preview_top_inset", 6, 0, 24)
if entry.has("background_kind"):
card_payload["background_kind"] = entry["background_kind"]
card_payload["finish_id"] = entry.get("finish_id", "")
card_payload["id"] = entry.get("id", "")
var is_sky_choice: bool = String(entry.get("background_kind", "")) == "sky"
var is_current_sky: bool = is_sky_choice and String(entry.get("id", "")) == _current_sky_id
if is_current_sky:
card_payload["status"] = "Current Sky"
var background_kind: String = String(entry.get("background_kind", ""))
var is_scene_background_choice: bool = background_kind == "sky" or background_kind == "plain"
var is_current_background: bool = background_kind == String(_current_background_kind) and (
background_kind == "plain" or String(entry.get("id", "")) == _current_sky_id
)
if is_current_background:
card_payload["status"] = "Current background"
card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF"))
cell.configure(card_payload, _thumbnails.get(key, null), null)
if kind == "scenery":
if kind == "scenery" or kind == "starters":
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_corner_radius: int = _theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
var card_neutral_surface: Color = _theme_color("stage_panels.choice.neutral_surface", Color("#171B24"))
var card_neutral_border: Color = _theme_color("stage_panels.choice.border_color", Color("#FFFFFF33"))
var card_hover_surface: Color = _theme_color("stage_panels.choice.hover_surface", Color("#303A4B"))
var card_selected_surface: Color = _theme_color("stage_panels.choice.selected_surface", Color("#12394A"))
var card_hover_border_width: int = card_border_width + 1
card_button.add_theme_stylebox_override("normal", _make_rounded_style(
_theme_color("stage_panels.choice.neutral_surface", Color("#171B24")),
_theme_color("stage_panels.choice.border_color", Color("#FFFFFF33")),
card_neutral_surface,
card_neutral_border,
card_border_width,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
card_corner_radius
))
card_button.add_theme_stylebox_override("hover", _make_rounded_style(
_theme_color("stage_panels.choice.hover_surface", Color("#303A4B")),
card_hover_surface,
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_border_width + 1,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
card_hover_border_width,
card_corner_radius
))
card_button.add_theme_stylebox_override("pressed", _make_rounded_style(
_theme_color("stage_panels.choice.selected_surface", Color("#12394A")),
card_selected_surface,
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_border_width + 1,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
card_hover_border_width,
card_corner_radius
))
card_button.add_theme_stylebox_override("focus", _make_rounded_style(
if kind == "starters":
card_button.add_theme_stylebox_override("hover_pressed", _make_rounded_style(
card_selected_surface.lightened(0.05),
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_hover_border_width,
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_style: StyleBoxFlat = _make_rounded_style(
Color(0, 0, 0, 0),
_theme_color("mode_colors.edit", Color("#22C6FF")),
card_border_width + 1,
_theme_integer("popups.asset_selector.corner_radius", 16, 8, 32)
))
focus_color,
card_hover_border_width,
card_corner_radius
)
if kind == "starters":
focus_style.set_expand_margin_all(float(card_border_width))
card_button.add_theme_stylebox_override("focus", focus_style)
cell.custom_minimum_size = card_size
cell.focus_mode = Control.FOCUS_ALL
cell.set_meta("entry_id", String(entry.get("id", "")))
cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry))
if is_sky_choice:
if is_scene_background_choice:
cell.button_group = _sky_card_group
cell.set_selected(is_current_sky, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF")))
cell.set_selected(is_current_background, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF")))
var tex: Texture2D = _thumbnails.get(key, null)
if kind == "starters":
var preview: Control = STARTER_CARD_PREVIEW.new() as Control
preview.call("configure", entry)
preview.call("configure", entry, thumb_size)
cell.set_preview_control(preview)
elif entry.has("background_kind"):
var preview: StageBackgroundCardPreview = BACKGROUND_CARD_PREVIEW.new() as StageBackgroundCardPreview
@@ -1012,6 +1401,10 @@ func _theme_integer(path: String, fallback: int, minimum: int, maximum: int) ->
return fallback if _sandbox_theme == null else _sandbox_theme.integer(path, fallback, minimum, maximum)
func _theme_number(path: String, fallback: float, minimum: float, maximum: float) -> float:
return fallback if _sandbox_theme == null else _sandbox_theme.number(path, fallback, minimum, maximum)
func _theme_vector2(path: String, fallback: Vector2) -> Vector2:
if _sandbox_theme == null:
return fallback
@@ -1054,7 +1447,8 @@ func _apply_chooser_visual_style() -> void:
frame_style.set_corner_radius_all(_theme_integer("popups.asset_selector.surface_corner_radius", 14, 4, 32))
preview_frame.add_theme_stylebox_override("panel", frame_style)
if _title_label != null:
_title_label.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_title_size", 30, 22, 48) if kind == "scenery" else 24)
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)
_title_label.add_theme_font_size_override("font_size", title_size)
_title_label.add_theme_color_override("font_color", text_color)
_apply_emphasis_font(_title_label)
for tab: Button in [_backgrounds_tab, _artwork_tab]:
@@ -1096,6 +1490,15 @@ func _apply_chooser_visual_style() -> void:
heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
heading.add_theme_color_override("font_color", text_color)
_apply_emphasis_font(heading)
if _surface_row_heading != null:
_surface_row_heading.add_theme_font_size_override("font_size", _theme_integer("popups.asset_selector.setup_heading_size", 26, 18, 48))
_surface_row_heading.add_theme_color_override("font_color", text_color)
_apply_emphasis_font(_surface_row_heading)
if _plain_color_helper != null:
_plain_color_helper.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 32))
_plain_color_helper.add_theme_color_override("font_color", _theme_color("stage_panels.type.muted_text_color", Color("#AAB2C0")))
_apply_font_to(_plain_color_helper)
_apply_rounded_button(_backgrounds_back_button, false, accent, text_color, neutral, hover, border, radius)
_apply_rounded_button(_setup_cancel_button, false, accent, text_color, neutral, hover, border, radius)
_apply_rounded_button(_setup_draw_button, true, accent, text_color, neutral, hover, border, radius)
_apply_emphasis_font(_setup_draw_button, true)
+1 -1
View File
@@ -44,7 +44,7 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) ->
continue
seen[color_key] = true
var button := Button.new()
var swatch_size: float = _swatch_size_override if _swatch_size_override > 0.0 else float(_integer("stage_surface_colors.swatch_size", 48))
var swatch_size: float = maxf(44.0, _swatch_size_override if _swatch_size_override > 0.0 else float(_integer("stage_surface_colors.swatch_size", 48)))
button.custom_minimum_size = Vector2(swatch_size, swatch_size)
button.toggle_mode = true
button.button_group = _group
+52
View File
@@ -61,6 +61,7 @@ var _warned: Dictionary = {}
func _init() -> void:
_apply_confirmation_defaults()
_apply_phase19_defaults()
func load_from(path: String = DEFAULT_PATH) -> bool:
invalidate_caches()
@@ -80,8 +81,59 @@ func load_from(path: String = DEFAULT_PATH) -> bool:
return false
_apply_legacy_aliases(parsed as Dictionary)
_config = _deep_merge(_config, parsed as Dictionary)
_apply_phase19_defaults()
return true
func _apply_phase19_defaults() -> void:
var grid: Dictionary = _config.get("grid", {}).duplicate(true)
for key: String in ["plain_light_minor", "plain_light_major", "plain_dark_minor", "plain_dark_major"]:
if not grid.has(key):
grid[key] = {
"plain_light_minor": "#11182120",
"plain_light_major": "#11182148",
"plain_dark_minor": "#FFFFFF25",
"plain_dark_major": "#FFFFFF58",
}[key]
_config["grid"] = grid
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
var asset_selector: Dictionary = popups.get("asset_selector", {}).duplicate(true)
asset_selector["compact_swatch_size"] = maxi(44, int(asset_selector.get("compact_swatch_size", 44)))
var starter_defaults: Dictionary = {
"starter_dialog_width": 1160.0,
"starter_viewport_height_ratio": 0.96,
"starter_card_min_width": 340.0,
"starter_preview_aspect": 2.0,
"starter_preview_corner_radius": 12,
"starter_preview_horizontal_inset": 10.0,
"starter_preview_top_inset": 6,
"starter_card_text_height": 108.0,
"starter_title_size": 30,
"starter_label_size": 22,
"starter_label_height": 34,
"starter_description_size": 20,
"starter_description_height": 48,
}
for key: String in starter_defaults:
if not asset_selector.has(key):
asset_selector[key] = starter_defaults[key]
popups["asset_selector"] = asset_selector
_config["popups"] = popups
var overlays: Dictionary = _config.get("overlays", {}).duplicate(true)
var gizmos: Dictionary = overlays.get("gizmos", {}).duplicate(true)
var defaults: Dictionary = {
"backing_color": "#111821E8",
"selection_color": "#F5F7FAE6",
"hover_color": "#FFE66DCC",
"rotate_color": "#4DB9FFE6",
"backing_extra_width": 3.0,
}
for key: String in defaults:
if not gizmos.has(key):
gizmos[key] = defaults[key]
overlays["gizmos"] = gizmos
_config["overlays"] = overlays
func _apply_confirmation_defaults() -> void:
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
popups["confirmation"] = CONFIRMATION_DEFAULTS.duplicate(true)
@@ -3,23 +3,29 @@ extends Control
## Reuses the Stage's Sky and wall-finish drawing rules in gallery cards.
const SKY_VIEW: Script = preload("res://scripts/stage/scenery/stage_sky_view.gd")
const BACKGROUND_VIEW: Script = preload("res://scripts/stage/scenery/stage_background_view.gd")
const SURFACE_RENDERER: Script = preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
var _background_kind: String = ""
var _preview_id: StringName = &""
var _sky_view: StageSkyView
var _background_view: StageBackgroundView
var _plain_color: Color = Color("#F5EFE0")
func configure(entry: Dictionary) -> void:
_background_kind = String(entry.get("background_kind", ""))
_preview_id = StringName(String(entry.get("id", ""))) if _background_kind == "sky" else StringName(String(entry.get("finish_id", "")))
var raw_plain_color: Variant = entry.get("plain_color", Color("#F5EFE0"))
_plain_color = raw_plain_color if raw_plain_color is Color else Color.from_string(String(raw_plain_color), Color("#F5EFE0"))
clip_contents = true
mouse_filter = Control.MOUSE_FILTER_IGNORE
if _background_kind == "sky":
_sky_view = SKY_VIEW.new() as StageSkyView
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_sky_view.set_sky(_preview_id)
add_child(_sky_view)
if _background_kind in ["sky", "plain"]:
_background_view = BACKGROUND_VIEW.new() as StageBackgroundView
_background_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_background_view.set_background(StringName(_background_kind), _preview_id, _plain_color)
add_child(_background_view)
_sky_view = _background_view.get_sky_view()
var frame: Panel = Panel.new()
frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
@@ -35,6 +41,8 @@ func configure(entry: Dictionary) -> void:
func _draw() -> void:
if _background_kind == "plain":
return
if _background_kind == "wall" and SURFACE_RENDERER.is_wall_finish(_preview_id):
var wall_tint: Color = Color("#7FA36C") if _preview_id == &"finish_wall_paint" else Color.WHITE
# The preview rect is already small and clipped, so draw its full pattern directly.
@@ -0,0 +1,74 @@
class_name StageBackgroundView
extends Control
## Shared full-rect backdrop for the live Stage and still card previews.
const SKY_VIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_sky_view.gd")
var _sky_view: StageSkyView
var _plain_fill: ColorRect
var _bound_camera: Camera2D
var _background_kind: StringName = &"sky"
var _sky_id: StringName = &"sky_neutral"
var _plain_color: Color = Color("#F5EFE0")
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
if _sky_view == null:
_sky_view = SKY_VIEW_SCRIPT.new() as StageSkyView
_sky_view.name = "StageSkyView"
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_sky_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_sky_view)
if _plain_fill == null:
_plain_fill = ColorRect.new()
_plain_fill.name = "PlainFill"
_plain_fill.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_plain_fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
_plain_fill.color = _opaque(_plain_color)
add_child(_plain_fill)
_refresh_children()
func set_background(kind: StringName, sky_id: StringName, plain_color: Color) -> void:
_background_kind = &"plain" if kind == &"plain" else &"sky"
_sky_id = sky_id
_plain_color = _opaque(plain_color)
_refresh_children()
func bind_camera(camera: Camera2D) -> void:
_bound_camera = camera
if _sky_view != null:
_sky_view.bind_camera(_bound_camera if _background_kind == &"sky" else null)
func apply_theme(stage_theme: SandboxTheme) -> void:
if _sky_view != null:
_sky_view.apply_theme(stage_theme)
func get_sky_view() -> StageSkyView:
return _sky_view
func get_background_kind() -> StringName:
return _background_kind
func get_plain_color() -> Color:
return _plain_color
func _refresh_children() -> void:
if _sky_view == null or _plain_fill == null:
return
_sky_view.set_sky(_sky_id)
_sky_view.visible = _background_kind == &"sky"
_sky_view.bind_camera(_bound_camera if _background_kind == &"sky" else null)
_plain_fill.color = _plain_color
_plain_fill.visible = _background_kind == &"plain"
func _opaque(color: Color) -> Color:
return Color(color.r, color.g, color.b, 1.0)
@@ -0,0 +1 @@
uid://dc72udajou30y
@@ -6,6 +6,7 @@ const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
const STAGE_SCENE_ADAPTER := preload("res://scripts/editors/stage/stage_scene_adapter.gd")
const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd")
const STAGE_SKY_VIEW := preload("res://scripts/stage/scenery/stage_sky_view.gd")
const STAGE_BACKGROUND_VIEW := preload("res://scripts/stage/scenery/stage_background_view.gd")
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
const PREVIEW_SIZE := Vector2i(256, 256)
@@ -14,14 +15,16 @@ const REFERENCE_VIEWPORT_SIZE := Vector2(1280.0, 720.0)
var _viewport: SubViewport
var _sky_view: StageSkyView
var _background_view: StageBackgroundView
var _world: Node2D
var _loaded_nodes: Array[Node2D] = []
var _scene_valid: bool = false
func configure(entry: Dictionary) -> void:
func configure(entry: Dictionary, requested_preview_size: Vector2 = Vector2.ZERO) -> void:
name = "StarterScenePreview"
custom_minimum_size = Vector2(128.0, 128.0)
var resolved_preview_size: Vector2 = requested_preview_size if requested_preview_size.x > 0.0 and requested_preview_size.y > 0.0 else Vector2(PREVIEW_SIZE)
custom_minimum_size = resolved_preview_size
mouse_filter = Control.MOUSE_FILTER_IGNORE
stretch = true
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
@@ -31,36 +34,40 @@ func configure(entry: Dictionary) -> void:
return
var data: Dictionary = result.get("data", {}) as Dictionary
_scene_valid = true
_build_preview_viewport(data)
_build_preview_viewport(data, resolved_preview_size)
func has_preview_scene() -> bool:
return _scene_valid and _viewport != null and _sky_view != null and _world != null
return _scene_valid and _viewport != null and _background_view != null and _world != null
func get_loaded_object_count() -> int:
return _loaded_nodes.size()
func _build_preview_viewport(data: Dictionary) -> void:
func _build_preview_viewport(data: Dictionary, requested_preview_size: Vector2) -> void:
_viewport = SubViewport.new()
_viewport.name = "PreviewViewport"
_viewport.size = PREVIEW_SIZE
_viewport.size = Vector2i(maxi(1, roundi(requested_preview_size.x)), maxi(1, roundi(requested_preview_size.y)))
_viewport.world_2d = World2D.new()
_viewport.transparent_bg = false
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
add_child(_viewport)
var sky_layer := CanvasLayer.new()
sky_layer.name = "PreviewSky"
sky_layer.layer = -1
_viewport.add_child(sky_layer)
_sky_view = STAGE_SKY_VIEW.new() as StageSkyView
_sky_view.name = "SkyView"
_sky_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_sky_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
_sky_view.set_sky(StringName(String(data.get("sky_id", "sky_neutral"))))
sky_layer.add_child(_sky_view)
var background_layer := CanvasLayer.new()
background_layer.name = "PreviewBackgroundLayer"
background_layer.layer = -1
_viewport.add_child(background_layer)
_background_view = STAGE_BACKGROUND_VIEW.new() as StageBackgroundView
_background_view.name = "StageBackgroundView"
_background_view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_background_view.set_background(
StringName(String(data.get("background_kind", "sky"))),
StringName(String(data.get("sky_id", "sky_neutral"))),
STAGE_SCENE_IO.color_from_data(data.get("plain_color", STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR), Color("#F5EFE0"))
)
background_layer.add_child(_background_view)
_sky_view = _background_view.get_sky_view()
_world = Node2D.new()
_world.name = "PreviewWorld"
@@ -85,27 +92,30 @@ func _build_preview_viewport(data: Dictionary) -> void:
func _fit_scene_to_preview(camera_state: Dictionary) -> void:
if _world == null or not is_instance_valid(_world):
return
var preview_frame: Dictionary = _preview_transform_for_camera(camera_state)
var preview_frame: Dictionary = _preview_transform_for_camera(camera_state, Vector2(_viewport.size))
var preview_scale: float = float(preview_frame["scale"])
_world.scale = Vector2.ONE * preview_scale
_world.position = preview_frame["position"] as Vector2
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
static func _preview_transform_for_camera(camera_state: Dictionary) -> Dictionary:
static func _preview_transform_for_camera(camera_state: Dictionary, preview_size: Vector2 = Vector2(PREVIEW_SIZE)) -> Dictionary:
var camera_position: Vector2 = STAGE_SCENE_IO.data_to_vector(camera_state.get("position"), Vector2.ZERO)
var camera_zoom: float = float(camera_state.get("zoom", 0.5))
if not is_finite(camera_zoom) or camera_zoom <= 0.0:
camera_zoom = 0.5
var safe_preview_size: Vector2 = preview_size if preview_size.x > 0.0 and preview_size.y > 0.0 else Vector2(PREVIEW_SIZE)
var preview_aspect: float = safe_preview_size.x / safe_preview_size.y
var square_world_extent: float = REFERENCE_VIEWPORT_SIZE.y / camera_zoom
var focus_size := Vector2(square_world_extent * preview_aspect, square_world_extent)
var focus_rect := Rect2(
camera_position - Vector2.ONE * square_world_extent * 0.5,
Vector2.ONE * square_world_extent
camera_position - focus_size * 0.5,
focus_size
)
var usable_size: Vector2 = Vector2(PREVIEW_SIZE) - Vector2.ONE * PREVIEW_MARGIN * 2.0
var usable_size: Vector2 = safe_preview_size - Vector2(PREVIEW_MARGIN * 2.0, PREVIEW_MARGIN * 2.0)
var preview_scale: float = minf(usable_size.x / focus_rect.size.x, usable_size.y / focus_rect.size.y)
return {
"focus_rect": focus_rect,
"scale": preview_scale,
"position": Vector2(PREVIEW_SIZE) * 0.5 - camera_position * preview_scale,
"position": safe_preview_size * 0.5 - camera_position * preview_scale,
}
@@ -29,14 +29,16 @@ func _draw() -> void:
var rect: Rect2 = Rect2(Vector2(3.0, 3.0), size - Vector2(6.0, 6.0))
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
return
if surface_kind == &"background_ground":
if surface_kind == &"plain_background":
draw_rect(rect, Color(tint.r, tint.g, tint.b, 1.0), true)
elif surface_kind == &"background_ground":
SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
else:
SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
var radius: float = minf(corner_radius, minf(rect.size.x, rect.size.y) * 0.5)
if radius > 0.0:
var wall: bool = surface_kind != &"background_ground"
var base_color: Color = SURFACE_RENDERER.surface_base_color(finish_id, tint, wall)
var base_color: Color = Color(tint.r, tint.g, tint.b, 1.0) if surface_kind == &"plain_background" else SURFACE_RENDERER.surface_base_color(finish_id, tint, wall)
_draw_rounded_corner(rect, radius, base_color, outside_color, 0)
_draw_rounded_corner(rect, radius, base_color, outside_color, 1)
_draw_rounded_corner(rect, radius, base_color, outside_color, 2)
+31 -4
View File
@@ -54,6 +54,13 @@ const MAX_SCALE: float = 20.0
# ---------------------------------------------------------------------------
var camera: Camera2D = null
var _selection_backing_color: Color = Color("#111821E8")
var _selection_core_color: Color = Color(1.0, 1.0, 1.0, 0.9)
var _hover_backing_color: Color = Color("#111821E8")
var _hover_core_color: Color = Color(1.0, 0.9, 0.2, 0.5)
var _rotate_backing_color: Color = Color("#111821E8")
var _rotate_core_color: Color = Color(0.3, 0.6, 1.0, 0.9)
var _outline_extra_width: float = 3.0
## When > 0, translation drags snap to this grid size (0 = off).
var snap_size: float = 0.0
@@ -105,6 +112,19 @@ func set_snap_size(size: float) -> void:
snap_size = maxf(size, 0.0)
func apply_theme(stage_theme: SandboxTheme) -> void:
if stage_theme == null:
return
_selection_backing_color = stage_theme.color("overlays.gizmos.backing_color", Color("#111821E8"))
_selection_core_color = stage_theme.color("overlays.gizmos.selection_color", SELECTION_COLOR)
_hover_backing_color = stage_theme.color("overlays.gizmos.backing_color", Color("#111821E8"))
_hover_core_color = stage_theme.color("overlays.gizmos.hover_color", HOVER_COLOR)
_rotate_backing_color = stage_theme.color("overlays.gizmos.backing_color", Color("#111821E8"))
_rotate_core_color = stage_theme.color("overlays.gizmos.rotate_color", ROTATE_COLOR)
_outline_extra_width = stage_theme.number("overlays.gizmos.backing_extra_width", 3.0, 1.0, 8.0)
queue_redraw()
func set_box_rect(rect: Rect2) -> void:
_box_rect = rect
queue_redraw()
@@ -253,16 +273,18 @@ func _draw() -> void:
return
var zoom := _zoom()
if _hovered != null and is_instance_valid(_hovered) and not _hovered.is_queued_for_deletion() and not _targets.has(_hovered):
draw_rect(_target_bounds(_hovered), HOVER_COLOR, false, HOVER_WIDTH / zoom)
_draw_halo_rect(_target_bounds(_hovered), _hover_core_color, HOVER_WIDTH / zoom, _hover_backing_color)
# A selection outline for every selected object.
for node: Node2D in _targets:
if node != null and is_instance_valid(node):
draw_rect(_target_bounds(node), SELECTION_COLOR, false, SELECTION_WIDTH / zoom)
_draw_halo_rect(_target_bounds(node), _selection_core_color, SELECTION_WIDTH / zoom, _selection_backing_color)
# Rotate ring only for a single selection.
if _transform_handles_visible and _targets.size() == 1 and is_instance_valid(_targets[0]):
var rect: Rect2 = _target_bounds(_targets[0])
if not _is_scenery_target(_targets[0]):
draw_arc(rect.get_center(), _rotate_ring_radius(rect), 0.0, TAU, 64, ROTATE_COLOR, ROTATE_WIDTH / zoom, true)
var ring_radius: float = _rotate_ring_radius(rect)
draw_arc(rect.get_center(), ring_radius, 0.0, TAU, 64, _rotate_backing_color, ROTATE_WIDTH / zoom + _outline_extra_width / zoom, true)
draw_arc(rect.get_center(), ring_radius, 0.0, TAU, 64, _rotate_core_color, ROTATE_WIDTH / zoom, true)
if _is_scenery_target(_targets[0]):
for corner: Vector2 in _corners(rect):
draw_circle(corner, 11.0 / zoom, Color("#111821E8"))
@@ -275,7 +297,7 @@ func _draw() -> void:
draw_line(corner + Vector2(-half, 0.0), corner + Vector2(half, 0.0), SELECTION_COLOR, 1.5 / zoom)
draw_line(corner + Vector2(0.0, -half), corner + Vector2(0.0, half), SELECTION_COLOR, 1.5 / zoom)
if _box_rect.size != Vector2.ZERO:
draw_rect(_box_rect.abs(), HOVER_COLOR, false, HOVER_WIDTH / zoom)
_draw_halo_rect(_box_rect.abs(), _hover_core_color, HOVER_WIDTH / zoom, _hover_backing_color)
# ---------------------------------------------------------------------------
# Internal helpers
@@ -287,6 +309,11 @@ func _zoom() -> float:
return 1.0
func _draw_halo_rect(rect: Rect2, core_color: Color, core_width: float, backing_color: Color) -> void:
draw_rect(rect, backing_color, false, core_width + _outline_extra_width / _zoom())
draw_rect(rect, core_color, false, core_width)
func _rotate_ring_radius(rect: Rect2) -> float:
return rect.size.length() * 0.5 + ROTATE_HANDLE_MARGIN_PX / _zoom()
+34 -2
View File
@@ -9,6 +9,12 @@ extends Node2D
var camera: Camera2D = null
var grid_size: float = 15.0
var enabled: bool = true
var _plain_background_active: bool = false
var _plain_background_color: Color = Color("#F5EFE0")
var _plain_light_minor_color: Color = Color("#11182120")
var _plain_light_major_color: Color = Color("#11182148")
var _plain_dark_minor_color: Color = Color("#FFFFFF25")
var _plain_dark_major_color: Color = Color("#FFFFFF58")
const GRID_COLOR := Color(1.0, 1.0, 1.0, 0.08)
const MAJOR_COLOR := Color(1.0, 1.0, 1.0, 0.16)
@@ -19,6 +25,23 @@ func _process(_delta: float) -> void:
queue_redraw()
func set_background_treatment(
plain_active: bool,
plain_color: Color,
light_minor: Color,
light_major: Color,
dark_minor: Color,
dark_major: Color
) -> void:
_plain_background_active = plain_active
_plain_background_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
_plain_light_minor_color = light_minor
_plain_light_major_color = light_major
_plain_dark_minor_color = dark_minor
_plain_dark_major_color = dark_major
queue_redraw()
func _draw() -> void:
if not enabled or grid_size <= 0.0:
return
@@ -34,13 +57,22 @@ func _draw() -> void:
var x := floorf(rect.position.x / grid_size) * grid_size
while x <= rect.end.x:
var grid_index := int(round(x / grid_size))
var color := MAJOR_COLOR if grid_index % MAJOR_EVERY == 0 else GRID_COLOR
var color: Color = _grid_color(grid_index % MAJOR_EVERY == 0)
draw_line(Vector2(x, rect.position.y), Vector2(x, rect.end.y), color, line_width)
x += grid_size
var y := floorf(rect.position.y / grid_size) * grid_size
while y <= rect.end.y:
var grid_index := int(round(y / grid_size))
var color := MAJOR_COLOR if grid_index % MAJOR_EVERY == 0 else GRID_COLOR
var color: Color = _grid_color(grid_index % MAJOR_EVERY == 0)
draw_line(Vector2(rect.position.x, y), Vector2(rect.end.x, y), color, line_width)
y += grid_size
func _grid_color(is_major: bool) -> Color:
if not _plain_background_active:
return MAJOR_COLOR if is_major else GRID_COLOR
var light_fill: bool = _plain_background_color.get_luminance() >= 0.55
if light_fill:
return _plain_light_major_color if is_major else _plain_light_minor_color
return _plain_dark_major_color if is_major else _plain_dark_minor_color
+28 -6
View File
@@ -3,7 +3,8 @@ extends RefCounted
## JSON boundary for Sandbox Stage documents. This class deliberately has no
## scene-tree dependencies, which makes rejected documents non-destructive.
const FILE_VERSION: String = "1.6"
const FILE_VERSION: String = "1.7"
const DEFAULT_PLAIN_COLOR: String = "#F5EFE0"
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
@@ -185,11 +186,13 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
prop_entry["wobble_on_impact"] = false
_assign_legacy_depths(current.get("objects", []))
return current
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5"]:
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"]:
return {}
var migrated: Dictionary = data.duplicate(true)
if version != "1.5":
if version not in ["1.5", "1.6"]:
migrated["sky_id"] = "sky_neutral"
migrated["background_kind"] = "sky"
migrated["plain_color"] = DEFAULT_PLAIN_COLOR
for raw: Variant in migrated.get("objects", []):
if not raw is Dictionary:
continue
@@ -203,9 +206,9 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
object["next_use_spot_number"] = object.get("next_use_spot_number", 1)
if not object.has("wobble_on_impact"):
object["wobble_on_impact"] = false
if version != "1.5" and String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
if version not in ["1.5", "1.6"] and String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
object["finish_id"] = "finish_ground_legacy"
if String(object.get("type", "")) == "wall_panel":
if version != "1.6" and String(object.get("type", "")) == "wall_panel":
_migrate_legacy_wall_tint(object)
migrated["version"] = FILE_VERSION
_assign_legacy_depths(migrated.get("objects", []))
@@ -254,9 +257,15 @@ static func _assign_legacy_depths(raw_objects: Variant) -> void:
roles["depth"] = PROP_STAGE_ROLES.DEPTH_FRONT if characters_before > characters_after else PROP_STAGE_ROLES.DEPTH_BEHIND
static func _validate_root(data: Dictionary) -> Dictionary:
var sky_id: Variant = data.get("sky_id", "sky_neutral")
var background_kind: Variant = data.get("background_kind", null)
if not background_kind is String or not ["sky", "plain"].has(String(background_kind)):
return {"ok": false, "error": "Scene background kind is invalid."}
var sky_id: Variant = data.get("sky_id", null)
if not sky_id is String or not SKY_IDS.has(String(sky_id)):
return {"ok": false, "error": "Scene Sky is invalid."}
var plain_color: Variant = data.get("plain_color", null)
if not _valid_opaque_hex_color(plain_color):
return {"ok": false, "error": "Scene Plain Background color is invalid."}
var seen: Dictionary = {}
var seen_stage_ids: Dictionary = {}
for raw: Variant in data["objects"]:
@@ -346,6 +355,19 @@ static func _valid_color(value: Variant) -> bool:
return true
static func _valid_opaque_hex_color(value: Variant) -> bool:
if not value is String:
return false
var raw: String = String(value).strip_edges()
if raw.length() != 7 or not raw.begins_with("#"):
return false
const HEX_DIGITS: String = "0123456789abcdefABCDEF"
for index: int in range(1, raw.length()):
if not HEX_DIGITS.contains(raw.substr(index, 1)):
return false
return true
static func _valid_rect_data(value: Variant) -> bool:
if not value is Dictionary:
return false