Add tests for scenery surface caching and preview behavior

- Introduced `test_scenery_brick_preview.gd` to validate focused placement and CPU draw-cost for large brick walls.
- Added `test_scenery_surface_cache.gd` to assess texture budget, visual equivalence, and cache invalidation.
- Created `test_scenery_surface_preview.gd` to evaluate adaptive preview selection and placement behavior.
- Implemented various helper functions to measure drawing performance and validate expected behaviors during tests.
- Added unique identifiers for new test files to ensure proper tracking and management.
This commit is contained in:
2026-10-21 11:06:57 -04:00
parent 80ba83b703
commit c1758efd3b
54 changed files with 5941 additions and 526 deletions
+506 -95
View File
@@ -62,6 +62,8 @@ const STAGE_PANEL_CHOICE_GROUP_SCRIPT := preload("res://scripts/shared/stage_ui/
const STAGE_SURFACE_COLOR_CONTROLS_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd")
const STAGE_SURFACE_COLOR_DIALOG_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd")
const STAGE_SURFACE_PREVIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
const STAGE_DEPTH_CHOICE_PREVIEW_SCRIPT := preload("res://scripts/shared/stage_ui/stage_depth_choice_preview.gd")
const PROP_FILE_SCRIPT := preload("res://scripts/editors/prop/prop_file.gd")
const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
# Phase 3c editor tools.
@@ -287,10 +289,20 @@ var _scenery_depth_section: VBoxContainer
var _scenery_depth_heading: Label
var _scenery_depth_group: StagePanelChoiceGroup
var _scenery_depth_helper: Label
var _scenery_depth_choice_cues: Array[Control] = []
var _scenery_depth_preview_texture: Texture2D
var _scenery_depth_preview_path: String = ""
var _scenery_depth_preview_busy: bool = false
var _scenery_depth_preview_generation: int = 0
var _scenery_depth_preview_scheduled_generation: int = -1
var _scenery_depth_thumbnail_renderer: PropThumbnail
var _scenery_depth_cursor_badge: PanelContainer
var _scenery_depth_cursor_badge_label: Label
var _scenery_size_row: HBoxContainer
var _scenery_smaller_button: Button
var _scenery_bigger_button: Button
var _scenery_erase_wall_area_button: Button
var _scenery_erase_background_ground_area_button: Button
var _scenery_erase_piece_button: Button
var _scenery_find_button: Button
var _scenery_choose_button: Button
@@ -300,7 +312,13 @@ var _scenery_repair_candidate: StageSceneryArtwork
var _scenery_repair_path: String = ""
var _missing_scenery_dialog: ConfirmationDialog
var _scenery_repair_confirm_dialog: ConfirmationDialog
var _scenery_erase_piece_dialog: ConfirmationDialog
var _scenery_erase_piece_dialog: Control
var _scenery_erase_piece_dialog_panel: PanelContainer
var _scenery_erase_piece_dialog_title: Label
var _scenery_erase_piece_dialog_message: Label
var _scenery_erase_piece_dialog_actions: BoxContainer
var _scenery_erase_piece_keep_button: Button
var _scenery_erase_piece_remove_button: Button
var _scenery_erase_piece_targets: Array[WeakRef] = []
var _surface_color_dialog: StageSurfaceColorDialog
var _surface_color_session: Dictionary = {}
@@ -1376,6 +1394,7 @@ func _process(delta: float) -> void:
_update_action_overlay()
_sync_awaiting_cursor()
_update_speech_badge_cursor()
_update_scenery_depth_cursor_badge()
if _toast_timer > 0.0:
_toast_timer -= delta
if _toast_timer <= 0.0:
@@ -1521,6 +1540,11 @@ func _unhandled_key_input(event: InputEvent) -> void:
return
if not (event is InputEventKey) or not event.pressed or event.echo:
return
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible:
if event.keycode == KEY_ESCAPE:
_dismiss_scenery_erase_piece_dialog()
get_viewport().set_input_as_handled()
return
if _speech_composer_is_open():
if event.keycode == KEY_ESCAPE:
if bool(_speech_composer.call("is_message_dialog_open")):
@@ -1692,9 +1716,7 @@ func _handle_world_click(mb: InputEventMouseButton) -> void:
_refresh_scenery_inspector()
_refresh_scenery_capture_bar()
elif _scenery_controller != null and _scenery_controller.is_group_preview_pending():
_scenery_controller.cancel_group()
_scenery_group_mode = &""
_refresh_scenery_capture_bar()
_keep_scenery_group_preview()
else:
_exit_scenery_edit_mode()
get_viewport().set_input_as_handled()
@@ -2557,6 +2579,8 @@ func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool:
_pending_file_action = PendingFileAction.NONE
return false
if not is_autosave:
if _scenery_controller != null:
_scenery_controller.commit_placement_session()
_paused_edit_autosave_remaining = -1.0
_set_scene_dirty(false)
_save_settings()
@@ -3211,15 +3235,21 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
_scenery_depth_section.add_child(_scenery_depth_heading)
_scenery_depth_group = STAGE_PANEL_CHOICE_GROUP_SCRIPT.new() as StagePanelChoiceGroup
_scenery_depth_group.name = "SceneryDepthChoices"
_scenery_depth_group.add_choice(&"far", "Far")
_scenery_depth_group.add_choice(&"near", "Near")
_scenery_depth_group.add_choice(&"floor", "On the floor")
_scenery_depth_group.configure_layout(true, 118.0, 24, 16, 1, Color("#087DBA"))
_scenery_depth_choice_cues.clear()
var back_cue: Control = _make_scenery_depth_choice_cue(&"far", Vector2(128.0, 82.0))
var middle_cue: Control = _make_scenery_depth_choice_cue(&"near", Vector2(128.0, 82.0))
var front_cue: Control = _make_scenery_depth_choice_cue(&"floor", Vector2(128.0, 82.0))
_scenery_depth_choice_cues = [back_cue, middle_cue, front_cue]
_scenery_depth_group.add_choice(&"far", "Back", back_cue, "Behind other scenery")
_scenery_depth_group.add_choice(&"near", "Middle", middle_cue, "In front of Back · behind tiles")
_scenery_depth_group.add_choice(&"floor", "Front", front_cue, "In front of ground tiles")
_scenery_depth_group.apply_theme(_sandbox_theme as SandboxTheme)
_scenery_depth_group.choice_selected.connect(_on_selected_scenery_depth_choice)
_scenery_depth_section.add_child(_scenery_depth_group)
_scenery_depth_helper = Label.new()
_scenery_depth_helper.name = "SceneryDepthHelper"
_scenery_depth_helper.text = "Far is behind Near. On the floor is in front of the ground."
_scenery_depth_helper.text = "The pictures show what can cover the artwork."
_scenery_depth_helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_scenery_depth_helper.add_theme_font_size_override("font_size", _stage_panel_integer("type.helper_size", 16, 12, 32))
_scenery_depth_helper.add_theme_color_override("font_color", _stage_panel_color("type.muted_text_color", Color("#AAB2C0")))
@@ -3237,9 +3267,14 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
_scenery_size_row.add_child(_scenery_bigger_button)
body.add_child(_scenery_size_row)
_scenery_erase_wall_area_button = _make_scenery_repair_button("EraseWallArea", "Erase wall area", true)
_scenery_inspector.style_primary_action_button(_scenery_erase_wall_area_button)
_scenery_erase_wall_area_button.pressed.connect(_begin_selected_wall_erase)
body.add_child(_scenery_erase_wall_area_button)
_scenery_erase_piece_button = _make_scenery_repair_button("EraseSceneryPiece", "Erase wall panel", true)
_scenery_erase_background_ground_area_button = _make_scenery_repair_button("EraseBackgroundGroundArea", "Erase background ground area", true)
_scenery_inspector.style_primary_action_button(_scenery_erase_background_ground_area_button)
_scenery_erase_background_ground_area_button.pressed.connect(_begin_selected_background_ground_erase)
body.add_child(_scenery_erase_background_ground_area_button)
_scenery_erase_piece_button = _make_scenery_repair_button("EraseSceneryPiece", "Remove wall panel", false, true)
_scenery_erase_piece_button.pressed.connect(_request_erase_selected_scenery)
body.add_child(_scenery_erase_piece_button)
_scenery_find_button = _make_scenery_repair_button("FindArtwork", "Find artwork…", true)
@@ -3272,16 +3307,7 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
_scenery_repair_confirm_dialog.canceled.connect(_cancel_scenery_repair_preview)
ui.add_child(_scenery_repair_confirm_dialog)
_apply_dialog_theme(_scenery_repair_confirm_dialog)
_scenery_erase_piece_dialog = ConfirmationDialog.new()
_scenery_erase_piece_dialog.name = "EraseSceneryPieceDialog"
_scenery_erase_piece_dialog.title = "Erase this piece?"
_scenery_erase_piece_dialog.ok_button_text = "Erase"
_scenery_erase_piece_dialog.cancel_button_text = "Keep it"
_scenery_erase_piece_dialog.min_size = _theme_dialog_size("confirm_size", Vector2i(520, 260))
_scenery_erase_piece_dialog.confirmed.connect(_confirm_erase_selected_scenery)
_scenery_erase_piece_dialog.canceled.connect(_clear_erase_scenery_targets)
ui.add_child(_scenery_erase_piece_dialog)
_apply_dialog_theme(_scenery_erase_piece_dialog)
_build_scenery_erase_piece_dialog(ui)
_surface_color_dialog = STAGE_SURFACE_COLOR_DIALOG_SCRIPT.new() as StageSurfaceColorDialog
_surface_color_dialog.name = "ScenerySurfaceColorDialog"
ui.add_child(_surface_color_dialog)
@@ -3301,10 +3327,314 @@ func _make_scenery_section_heading(label_text: String) -> Label:
return heading
func _make_scenery_depth_choice_cue(depth: StringName, cue_size: Vector2) -> Control:
var cue: StageDepthChoicePreview = STAGE_DEPTH_CHOICE_PREVIEW_SCRIPT.new() as StageDepthChoicePreview
cue.configure(depth, _scenery_depth_preview_texture, cue_size)
return cue
func _make_scenery_depth_capture_action(action_id: StringName, depth: StringName, label: String, selected_depth: StringName) -> Dictionary:
return {
"id": action_id,
"label": label,
"choice": true,
"selected": depth == selected_depth,
"cue": _make_scenery_depth_choice_cue(depth, Vector2(38.0, 34.0)),
}
func _set_scenery_depth_preview_path(path: String) -> void:
var canonical_path: String = CONTENT_PATHS.canonicalize(path) if not path.is_empty() else ""
if canonical_path == _scenery_depth_preview_path and (_scenery_depth_preview_texture != null or _scenery_depth_preview_busy or _scenery_depth_preview_scheduled_generation == _scenery_depth_preview_generation):
return
if canonical_path != _scenery_depth_preview_path:
_scenery_depth_preview_path = canonical_path
_scenery_depth_preview_texture = null
_scenery_depth_preview_generation += 1
_update_scenery_depth_choice_cues()
if canonical_path.is_empty() or not FileAccess.file_exists(canonical_path):
return
if not _scenery_depth_preview_busy:
_scenery_depth_preview_scheduled_generation = _scenery_depth_preview_generation
call_deferred("_render_scenery_depth_preview", canonical_path, _scenery_depth_preview_generation)
func _render_scenery_depth_preview(path: String, generation: int) -> void:
if _scenery_depth_preview_busy or path.is_empty():
return
_scenery_depth_preview_scheduled_generation = -1
_scenery_depth_preview_busy = true
if _scenery_depth_thumbnail_renderer == null:
_scenery_depth_thumbnail_renderer = PROP_THUMBNAIL.new() as PropThumbnail
_scenery_depth_thumbnail_renderer.name = "SceneryDepthThumbnailRenderer"
add_child(_scenery_depth_thumbnail_renderer)
await get_tree().process_frame
var parsed: Dictionary = PROP_FILE_SCRIPT.from_file_dict(PROP_FILE_SCRIPT.load(path))
var shapes: Array[Dictionary] = parsed.get("shapes", []) as Array[Dictionary]
var texture: Texture2D
if not shapes.is_empty() and _scenery_depth_thumbnail_renderer != null:
texture = await _scenery_depth_thumbnail_renderer.render_shapes(shapes)
if generation == _scenery_depth_preview_generation and path == _scenery_depth_preview_path and texture != null:
_scenery_depth_preview_texture = texture
_update_scenery_depth_choice_cues()
if _scenery_controller != null and _scenery_controller.get_active_tool() == &"artwork":
_refresh_scenery_capture_bar()
_scenery_depth_preview_busy = false
if not _scenery_depth_preview_path.is_empty() and _scenery_depth_preview_path != path:
_scenery_depth_preview_scheduled_generation = _scenery_depth_preview_generation
call_deferred("_render_scenery_depth_preview", _scenery_depth_preview_path, _scenery_depth_preview_generation)
func _update_scenery_depth_choice_cues() -> void:
for cue_control: Control in _scenery_depth_choice_cues:
if cue_control is StageDepthChoicePreview:
(cue_control as StageDepthChoicePreview).set_artwork_texture(_scenery_depth_preview_texture)
func _build_scenery_erase_piece_dialog(ui: CanvasLayer) -> void:
_scenery_erase_piece_dialog = Control.new()
_scenery_erase_piece_dialog.name = "EraseSceneryPieceDialog"
_scenery_erase_piece_dialog.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_scenery_erase_piece_dialog.mouse_filter = Control.MOUSE_FILTER_STOP
_scenery_erase_piece_dialog.z_index = 100
_scenery_erase_piece_dialog.visible = false
ui.add_child(_scenery_erase_piece_dialog)
var backdrop: ColorRect = ColorRect.new()
backdrop.name = "DialogBackdrop"
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
backdrop.color = _confirmation_dialog_color("backdrop_color", Color("#10151D66"))
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
_scenery_erase_piece_dialog.add_child(backdrop)
var center: CenterContainer = CenterContainer.new()
center.name = "DialogCenter"
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
_scenery_erase_piece_dialog.add_child(center)
_scenery_erase_piece_dialog_panel = PanelContainer.new()
_scenery_erase_piece_dialog_panel.name = "DialogCard"
center.add_child(_scenery_erase_piece_dialog_panel)
var margin: MarginContainer = MarginContainer.new()
margin.name = "DialogMargin"
var padding: int = _confirmation_dialog_integer("padding", 18, 12, 32)
margin.add_theme_constant_override("margin_left", padding)
margin.add_theme_constant_override("margin_right", padding)
margin.add_theme_constant_override("margin_top", padding)
margin.add_theme_constant_override("margin_bottom", padding)
_scenery_erase_piece_dialog_panel.add_child(margin)
var layout: VBoxContainer = VBoxContainer.new()
layout.name = "DialogLayout"
layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
layout.size_flags_vertical = Control.SIZE_EXPAND_FILL
layout.add_theme_constant_override("separation", _confirmation_dialog_integer("body_gap", 10, 4, 24))
margin.add_child(layout)
_scenery_erase_piece_dialog_title = Label.new()
_scenery_erase_piece_dialog_title.name = "DialogTitle"
_scenery_erase_piece_dialog_title.text = "Remove this scenery?"
_scenery_erase_piece_dialog_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_scenery_erase_piece_dialog_title.add_theme_font_size_override("font_size", _confirmation_dialog_integer("title_font_size", 26, 20, 40))
_scenery_erase_piece_dialog_title.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA")))
_apply_ui_font(_scenery_erase_piece_dialog_title)
var title_font: Font = _ui_font
if title_font != null:
var strong_title_font: FontVariation = FontVariation.new()
strong_title_font.base_font = title_font
strong_title_font.variation_embolden = 0.55
_scenery_erase_piece_dialog_title.add_theme_font_override("font", strong_title_font)
layout.add_child(_scenery_erase_piece_dialog_title)
_scenery_erase_piece_dialog_message = Label.new()
_scenery_erase_piece_dialog_message.name = "DialogMessage"
_scenery_erase_piece_dialog_message.text = "Remove this whole piece? You can’t undo this."
_scenery_erase_piece_dialog_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_scenery_erase_piece_dialog_message.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_scenery_erase_piece_dialog_message.add_theme_font_size_override("font_size", _confirmation_dialog_integer("message_font_size", 20, 16, 32))
_scenery_erase_piece_dialog_message.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA")))
_apply_ui_font(_scenery_erase_piece_dialog_message)
layout.add_child(_scenery_erase_piece_dialog_message)
var actions_spacer: Control = Control.new()
actions_spacer.name = "ActionsSpacer"
actions_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
layout.add_child(actions_spacer)
_scenery_erase_piece_dialog_actions = BoxContainer.new()
_scenery_erase_piece_dialog_actions.name = "DialogActions"
_scenery_erase_piece_dialog_actions.alignment = BoxContainer.ALIGNMENT_END
_scenery_erase_piece_dialog_actions.add_theme_constant_override("separation", _confirmation_dialog_integer("footer_gap", 12, 8, 24))
layout.add_child(_scenery_erase_piece_dialog_actions)
_scenery_erase_piece_keep_button = Button.new()
_scenery_erase_piece_keep_button.name = "KeepSceneryButton"
_scenery_erase_piece_keep_button.text = "Keep it"
_scenery_erase_piece_keep_button.focus_mode = Control.FOCUS_ALL
_scenery_erase_piece_keep_button.pressed.connect(_dismiss_scenery_erase_piece_dialog)
_scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_keep_button)
_scenery_erase_piece_remove_button = Button.new()
_scenery_erase_piece_remove_button.name = "RemoveSceneryButton"
_scenery_erase_piece_remove_button.text = "Remove scenery"
_scenery_erase_piece_remove_button.focus_mode = Control.FOCUS_ALL
_scenery_erase_piece_remove_button.pressed.connect(_confirm_erase_scenery_from_dialog)
_scenery_erase_piece_dialog_actions.add_child(_scenery_erase_piece_remove_button)
_apply_scenery_erase_piece_dialog_style()
if not get_tree().root.size_changed.is_connected(_refresh_scenery_erase_piece_dialog_size):
get_tree().root.size_changed.connect(_refresh_scenery_erase_piece_dialog_size)
func _apply_scenery_erase_piece_dialog_style() -> void:
if _scenery_erase_piece_dialog == null:
return
var shell: StyleBoxFlat = StyleBoxFlat.new()
shell.bg_color = _confirmation_dialog_color("surface_color", Color("#202633FA"))
shell.border_color = _confirmation_dialog_color("border_color", Color("#AAB2C066"))
shell.set_border_width_all(_confirmation_dialog_integer("border_width", 1, 1, 4))
shell.set_corner_radius_all(_confirmation_dialog_integer("corner_radius", 16, 10, 28))
_scenery_erase_piece_dialog_panel.add_theme_stylebox_override("panel", shell)
_apply_scenery_erase_piece_action_style(_scenery_erase_piece_keep_button, true)
_apply_scenery_erase_piece_action_style(_scenery_erase_piece_remove_button, false)
func _apply_scenery_erase_piece_action_style(button: Button, primary: bool) -> void:
if button == null:
return
var accent: Color = _confirmation_dialog_color("accent_color", Color("#22C6FF"))
var blue: Color = _confirmation_dialog_color("action_color", Color("#087DBA"))
var danger: Color = _confirmation_dialog_color("danger_color", Color("#FF626B"))
var neutral: Color = _stage_panel_color("action.surface_color", Color("#171B24"))
var hover_neutral: Color = _confirmation_dialog_color("danger_hover_surface", Color("#3B252C"))
var pressed_neutral: Color = _confirmation_dialog_color("danger_pressed_surface", Color("#4A252D"))
var border_width: int = _confirmation_dialog_integer("button_border_width", 2, 1, 5)
var radius: int = _confirmation_dialog_integer("button_radius", 12, 8, 24)
var normal_color: Color = blue if primary else neutral
var hover_color: Color = blue.lightened(0.12) if primary else hover_neutral
var pressed_color: Color = blue.darkened(0.12) if primary else pressed_neutral
var edge_color: Color = accent if primary else danger
button.add_theme_stylebox_override("normal", _make_scenery_erase_button_style(normal_color, edge_color, border_width, radius))
button.add_theme_stylebox_override("hover", _make_scenery_erase_button_style(hover_color, edge_color.lightened(0.12), border_width + 1, radius))
button.add_theme_stylebox_override("pressed", _make_scenery_erase_button_style(pressed_color, edge_color, border_width + 1, radius))
button.add_theme_stylebox_override("hover_pressed", _make_scenery_erase_button_style(pressed_color.lightened(0.05), edge_color, border_width + 1, radius))
button.add_theme_stylebox_override("focus", _make_scenery_erase_button_style(normal_color, accent, border_width + 1, radius))
var label_color: Color = Color.WHITE if primary else danger
button.add_theme_color_override("font_color", label_color)
button.add_theme_color_override("font_hover_color", label_color)
button.add_theme_color_override("font_pressed_color", label_color)
button.add_theme_font_size_override("font_size", _confirmation_dialog_integer("button_font_size", 20, 16, 32))
button.custom_minimum_size = Vector2(
float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240)) if not primary else float(_confirmation_dialog_integer("keep_button_width", 132, 112, 220)),
float(_confirmation_dialog_integer("button_height", 48, 44, 80))
)
var font: Font = _ui_font
if primary and font != null:
var strong_font: FontVariation = FontVariation.new()
strong_font.base_font = font
strong_font.variation_embolden = 0.55
button.add_theme_font_override("font", strong_font)
else:
_apply_ui_font(button)
func _make_scenery_erase_button_style(background: Color, border: Color, width: int, radius: int) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(width)
style.set_corner_radius_all(radius)
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 8.0
style.content_margin_bottom = 8.0
return style
func _confirmation_dialog_integer(token: String, fallback: int, minimum: int, maximum: int) -> int:
return fallback if _sandbox_theme == null else _sandbox_theme.integer("popups.confirmation." + token, fallback, minimum, maximum)
func _confirmation_dialog_color(token: String, fallback: Color) -> Color:
return fallback if _sandbox_theme == null else _sandbox_theme.color("popups.confirmation." + token, fallback)
func _open_scenery_erase_piece_dialog() -> void:
if _scenery_erase_piece_dialog == null:
return
var popup_size: Vector2i = _scenery_erase_piece_dialog_size()
_apply_scenery_erase_piece_dialog_layout(popup_size.x)
if _scenery_erase_piece_dialog_panel != null:
_scenery_erase_piece_dialog_panel.custom_minimum_size = Vector2(popup_size)
_scenery_erase_piece_dialog.visible = true
call_deferred("_focus_scenery_erase_piece_keep_button")
func _scenery_erase_piece_dialog_size() -> Vector2i:
var preferred: Vector2i = Vector2i(520, 208)
var raw_size: Variant = _sandbox_theme.value("popups.confirmation.scenery_erase_size", [520, 208]) if _sandbox_theme != null else [520, 208]
if raw_size is Array and (raw_size as Array).size() == 2:
preferred = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1]))
var available: Vector2i = Vector2i(get_tree().root.size)
var usable_rect: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen())
if usable_rect.size.x > 0 and usable_rect.size.y > 0:
available.x = mini(available.x, usable_rect.size.x)
available.y = mini(available.y, usable_rect.size.y)
available = Vector2i(maxi(1, available.x - 32), maxi(1, available.y - 32))
var width: int = mini(preferred.x, available.x)
var height: int = mini(preferred.y, available.y)
if width < 440:
height = mini(available.y, height + 56)
return Vector2i(width, height)
func _apply_scenery_erase_piece_dialog_layout(dialog_width: int) -> void:
if _scenery_erase_piece_dialog_actions == null or _scenery_erase_piece_keep_button == null or _scenery_erase_piece_remove_button == null:
return
var stacked: bool = dialog_width < 440
_scenery_erase_piece_dialog_actions.vertical = stacked
_scenery_erase_piece_dialog_actions.alignment = BoxContainer.ALIGNMENT_END
_scenery_erase_piece_dialog_actions.add_theme_constant_override("separation", 8 if stacked else _confirmation_dialog_integer("footer_gap", 12, 8, 24))
if stacked:
var button_width: float = maxf(1.0, float(dialog_width - _confirmation_dialog_integer("padding", 18, 12, 32) * 2))
var button_height: float = float(_confirmation_dialog_integer("button_height", 48, 44, 80))
_scenery_erase_piece_remove_button.custom_minimum_size = Vector2(button_width, button_height)
_scenery_erase_piece_keep_button.custom_minimum_size = Vector2(button_width, button_height)
else:
_scenery_erase_piece_remove_button.custom_minimum_size.x = float(_confirmation_dialog_integer("remove_button_width", 170, 128, 240))
_scenery_erase_piece_keep_button.custom_minimum_size.x = float(_confirmation_dialog_integer("keep_button_width", 132, 112, 220))
func _refresh_scenery_erase_piece_dialog_size() -> void:
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible:
_open_scenery_erase_piece_dialog()
func _focus_scenery_erase_piece_keep_button() -> void:
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible and _scenery_erase_piece_keep_button != null:
_scenery_erase_piece_keep_button.grab_focus()
func _dismiss_scenery_erase_piece_dialog() -> void:
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible:
_scenery_erase_piece_dialog.hide()
_clear_erase_scenery_targets()
func _confirm_erase_scenery_from_dialog() -> void:
_confirm_erase_selected_scenery()
if _scenery_erase_piece_dialog != null and _scenery_erase_piece_dialog.visible:
_scenery_erase_piece_dialog.hide()
func _make_surface_choice_cue(kind: StringName, finish: StringName) -> StageSurfacePreview:
var cue: StageSurfacePreview = STAGE_SURFACE_PREVIEW_SCRIPT.new() as StageSurfacePreview
cue.custom_minimum_size = Vector2(64.0, 40.0)
cue.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
cue.set_rounded_frame(
float(_sandbox_theme.integer("popups.asset_selector.look_corner_radius", 8, 2, 20)) if _sandbox_theme != null else 8.0,
_sandbox_theme.color("popups.style_defaults.bg_color", Color("#202633FA")) if _sandbox_theme != null else Color("#202633FA")
)
cue.configure(kind, finish, STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish))
return cue
@@ -3319,14 +3649,17 @@ func _surface_color_presets() -> Array[Color]:
return colors
func _make_scenery_repair_button(node_name: String, label: String, prominent: bool) -> Button:
func _make_scenery_repair_button(node_name: String, label: String, prominent: bool, destructive: bool = false) -> Button:
var button: Button = Button.new()
button.name = node_name
button.text = label
button.focus_mode = Control.FOCUS_ALL
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.custom_minimum_size.y = float(_stage_panel_integer("action.min_target_size", 48, 44, 96))
_scenery_inspector.style_action_button(button, prominent)
if destructive:
_scenery_inspector.style_destructive_button(button)
else:
_scenery_inspector.style_action_button(button, prominent)
return button
@@ -3365,6 +3698,65 @@ func _build_use_target_capture_bar(ui: CanvasLayer) -> void:
_use_target_capture_bar.change_prop_requested.connect(_choose_different_use_prop)
_use_target_capture_bar.capture_action_requested.connect(_on_capture_action_requested)
ui.add_child(_use_target_capture_bar)
_build_scenery_depth_cursor_badge(ui)
func _build_scenery_depth_cursor_badge(ui: CanvasLayer) -> void:
_scenery_depth_cursor_badge = PanelContainer.new()
_scenery_depth_cursor_badge.name = "SceneryDepthCursorBadge"
_scenery_depth_cursor_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
_scenery_depth_cursor_badge.z_index = 2
var style := StyleBoxFlat.new()
style.bg_color = _stage_panel_color("choice.selected_surface", Color("#12394A"))
style.border_color = _sandbox_theme.color("mode_colors.edit", Color("#22C6FF")) if _sandbox_theme != null else Color("#22C6FF")
style.set_border_width_all(1)
style.set_corner_radius_all(10)
style.content_margin_left = 9.0
style.content_margin_right = 9.0
style.content_margin_top = 3.0
style.content_margin_bottom = 3.0
_scenery_depth_cursor_badge.add_theme_stylebox_override("panel", style)
_scenery_depth_cursor_badge_label = Label.new()
_scenery_depth_cursor_badge_label.name = "DepthLabel"
_scenery_depth_cursor_badge_label.text = "MIDDLE"
_scenery_depth_cursor_badge_label.add_theme_font_size_override("font_size", 13)
_scenery_depth_cursor_badge_label.add_theme_color_override("font_color", Color("#F5F7FA"))
_apply_ui_font(_scenery_depth_cursor_badge_label)
_scenery_depth_cursor_badge.add_child(_scenery_depth_cursor_badge_label)
_scenery_depth_cursor_badge.visible = false
ui.add_child(_scenery_depth_cursor_badge)
func _update_scenery_depth_cursor_badge() -> void:
if _scenery_depth_cursor_badge == null or _scenery_controller == null:
return
if current_mode != StageMode.EDIT or _scenery_controller.get_active_tool() != &"artwork":
_scenery_depth_cursor_badge.visible = false
return
var mouse_position: Vector2 = get_viewport().get_mouse_position()
if _use_target_capture_bar != null and _use_target_capture_bar.visible and _use_target_capture_bar.get_global_rect().has_point(mouse_position):
_scenery_depth_cursor_badge.visible = false
return
var ghost_bounds: Rect2 = _scenery_controller.get_active_artwork_preview_bounds()
if ghost_bounds.size.x <= 0.0 or ghost_bounds.size.y <= 0.0:
_scenery_depth_cursor_badge.visible = false
return
var depth: StringName = _scenery_controller.get_artwork_depth()
var depth_label: String = "Back" if depth == &"far" else "Front" if depth == &"floor" else "Middle"
_scenery_depth_cursor_badge_label.text = depth_label.to_upper()
var badge_size: Vector2 = _scenery_depth_cursor_badge.get_combined_minimum_size()
_scenery_depth_cursor_badge.size = badge_size
var viewport_size: Vector2 = get_viewport_rect().size
var screen_top_left: Vector2 = _world_to_screen(ghost_bounds.position)
var screen_bottom_right: Vector2 = _world_to_screen(ghost_bounds.end)
var ghost_screen_rect: Rect2 = Rect2(screen_top_left, screen_bottom_right - screen_top_left).abs()
var badge_position := Vector2(ghost_screen_rect.get_center().x - badge_size.x * 0.5, ghost_screen_rect.position.y - badge_size.y - 8.0)
if badge_position.y < 8.0:
badge_position.y = ghost_screen_rect.position.y + 8.0
badge_position.x = clampf(badge_position.x, 8.0, maxf(8.0, viewport_size.x - badge_size.x - 8.0))
badge_position.y = clampf(badge_position.y, 8.0, maxf(8.0, viewport_size.y - badge_size.y - 8.0))
_scenery_depth_cursor_badge.position = badge_position
_scenery_depth_cursor_badge.visible = true
func _build_ground_finish_popup(ui: CanvasLayer) -> void:
@@ -4527,7 +4919,7 @@ func _refresh_scenery_inspector() -> void:
_scenery_wall_look_group.visible = is_wall
_scenery_background_ground_look_heading.visible = is_background_ground
_scenery_background_ground_look_group.visible = is_background_ground
_scenery_surface_helper.visible = surface != null
_scenery_surface_helper.visible = is_wall
_scenery_surface_colors.visible = false
if surface != null:
if is_wall:
@@ -4535,7 +4927,6 @@ func _refresh_scenery_inspector() -> void:
_scenery_surface_helper.text = "Looks like a wall · Stickmen can pass through."
else:
_scenery_background_ground_look_group.select_no_signal(surface.finish_id)
_scenery_surface_helper.text = "Decorative only · Stickmen don’t walk here."
var color_editable: bool = is_wall or surface.finish_id == &"finish_background_carpet"
_scenery_surface_colors.visible = color_editable
if color_editable:
@@ -4543,16 +4934,18 @@ func _refresh_scenery_inspector() -> void:
_scenery_surface_colors.set_recent_colors(_recent_surface_colors)
_scenery_depth_section.visible = is_artwork
if artwork != null:
_set_scenery_depth_preview_path(artwork.asset_path)
_scenery_depth_group.select_no_signal(artwork.depth)
_scenery_size_row.visible = not is_missing or artwork.has_known_bounds
_scenery_smaller_button.disabled = is_missing and not artwork.has_known_bounds
_scenery_bigger_button.disabled = is_missing and not artwork.has_known_bounds
_scenery_erase_wall_area_button.visible = is_wall
_scenery_erase_background_ground_area_button.visible = is_background_ground
_scenery_erase_piece_button.visible = surface != null
if is_background_ground:
_scenery_erase_piece_button.text = "Erase background ground"
_scenery_erase_piece_button.text = "Remove background ground"
else:
_scenery_erase_piece_button.text = "Erase wall panel"
_scenery_erase_piece_button.text = "Remove wall panel"
_scenery_find_button.visible = is_missing
_scenery_choose_button.visible = is_missing
_scenery_keep_button.visible = is_missing
@@ -4595,6 +4988,16 @@ func _is_scenery_node(node: Node2D) -> bool:
return node != null and (node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT or node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT)
func _has_remaining_scenery() -> bool:
if _world == null or not is_instance_valid(_world):
return false
for child: Node in _world.get_children():
var node: Node2D = child as Node2D
if node != null and not node.is_queued_for_deletion() and _is_scenery_node(node):
return true
return false
func _on_selected_surface_look_choice(finish: StringName) -> void:
var surface: StageRectSurface = _selected_rect_surface()
if surface == null:
@@ -4737,10 +5140,10 @@ func _request_erase_selected_scenery() -> void:
_scenery_erase_piece_targets.append(weakref(node))
var count: int = targets.size()
if count == 1:
_scenery_erase_piece_dialog.dialog_text = "Erase this piece? You can’t undo this."
_scenery_erase_piece_dialog_message.text = "Remove this whole piece? You can’t undo this."
else:
_scenery_erase_piece_dialog.dialog_text = "Erase these %d scenery pieces? You can’t undo this." % count
_scenery_erase_piece_dialog.popup_centered()
_scenery_erase_piece_dialog_message.text = "Remove these %d scenery pieces? You can’t undo this." % count
_open_scenery_erase_piece_dialog()
func _confirm_erase_selected_scenery() -> void:
@@ -4764,7 +5167,10 @@ func _confirm_erase_selected_scenery() -> void:
_director_visuals.mark_dirty()
object_deleted.emit(erased)
_mark_scene_dirty()
_refresh_scenery_inspector()
if not _has_remaining_scenery():
_exit_scenery_edit_mode()
else:
_refresh_scenery_inspector()
_refresh_status()
@@ -4790,13 +5196,31 @@ func _begin_selected_wall_erase() -> void:
_begin_wall_erase_tool()
func _begin_selected_background_ground_erase() -> void:
var selected: Array[Node2D] = _selection.get_selected() if _selection != null else []
if selected.size() != 1 or selected[0].get_script() != STAGE_BACKGROUND_GROUND_SCRIPT or _scenery_controller == null:
return
_begin_background_ground_erase_tool()
func _begin_wall_erase_tool() -> void:
_begin_surface_erase_tool(&"wall")
func _begin_background_ground_erase_tool() -> void:
_begin_surface_erase_tool(&"background_ground")
func _begin_surface_erase_tool(surface_kind: StringName) -> void:
if _scenery_controller == null or not _scenery_edit_mode:
return
_selection.clear_selection()
_scenery_inspector.hide()
if not _scenery_controller.begin_wall_eraser(_viewport_to_world(get_viewport().get_mouse_position())):
_show_toast("Could not start the wall eraser. Try again.")
var pointer_world: Vector2 = _viewport_to_world(get_viewport().get_mouse_position())
var started: bool = _scenery_controller.begin_surface_eraser(pointer_world, surface_kind)
if not started:
var surface_label: String = "wall and background ground" if surface_kind == &"all" else ("wall" if surface_kind == &"wall" else "background ground")
_show_toast("Could not start the %s eraser. Try again." % surface_label)
_refresh_scenery_capture_bar()
@@ -4926,7 +5350,6 @@ func _on_scenery_placement_committed(node: Node2D) -> void:
if node == null or not is_instance_valid(node):
return
_scenery_placed_count += 1
_mark_scene_dirty()
_refresh_scenery_capture_bar()
@@ -4941,34 +5364,40 @@ func _refresh_scenery_capture_bar() -> void:
_use_target_capture_bar.clear_horizontal_safe_bounds()
var actions: Array[Dictionary] = []
if _scenery_controller.get_active_tool() == &"artwork":
actions.append({"id": "depth_far", "label": "Far"})
actions.append({"id": "depth_near", "label": "Near"})
actions.append({"id": "depth_floor", "label": "On the floor"})
_set_scenery_depth_preview_path(_scenery_controller.get_active_artwork_path())
var selected_depth: StringName = _scenery_controller.get_artwork_depth()
actions.append(_make_scenery_depth_capture_action(&"depth_far", &"far", "Back", selected_depth))
actions.append(_make_scenery_depth_capture_action(&"depth_near", &"near", "Middle", selected_depth))
actions.append(_make_scenery_depth_capture_action(&"depth_floor", &"floor", "Front", selected_depth))
var artwork_name: String = String(_scenery_controller.get_active_artwork_name())
var prompt: String = "Click to place %s" % artwork_name if _scenery_placed_count == 0 else "Place another %s · placed pieces stay" % artwork_name
var prompt: String = "Click to place %s" % artwork_name if _scenery_placed_count == 0 else "Place another · Cancel undoes"
actions.append({"id": "done_tool", "label": "Done", "primary": true})
actions.append({"id": "cancel_tool", "label": "✕ Cancel"})
_use_target_capture_bar.set_capture_actions(prompt, actions)
elif _scenery_controller.get_active_tool() == &"wall_eraser":
var erase_prompt: String = "Drag over the wall · release to erase"
if _scenery_controller.get_wall_areas_erased() > 0:
erase_prompt = "Erase another area · earlier openings stay"
actions.append({"id": "cancel_tool", "label": "Cancel"})
_use_target_capture_bar.set_scenery_capture_actions(prompt, actions)
elif _scenery_controller.get_active_tool() in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"]:
var erase_surface: String = _scenery_controller.get_active_eraser_surface_label()
var erase_prompt: String = "Drag over the %s · release to erase" % erase_surface
if _scenery_controller.get_active_tool() == &"scenery_eraser":
erase_prompt = "Drag over wall or ground to erase"
if _scenery_controller.get_surface_areas_erased() > 0:
erase_prompt = "Erase another area · Cancel undoes"
actions.append({"id": "done_tool", "label": "Done", "primary": true})
actions.append({"id": "cancel_tool", "label": "✕ Cancel"})
_use_target_capture_bar.set_capture_actions(erase_prompt, actions)
actions.append({"id": "cancel_tool", "label": "Cancel"})
_use_target_capture_bar.set_scenery_capture_actions(erase_prompt, actions)
else:
actions.append({"id": "choose_scenery", "label": "Choose scenery"})
actions.append({"id": "done_tool", "label": "Done", "primary": true})
actions.append({"id": "cancel_tool", "label": "✕ Cancel"})
actions.append({"id": "cancel_tool", "label": "Cancel"})
if _scenery_controller.get_active_tool() == &"background_ground":
var ground_prompt: String = "Draw background ground · No walking"
if _scenery_placed_count > 0:
ground_prompt = "Draw another · placed pieces stay"
_use_target_capture_bar.set_capture_actions(ground_prompt, actions)
ground_prompt = "Draw another · Cancel undoes"
_use_target_capture_bar.set_scenery_capture_actions(ground_prompt, actions)
else:
var wall_prompt: String = "Draw wall panel · Stickmen pass through"
if _scenery_placed_count > 0:
wall_prompt = "Draw another · placed pieces stay"
_use_target_capture_bar.set_capture_actions(wall_prompt, actions)
wall_prompt = "Draw another · Cancel undoes"
_use_target_capture_bar.set_scenery_capture_actions(wall_prompt, actions)
return
if _scenery_controller != null and _scenery_controller.is_group_preview_pending():
_use_target_capture_bar.clear_horizontal_safe_bounds()
@@ -4976,52 +5405,24 @@ func _refresh_scenery_capture_bar() -> void:
{"id": "keep_group", "label": "Keep changes", "primary": true},
{"id": "cancel_group", "label": "✕ Cancel"},
]
var group_prompt: String = "Move together · Ground and Props stay put" if _scenery_group_mode == &"move" else "Resize together · Ground and Props stay put"
_use_target_capture_bar.set_capture_actions(group_prompt, preview_actions)
var group_count: int = _selection.get_selected().size()
var group_verb: String = "Move" if _scenery_group_mode == &"move" else "Resize"
var group_prompt: String = "%s %d scenery pieces\nGround and Props stay put" % [group_verb, group_count]
_use_target_capture_bar.set_scenery_capture_actions(group_prompt, preview_actions)
return
if _scenery_controller != null and _scenery_controller.is_single_transform_active():
_set_scenery_selection_capture_bounds()
var target: Node2D = _scenery_controller.get_single_transform_node()
var target_name: String = "scenery"
if target != null and target.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT:
target_name = (target as StageSceneryArtwork).display_name
elif target != null and target.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT:
target_name = "background ground"
elif target != null:
target_name = "wall panel"
var is_move: bool = _scenery_controller.get_single_transform_kind() == &"move"
var prompt_verb: String = "Move" if is_move else "Resize"
var release_verb: String = "place" if is_move else "resize"
var cancel_actions: Array[Dictionary] = [{"id": "cancel_single", "label": "✕ Cancel"}]
_use_target_capture_bar.set_capture_actions("%s %s · release to %s" % [prompt_verb, target_name, release_verb], cancel_actions)
_hide_scenery_capture_bar()
return
if _scenery_edit_mode:
var count: int = _selection.get_selected().size()
if count == 1:
_set_scenery_selection_capture_bounds()
else:
_use_target_capture_bar.clear_horizontal_safe_bounds()
var prompt: String = "%d scenery piece%s selected" % [count, "" if count == 1 else "s"]
var edit_actions: Array[Dictionary] = []
if count > 1:
edit_actions.append({"id": "move_group", "label": "Move together"})
edit_actions.append({"id": "resize_group", "label": "Resize together"})
edit_actions.append({"id": "clear_selection", "label": "Clear selection"})
else:
edit_actions.append({"id": "select_several", "label": "Select several"})
edit_actions.append({"id": "select_all", "label": "Select all scenery"})
if count > 0:
edit_actions.append({"id": "clear_selection", "label": "Clear selection"})
else:
edit_actions.append({"id": "erase_wall_area", "label": "Erase wall area"})
edit_actions.append({"id": "done_edit", "label": "Done editing", "primary": true})
_use_target_capture_bar.set_capture_actions(prompt, edit_actions)
_set_scenery_selection_capture_bounds(count == 1)
_use_target_capture_bar.set_scenery_edit_state(count)
return
_use_target_capture_bar.clear_horizontal_safe_bounds()
_use_target_capture_bar.hide_capture()
func _set_scenery_selection_capture_bounds() -> void:
func _set_scenery_selection_capture_bounds(include_inspector: bool = true) -> void:
if _use_target_capture_bar == null:
return
var viewport_width: float = get_viewport_rect().size.x
@@ -5030,7 +5431,9 @@ func _set_scenery_selection_capture_bounds() -> void:
var palette_width: float = float(palette_config.get("dock_width", 116.0))
var panel_width: float = float(_stage_panel_integer("shell.preferred_width", 380, 220, 720))
var left: float = palette_width + inset
var right: float = viewport_width - panel_width - inset * 2.0
var right: float = viewport_width - inset
if include_inspector:
right = viewport_width - panel_width - inset * 2.0
_use_target_capture_bar.set_horizontal_safe_bounds(left, right)
@@ -5042,6 +5445,8 @@ func _hide_scenery_capture_bar() -> void:
func _on_capture_action_requested(action_id: StringName) -> void:
match action_id:
&"choose_scenery":
_open_selector("scenery")
&"depth_far":
_scenery_controller.set_depth(&"far")
_refresh_scenery_capture_bar()
@@ -5051,9 +5456,12 @@ func _on_capture_action_requested(action_id: StringName) -> void:
&"depth_floor":
_scenery_controller.set_depth(&"floor")
_refresh_scenery_capture_bar()
&"done_tool", &"cancel_tool":
&"done_tool":
_scenery_controller.exit_tool()
_hide_scenery_capture_bar()
&"cancel_tool":
_scenery_controller.cancel_active_tool()
_hide_scenery_capture_bar()
&"select_several":
_scenery_multi_select = true
_show_toast("Tap scenery pieces to add them. Shift-click also works.")
@@ -5065,27 +5473,30 @@ func _on_capture_action_requested(action_id: StringName) -> void:
_refresh_scenery_capture_bar()
&"erase_wall_area":
_begin_wall_erase_tool()
&"erase_area":
_begin_surface_erase_tool(&"all")
&"move_group":
_begin_scenery_group_transform(&"move")
&"resize_group":
_begin_scenery_group_transform(&"resize")
&"keep_group":
_scenery_controller.commit_group()
_scenery_group_mode = &""
_refresh_scenery_capture_bar()
_keep_scenery_group_preview()
&"cancel_group":
_scenery_controller.cancel_group()
_scenery_group_mode = &""
_refresh_scenery_capture_bar()
&"cancel_single":
_scenery_controller.cancel_single_transform()
_scenery_single_dragging = false
_refresh_scenery_inspector()
_refresh_scenery_capture_bar()
&"done_edit":
_exit_scenery_edit_mode()
func _keep_scenery_group_preview() -> void:
if _scenery_controller == null or not _scenery_controller.is_group_preview_pending():
return
_scenery_controller.commit_group()
_scenery_group_mode = &""
_refresh_scenery_capture_bar()
func _begin_scenery_group_transform(kind: StringName) -> void:
var selected: Array[Node2D] = _selection.get_selected().duplicate()
if _scenery_controller == null or not _scenery_controller.begin_group_transform(kind, selected):
+1 -1
View File
@@ -157,7 +157,7 @@ func _scenery_background_entries() -> Array[Dictionary]:
{"id": "sky_sunset", "name": "Sunset", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌅"},
{"id": "sky_starry_night", "name": "Starry Night", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌙"},
{"id": "wall_panel_setup", "name": "Wall panel", "category": "Scenery", "gallery_tab": "backgrounds", "background_kind": "wall_setup", "icon": "🧱"},
{"id": "background_ground_setup", "name": "Background ground", "category": "Scenery", "description": "Decorative · No walking", "gallery_tab": "backgrounds", "background_kind": "background_ground_setup", "icon": "🌿"}
{"id": "background_ground_setup", "name": "Background ground", "category": "Scenery", "gallery_tab": "backgrounds", "background_kind": "background_ground_setup", "icon": "🌿"}
]
@@ -60,6 +60,8 @@ func collect(
entry["size"] = STAGE_SCENE_IO.vector_to_data(background.panel_size)
entry["finish_id"] = String(background.finish_id)
entry["color"] = "#" + background.tint.to_html(true)
if not background.motif_origin_offset.is_zero_approx():
entry["motif_origin_offset"] = STAGE_SCENE_IO.vector_to_data(background.motif_origin_offset)
elif stage_node.get_script() == STAGE_WALL_PANEL:
var wall: StageWallPanel = stage_node as StageWallPanel
entry["size"] = STAGE_SCENE_IO.vector_to_data(wall.panel_size)
@@ -182,6 +184,8 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
StringName(String(entry.get("finish_id", "finish_background_grass"))),
STAGE_SCENE_IO.color_from_data(entry.get("color", "#72A84CFF"), Color("#72A84C"))
)
if entry.has("motif_origin_offset") and STAGE_SCENE_IO.is_vector_data(entry.get("motif_origin_offset")):
background.motif_origin_offset = STAGE_SCENE_IO.data_to_vector(entry.get("motif_origin_offset"), Vector2.ZERO)
world.add_child(background)
node = background
"scenery_artwork":
+248 -37
View File
@@ -1,6 +1,6 @@
class_name StageSceneryController
extends Node
## Owns inert Scenery placement drafts and snapshot-based group transforms.
## Owns scenery placement transactions, eraser transactions, and snapshot-based transforms.
signal placement_committed(node: Node2D)
signal document_change_requested
@@ -17,7 +17,6 @@ const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
const MIN_PANEL_SIZE: float = 24.0
const MIN_WALL_ERASE_SIZE: float = 8.0
const DEFAULT_PANEL_SIZE: Vector2 = Vector2(240.0, 280.0)
const DEFAULT_BACKGROUND_SIZE: Vector2 = Vector2(800.0, 320.0)
const WALL_ERASER_CURSOR_SIZE: Vector2 = Vector2(120.0, 120.0)
const MIN_ART_SCALE: float = 0.08
const MAX_ART_SCALE: float = 12.0
@@ -28,6 +27,7 @@ var _camera: Camera2D
var _ghost: Node2D
var _active_tool: StringName = &""
var _asset_path: String = ""
var _placement_nodes: Array[Node2D] = []
var _depth: StringName = &"near"
var _wall_finish: StringName = &"finish_wall_paint"
var _wall_tint: Color = Color("#5A9D70")
@@ -38,7 +38,12 @@ var _wall_anchor_active: bool = false
var _wall_erase_start: Vector2 = Vector2.INF
var _wall_erase_dragging: bool = false
var _wall_erase_pointer: Vector2 = Vector2.ZERO
var _wall_areas_erased: int = 0
var _surface_erase_kind: StringName = &"wall"
var _surface_areas_erased: int = 0
# Keep original surfaces detached during an eraser session so Cancel can restore them.
var _surface_erase_originals: Array[Dictionary] = []
var _surface_erase_detached: Array[Node2D] = []
var _surface_erase_changed: bool = false
var _snap_enabled: bool = false
var _grid_size: float = 15.0
var _group_kind: StringName = &""
@@ -102,7 +107,21 @@ func get_active_tool() -> StringName:
func get_wall_areas_erased() -> int:
return _wall_areas_erased
return _surface_areas_erased
func get_surface_areas_erased() -> int:
return _surface_areas_erased
func is_surface_eraser_active() -> bool:
return _is_surface_eraser()
func get_active_eraser_surface_label() -> String:
if _surface_erase_kind == &"all":
return "wall or ground"
return "background ground" if _surface_erase_kind == &"background_ground" else "wall"
func get_group_bounds() -> Rect2:
@@ -115,6 +134,20 @@ func get_active_artwork_name() -> String:
return _asset_path.get_file().get_basename()
func get_active_artwork_path() -> String:
return _asset_path if _active_tool == &"artwork" else ""
func get_active_artwork_preview_bounds() -> Rect2:
if _active_tool != &"artwork" or _ghost == null or not is_instance_valid(_ghost) or _ghost.get_script() != SCENERY_ARTWORK:
return Rect2()
return (_ghost as StageSceneryArtwork).get_visual_world_bounds()
func get_artwork_depth() -> StringName:
return _depth
func get_active_surface_label() -> String:
if _active_tool == &"background_ground":
return "background ground"
@@ -123,7 +156,7 @@ func get_active_surface_label() -> String:
func handle_key(event: InputEventKey) -> bool:
if is_active() and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
exit_tool()
cancel_active_tool()
return true
if _group_pending and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
cancel_group()
@@ -171,13 +204,12 @@ func _begin_rect_surface(tool: StringName, finish: StringName, initial_world_pos
_wall_dragging = false
_wall_anchor_active = false
var panel: StageRectSurface
var initial_size: Vector2 = DEFAULT_PANEL_SIZE
if tool == &"background_ground":
panel = BACKGROUND_GROUND.new() as StageBackgroundGround
initial_size = DEFAULT_BACKGROUND_SIZE
else:
panel = WALL_PANEL.new() as StageWallPanel
panel.configure(initial_size, _wall_finish, _wall_tint)
panel.interactive_preview = true
panel.configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint)
panel.modulate = Color(1.0, 1.0, 1.0, 0.45)
_ghost_holder.add_child(panel)
_ghost = panel
@@ -188,16 +220,37 @@ func _begin_rect_surface(tool: StringName, finish: StringName, initial_world_pos
func begin_wall_eraser(initial_world_position: Vector2) -> bool:
return begin_surface_eraser(initial_world_position, &"wall")
func begin_background_ground_eraser(initial_world_position: Vector2) -> bool:
return begin_surface_eraser(initial_world_position, &"background_ground")
func begin_surface_eraser(initial_world_position: Vector2, surface_kind: StringName) -> bool:
if _world == null or _ghost_holder == null:
return false
if surface_kind not in [&"wall", &"background_ground", &"all"]:
return false
exit_tool()
_active_tool = &"wall_eraser"
_surface_erase_kind = surface_kind
match surface_kind:
&"background_ground":
_active_tool = &"background_ground_eraser"
&"all":
_active_tool = &"scenery_eraser"
_:
_active_tool = &"wall_eraser"
_wall_erase_start = Vector2.INF
_wall_erase_dragging = false
_wall_areas_erased = 0
_surface_areas_erased = 0
_surface_erase_originals.clear()
_surface_erase_detached.clear()
_surface_erase_changed = false
_capture_surface_eraser_snapshot(surface_kind)
_wall_erase_pointer = initial_world_position
var preview := Node2D.new()
preview.name = "WallEraserPreview"
preview.name = "BackgroundGroundEraserPreview" if surface_kind == &"background_ground" else "WallEraserPreview"
var fill := Polygon2D.new()
fill.name = "EraseArea"
fill.color = Color("#FF5B62", 0.2)
@@ -237,7 +290,7 @@ func update_pointer(world_position: Vector2) -> void:
elif _is_rect_placement_tool():
var preview_top_left: Vector2 = _snap(world_position) if _snap_enabled else world_position
_ghost.global_position = _move_node_visible_top_left(_ghost, preview_top_left)
elif _active_tool == &"wall_eraser":
elif _is_surface_eraser():
_wall_erase_pointer = world_position
if _wall_erase_dragging:
_set_wall_eraser_preview_rect(_drag_rect(_wall_erase_start, _snap(world_position)))
@@ -252,13 +305,17 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
if not is_active():
return false
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
if _wall_dragging or _wall_anchor_active:
cancel_active_gesture()
exit_tool()
# Right-click finishes placement and area-erasure sessions. This commits
# completed work while dropping any unfinished preview gesture. Cancel
# and Escape remain the explicit rollback paths.
if _is_placement_tool() or _is_surface_eraser():
exit_tool()
else:
cancel_active_tool()
return true
var completes_drag: bool = not event.pressed and event.button_index == MOUSE_BUTTON_LEFT and (
(_is_rect_placement_tool() and _wall_dragging)
or (_active_tool == &"wall_eraser" and _wall_erase_dragging)
or (_is_surface_eraser() and _wall_erase_dragging)
)
if (over_ui and not completes_drag) or event.button_index != MOUSE_BUTTON_LEFT:
return false
@@ -292,7 +349,7 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
_wall_anchor_active = false
_restore_rect_surface_ghost(world_position)
return true
if _active_tool == &"wall_eraser":
if _is_surface_eraser():
if event.pressed:
_wall_erase_start = _snap(world_position) if _snap_enabled else world_position
_wall_erase_dragging = true
@@ -304,10 +361,10 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
_wall_erase_dragging = false
_wall_erase_start = Vector2.INF
if erase_rect.size.x >= MIN_WALL_ERASE_SIZE and erase_rect.size.y >= MIN_WALL_ERASE_SIZE:
var changed: int = _erase_wall_region(erase_rect)
var changed: int = _erase_surface_region(erase_rect, _surface_erase_kind)
if changed > 0:
_wall_areas_erased += 1
document_change_requested.emit()
_surface_areas_erased += 1
_surface_erase_changed = true
update_pointer(world_position)
return true
return false
@@ -329,13 +386,17 @@ func cancel_active_gesture() -> void:
_wall_erase_start = Vector2.INF
if _is_rect_placement_tool() and _ghost != null and is_instance_valid(_ghost):
(_ghost as StageRectSurface).configure(_default_size_for_active_surface(), _wall_finish, _wall_tint)
elif _active_tool == &"wall_eraser" and _ghost != null and is_instance_valid(_ghost):
elif _is_surface_eraser() and _ghost != null and is_instance_valid(_ghost):
_set_wall_eraser_preview_rect(Rect2(_wall_erase_pointer, WALL_ERASER_CURSOR_SIZE))
func exit_tool() -> void:
if _single_active:
cancel_single_transform()
if _is_placement_tool():
commit_placement_session()
if _is_surface_eraser():
_commit_surface_eraser()
cancel_active_gesture()
_free_ghost()
_active_tool = &""
@@ -346,6 +407,115 @@ func exit_tool() -> void:
preview_state_changed.emit(false)
func cancel_active_tool() -> void:
if _is_surface_eraser():
cancel_surface_eraser()
return
if _is_placement_tool():
_rollback_placement_session()
exit_tool()
func commit_placement_session() -> void:
if not _is_placement_tool():
return
var has_placed_nodes: bool = false
for node: Node2D in _placement_nodes:
if node != null and is_instance_valid(node) and node.get_parent() != null:
has_placed_nodes = true
_placement_nodes.clear()
if has_placed_nodes:
document_change_requested.emit()
func _rollback_placement_session() -> void:
for node: Node2D in _placement_nodes:
if node == null or not is_instance_valid(node):
continue
var parent: Node = node.get_parent()
if parent != null:
parent.remove_child(node)
node.queue_free()
_placement_nodes.clear()
func cancel_surface_eraser() -> void:
if not _is_surface_eraser():
return
if _surface_erase_changed:
_restore_surface_eraser_snapshot()
_surface_areas_erased = 0
_clear_surface_eraser_transaction()
exit_tool()
func _commit_surface_eraser() -> void:
if _surface_erase_changed:
document_change_requested.emit()
for detached: Node2D in _surface_erase_detached:
if detached != null and is_instance_valid(detached):
detached.queue_free()
_clear_surface_eraser_transaction()
func _clear_surface_eraser_transaction() -> void:
_surface_erase_originals.clear()
_surface_erase_detached.clear()
_surface_erase_changed = false
func _capture_surface_eraser_snapshot(surface_kind: StringName) -> void:
if _world == null or not is_instance_valid(_world):
return
var target_scripts: Array[Script] = _surface_erase_target_scripts(surface_kind)
for child: Node in _world.get_children():
if not target_scripts.has(child.get_script()):
continue
var surface: Node2D = child as Node2D
if surface == null:
continue
_surface_erase_originals.append({
"node": surface,
"parent": surface.get_parent(),
"index": surface.get_index(),
})
func _restore_surface_eraser_snapshot() -> void:
if _world == null or not is_instance_valid(_world):
return
var original_ids: Dictionary = {}
for snapshot: Dictionary in _surface_erase_originals:
var original: Node2D = snapshot.get("node") as Node2D
if original != null and is_instance_valid(original):
original_ids[original.get_instance_id()] = true
var target_scripts: Array[Script] = _surface_erase_target_scripts(_surface_erase_kind)
for child: Node in _world.get_children().duplicate():
if not target_scripts.has(child.get_script()):
continue
_world.remove_child(child)
if not original_ids.has(child.get_instance_id()):
child.queue_free()
for detached: Node2D in _surface_erase_detached:
if detached == null or not is_instance_valid(detached):
continue
if not original_ids.has(detached.get_instance_id()):
detached.queue_free()
var restore_order: Array[Dictionary] = _surface_erase_originals.duplicate()
restore_order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return int(a.get("index", 0)) < int(b.get("index", 0)))
for snapshot: Dictionary in restore_order:
var original: Node2D = snapshot.get("node") as Node2D
var parent: Node = snapshot.get("parent") as Node
if original == null or not is_instance_valid(original):
continue
if parent == null or not is_instance_valid(parent):
parent = _world
if original.get_parent() != null:
original.get_parent().remove_child(original)
parent.add_child(original)
parent.move_child(original, clampi(int(snapshot.get("index", 0)), 0, parent.get_child_count() - 1))
func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool:
if _single_active or members.is_empty() or kind not in [&"move", &"resize"]:
return false
@@ -591,8 +761,8 @@ func _commit_artwork(position: Vector2) -> void:
artwork.set_depth(_depth)
artwork.global_position = _snap_visual_center(artwork, position)
_world.add_child(artwork)
_placement_nodes.append(artwork)
placement_committed.emit(artwork)
document_change_requested.emit()
func _commit_rect_surface(first: Vector2, second: Vector2) -> void:
@@ -605,8 +775,8 @@ func _commit_rect_surface(first: Vector2, second: Vector2) -> void:
panel.configure(size, _wall_finish, _wall_tint)
panel.global_position = (first + second) * 0.5
_world.add_child(panel)
_placement_nodes.append(panel)
placement_committed.emit(panel)
document_change_requested.emit()
func _restore_rect_surface_ghost(world_position: Vector2) -> void:
@@ -620,23 +790,39 @@ func _is_rect_placement_tool() -> bool:
return _active_tool in [&"wall", &"background_ground"]
func _is_placement_tool() -> bool:
return _active_tool in [&"artwork", &"wall", &"background_ground"]
func _default_size_for_active_surface() -> Vector2:
return DEFAULT_BACKGROUND_SIZE if _active_tool == &"background_ground" else DEFAULT_PANEL_SIZE
return DEFAULT_PANEL_SIZE
func _is_surface_eraser() -> bool:
return _active_tool in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"]
func _erase_wall_region(world_rect: Rect2) -> int:
return _erase_surface_region(world_rect, &"wall")
func _erase_surface_region(world_rect: Rect2, surface_kind: StringName) -> int:
if _world == null or world_rect.size.x <= 0.0 or world_rect.size.y <= 0.0:
return 0
var changed_panels: int = 0
var target_scripts: Array[Script] = _surface_erase_target_scripts(surface_kind)
for child: Node in _world.get_children().duplicate():
if child.get_script() != WALL_PANEL:
if not target_scripts.has(child.get_script()):
continue
var wall: StageWallPanel = child as StageWallPanel
var panel_rect: Rect2 = wall.get_local_bounds()
var surface: StageRectSurface = child as StageRectSurface
if surface == null:
continue
var is_background_ground: bool = child.get_script() == BACKGROUND_GROUND
var panel_rect: Rect2 = surface.get_local_bounds()
var erase_corners: Array[Vector2] = [world_rect.position, Vector2(world_rect.end.x, world_rect.position.y), world_rect.end, Vector2(world_rect.position.x, world_rect.end.y)]
var local_erase := Rect2()
for corner_index: int in erase_corners.size():
var local_point: Vector2 = wall.to_local(erase_corners[corner_index])
var local_point: Vector2 = surface.to_local(erase_corners[corner_index])
if corner_index == 0:
local_erase = Rect2(local_point, Vector2.ZERO)
else:
@@ -647,27 +833,52 @@ func _erase_wall_region(world_rect: Rect2) -> int:
if cut.size.x <= 0.0 or cut.size.y <= 0.0:
continue
var fragments: Array[Rect2] = _wall_rect_fragments(panel_rect, cut)
var parent: Node = wall.get_parent()
var parent: Node = surface.get_parent()
if parent == null:
continue
var sibling_index: int = wall.get_index()
var source_transform: Transform2D = wall.transform
var source_motif_origin: Vector2 = wall.get_effective_motif_origin_world()
var sibling_index: int = surface.get_index()
var source_transform: Transform2D = surface.transform
var source_draw_origin: Vector2 = surface.get_surface_draw_origin_world()
if surface is StageWallPanel:
source_draw_origin = (surface as StageWallPanel).get_effective_motif_origin_world()
for fragment_rect: Rect2 in fragments:
var fragment := WALL_PANEL.new() as StageWallPanel
var fragment: StageRectSurface
if is_background_ground:
fragment = BACKGROUND_GROUND.new() as StageBackgroundGround
else:
fragment = WALL_PANEL.new() as StageWallPanel
fragment.transform = source_transform
fragment.position += source_transform.basis_xform(fragment_rect.get_center())
fragment.configure(fragment_rect.size, wall.finish_id, wall.tint)
fragment.configure(fragment_rect.size, surface.finish_id, surface.tint)
parent.add_child(fragment)
fragment.motif_origin_offset = source_motif_origin - fragment.global_position
if fragment is StageWallPanel:
(fragment as StageWallPanel).motif_origin_offset = source_draw_origin - fragment.global_position
elif fragment is StageBackgroundGround:
(fragment as StageBackgroundGround).motif_origin_offset = source_draw_origin - fragment.global_position
parent.move_child(fragment, sibling_index)
sibling_index += 1
parent.remove_child(wall)
wall.queue_free()
parent.remove_child(surface)
if _is_surface_eraser():
_surface_erase_detached.append(surface)
else:
surface.queue_free()
changed_panels += 1
return changed_panels
func _surface_erase_target_scripts(surface_kind: StringName) -> Array[Script]:
var target_scripts: Array[Script] = []
match surface_kind:
&"background_ground":
target_scripts.append(BACKGROUND_GROUND)
&"all":
target_scripts.append(WALL_PANEL)
target_scripts.append(BACKGROUND_GROUND)
_:
target_scripts.append(WALL_PANEL)
return target_scripts
func _wall_rect_fragments(panel_rect: Rect2, cut: Rect2) -> Array[Rect2]:
var fragments: Array[Rect2] = []
_add_wall_fragment(fragments, Rect2(panel_rect.position, Vector2(panel_rect.size.x, cut.position.y - panel_rect.position.y)))
@@ -7,12 +7,16 @@ signal change_prop_requested
signal capture_action_requested(action_id: StringName)
const MAX_BAR_WIDTH: float = 800.0
const SCENERY_CAPTURE_MAX_WIDTH: float = 900.0
const VIEWPORT_WIDTH_RATIO: float = 0.68
const HORIZONTAL_LAYOUT_MIN_WIDTH: float = 760.0
const ACTION_ROW_MIN_WIDTH: float = 480.0
const ACTION_BUTTON_WIDTH: float = 200.0
const DEFAULT_BUTTON_HEIGHT: float = 56.0
const NARROW_COMPACT_WIDTH: float = 244.0
const SCENERY_EDIT_MAX_WIDTH: float = 920.0
const SCENERY_EDIT_BUTTON_HEIGHT: float = 54.0
const SCENERY_CAPTURE_BUTTON_HEIGHT: float = 56.0
var _stage_theme: SandboxTheme
var _top_bar_height: float = 64.0
@@ -25,12 +29,28 @@ var _actions_row: HBoxContainer
var _actions_column: VBoxContainer
var _actions_parent: BoxContainer
var _prompt: Label
var _scenery_edit_layout: VBoxContainer
var _scenery_edit_prompt: Label
var _scenery_edit_count: Label
var _scenery_edit_count_badge: PanelContainer
var _scenery_selection_group: PanelContainer
var _scenery_edit_actions: HBoxContainer
var _scenery_selection_actions: HBoxContainer
var _scenery_select_several_button: Button
var _scenery_select_all_button: Button
var _scenery_move_group_button: Button
var _scenery_resize_group_button: Button
var _scenery_clear_selection_button: Button
var _scenery_erase_area_button: Button
var _scenery_done_button: Button
var _change_prop_button: Button
var _cancel_button: Button
var _custom_action_buttons: Array[Button] = []
var _has_horizontal_safe_bounds: bool = false
var _safe_left: float = 0.0
var _safe_right: float = 0.0
var _scenery_edit_active: bool = false
var _scenery_capture_active: bool = false
func configure(stage_theme: SandboxTheme, top_bar_height: float) -> void:
@@ -51,6 +71,7 @@ func _ready() -> void:
func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) -> void:
if _prompt == null:
return
_show_standard_capture_layout()
_clear_custom_actions()
if not prop_selected:
_prompt.text = "Choose a prop to use"
@@ -65,8 +86,17 @@ func set_capture_state(prop_selected: bool, prop_has_use_spots: bool = true) ->
func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
_set_capture_actions(prompt, actions, false)
func set_scenery_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
_set_capture_actions(prompt, actions, true)
func _set_capture_actions(prompt: String, actions: Array[Dictionary], scenery_capture_style: bool) -> void:
if _prompt == null:
return
_show_standard_capture_layout()
_prompt.text = prompt
_change_prop_button.visible = false
_cancel_button.visible = false
@@ -77,14 +107,75 @@ func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
continue
var button := Button.new()
button.name = "Action_%s" % String(action_id)
button.text = String(action.get("label", action_id))
var action_label: String = String(action.get("label", action_id))
var cue: Control = action.get("cue") as Control
button.text = "" if cue != null else action_label
button.tooltip_text = button.text
if cue != null:
button.tooltip_text = action_label
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 4)
margin.add_theme_constant_override("margin_right", 4)
margin.add_theme_constant_override("margin_top", 3)
margin.add_theme_constant_override("margin_bottom", 3)
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
var cue_layout := BoxContainer.new()
cue_layout.vertical = true
cue_layout.add_theme_constant_override("separation", 2)
cue_layout.alignment = BoxContainer.ALIGNMENT_CENTER
cue_layout.mouse_filter = Control.MOUSE_FILTER_IGNORE
cue.mouse_filter = Control.MOUSE_FILTER_IGNORE
cue_layout.add_child(cue)
var cue_label := Label.new()
cue_label.text = action_label
cue_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
cue_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
cue_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
cue_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
cue_layout.add_child(cue_label)
margin.add_child(cue_layout)
button.add_child(margin)
button.custom_minimum_size.x = ACTION_BUTTON_WIDTH
button.focus_mode = Control.FOCUS_ALL
button.set_meta("capture_primary", bool(action.get("primary", false)))
button.set_meta("capture_choice", bool(action.get("choice", false)))
button.set_meta("capture_selected", bool(action.get("selected", false)))
button.pressed.connect(func() -> void: capture_action_requested.emit(action_id))
_style_button(button, bool(action.get("primary", false)))
_custom_action_buttons.append(button)
_actions_parent.add_child(button)
_scenery_capture_active = scenery_capture_style
if _scenery_capture_active:
_set_layout(true, false)
visible = true
apply_theme()
_layout_capture_bar()
func set_scenery_edit_state(selected_count: int) -> void:
if _scenery_edit_layout == null:
return
_scenery_capture_active = false
_scenery_edit_active = true
_clear_custom_actions()
if _active_layout.get_parent() == self:
_active_layout.visible = false
remove_child(_active_layout)
_layout_stash.add_child(_active_layout)
if _scenery_edit_layout.get_parent() == _layout_stash:
_layout_stash.remove_child(_scenery_edit_layout)
add_child(_scenery_edit_layout)
_scenery_edit_layout.visible = true
_scenery_edit_prompt.text = "Tap scenery to select it"
_scenery_edit_count.text = "%d selected" % maxi(0, selected_count)
var has_group: bool = selected_count > 1
_scenery_select_several_button.visible = not has_group
_scenery_select_all_button.visible = not has_group
_scenery_move_group_button.visible = has_group
_scenery_resize_group_button.visible = has_group
_scenery_erase_area_button.visible = selected_count == 0
_scenery_clear_selection_button.visible = selected_count > 0
visible = true
apply_theme()
_layout_capture_bar()
@@ -104,11 +195,18 @@ func clear_horizontal_safe_bounds() -> void:
func hide_capture() -> void:
visible = false
_show_standard_capture_layout()
func apply_theme() -> void:
if not is_node_ready() or _prompt == null:
return
if _scenery_edit_active:
_apply_scenery_edit_theme()
return
if _scenery_capture_active:
_apply_scenery_capture_theme()
return
theme = _stage_theme.control_theme() if _stage_theme != null else null
var accent: Color = _color("mode_colors.direct", Color("#FFB300"))
var shell: StyleBoxFlat = StyleBoxFlat.new()
@@ -155,6 +253,197 @@ func apply_theme() -> void:
_layout_capture_bar()
func _apply_scenery_edit_theme() -> void:
theme = _stage_theme.control_theme() if _stage_theme != null else null
var edit_accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
var panel_style: StyleBoxFlat = StyleBoxFlat.new()
panel_style.bg_color = _color("stage_panels.shell.surface_color", Color("#202633FA"))
panel_style.border_color = Color("#667386")
panel_style.set_border_width_all(2)
panel_style.set_corner_radius_all(18)
panel_style.content_margin_left = 18.0
panel_style.content_margin_right = 18.0
panel_style.content_margin_top = 14.0
panel_style.content_margin_bottom = 14.0
panel_style.shadow_color = Color(0.0, 0.0, 0.0, 0.34)
panel_style.shadow_size = 8
panel_style.shadow_offset = Vector2(0.0, 4.0)
add_theme_stylebox_override("panel", panel_style)
var font: Font = _stage_theme.font() if _stage_theme != null else ThemeDB.fallback_font
if font != null:
_scenery_edit_prompt.add_theme_font_override("font", font)
_scenery_edit_count.add_theme_font_override("font", font)
for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_done_button]:
if font != null:
button.add_theme_font_override("font", font)
button.add_theme_font_size_override("font_size", 19)
button.custom_minimum_size.y = SCENERY_EDIT_BUTTON_HEIGHT
_style_scenery_edit_button(button, button == _scenery_done_button, false)
_scenery_edit_prompt.add_theme_font_size_override("font_size", 27)
_scenery_edit_prompt.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
_scenery_edit_count.add_theme_font_size_override("font_size", 17)
_scenery_edit_count.add_theme_color_override("font_color", Color("#F5F7FA"))
var badge_style: StyleBoxFlat = StyleBoxFlat.new()
badge_style.bg_color = edit_accent.darkened(0.78)
badge_style.border_color = edit_accent
badge_style.set_border_width_all(1)
badge_style.set_corner_radius_all(14)
badge_style.content_margin_left = 14.0
badge_style.content_margin_right = 14.0
badge_style.content_margin_top = 6.0
badge_style.content_margin_bottom = 6.0
_scenery_edit_count_badge.add_theme_stylebox_override("panel", badge_style)
var group_style: StyleBoxFlat = StyleBoxFlat.new()
group_style.bg_color = _color("stage_panels.action.surface_color", Color("#171B24"))
group_style.border_color = Color("#667386")
group_style.set_border_width_all(1)
group_style.set_corner_radius_all(14)
group_style.content_margin_left = 2.0
group_style.content_margin_right = 2.0
group_style.content_margin_top = 2.0
group_style.content_margin_bottom = 2.0
_scenery_selection_group.add_theme_stylebox_override("panel", group_style)
_scenery_edit_layout.add_theme_constant_override("separation", 12)
_scenery_selection_actions.add_theme_constant_override("separation", 0)
_style_scenery_selection_segment(_scenery_select_several_button, true, false)
_style_scenery_selection_segment(_scenery_select_all_button, false, true)
_layout_capture_bar()
func _apply_scenery_capture_theme() -> void:
theme = _stage_theme.control_theme() if _stage_theme != null else null
var panel_style: StyleBoxFlat = StyleBoxFlat.new()
panel_style.bg_color = _color("stage_panels.shell.surface_color", Color("#202633FA"))
panel_style.border_color = Color("#667386")
panel_style.set_border_width_all(2)
panel_style.set_corner_radius_all(18)
panel_style.content_margin_left = 18.0
panel_style.content_margin_right = 18.0
panel_style.content_margin_top = 14.0
panel_style.content_margin_bottom = 14.0
panel_style.shadow_color = Color(0.0, 0.0, 0.0, 0.34)
panel_style.shadow_size = 8
panel_style.shadow_offset = Vector2(0.0, 4.0)
add_theme_stylebox_override("panel", panel_style)
var font: Font = _stage_theme.font() if _stage_theme != null else ThemeDB.fallback_font
if font != null:
_prompt.add_theme_font_override("font", font)
_prompt.add_theme_font_size_override("font_size", 25)
_prompt.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
for button: Button in _custom_action_buttons:
if font != null:
button.add_theme_font_override("font", font)
button.add_theme_font_size_override("font_size", 19)
button.custom_minimum_size.y = SCENERY_EDIT_BUTTON_HEIGHT
if bool(button.get_meta("capture_choice", false)):
_style_scenery_depth_choice(button, bool(button.get_meta("capture_selected", false)))
var margin: MarginContainer = button.get_child(0) as MarginContainer if button.get_child_count() > 0 else null
var cue_stack: VBoxContainer = margin.get_child(0) as VBoxContainer if margin != null and margin.get_child_count() > 0 else null
if cue_stack != null and cue_stack.get_child_count() > 1:
var cue_label: Label = cue_stack.get_child(1) as Label
if font != null:
cue_label.add_theme_font_override("font", font)
cue_label.add_theme_font_size_override("font_size", 17)
cue_label.add_theme_color_override("font_color", Color("#F5F7FA"))
else:
_style_scenery_edit_button(button, bool(button.get_meta("capture_primary", false)), false)
_actions_row.add_theme_constant_override("separation", 12)
_layout_capture_bar()
func _style_scenery_depth_choice(button: Button, selected: bool) -> void:
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
var background: Color = _color("stage_panels.action.primary_color", Color("#087DBA")) if selected else _color("stage_panels.action.surface_color", Color("#171B24"))
var hover: Color = _color("stage_panels.action.primary_hover_color", Color("#1596D3")) if selected else _color("stage_panels.action.hover_surface", Color("#303A4B"))
var border: Color = accent if selected else Color("#667386")
var border_width: int = 2 if selected else 1
button.add_theme_stylebox_override("normal", _scenery_depth_choice_button_style(background, border, border_width))
button.add_theme_stylebox_override("hover", _scenery_depth_choice_button_style(hover, accent, 2))
button.add_theme_stylebox_override("pressed", _scenery_depth_choice_button_style(background, accent, 2))
button.add_theme_stylebox_override("hover_pressed", _scenery_depth_choice_button_style(hover, accent, 2))
button.add_theme_stylebox_override("focus", _scenery_depth_choice_button_style(background, accent, 2))
button.add_theme_color_override("font_color", Color("#F5F7FA"))
button.add_theme_color_override("font_hover_color", Color("#F5F7FA"))
button.add_theme_color_override("font_pressed_color", Color("#F5F7FA"))
button.add_theme_color_override("font_focus_color", Color("#F5F7FA"))
func _scenery_depth_choice_button_style(background: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style: StyleBoxFlat = _scenery_button_style(background, border, border_width)
style.content_margin_left = 6.0
style.content_margin_right = 6.0
style.content_margin_top = 4.0
style.content_margin_bottom = 4.0
return style
func _style_scenery_edit_button(button: Button, primary: bool, destructive: bool) -> void:
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
var danger: Color = _color("stage_panels.action.destructive_color", Color("#FF626B"))
var border: Color = danger if destructive else (accent if primary else Color("#667386"))
var background: Color = _color("stage_panels.action.primary_color", Color("#087DBA")) if primary else _color("stage_panels.action.surface_color", Color("#171B24"))
var hover_background: Color = _color("stage_panels.action.primary_hover_color", Color("#1596D3")) if primary else _color("stage_panels.action.hover_surface", Color("#303A4B"))
var pressed_background: Color = _color("stage_panels.action.primary_pressed_color", Color("#066B9F")) if primary else _color("stage_panels.action.selected_surface", Color("#12394A"))
var text_color: Color = Color("#F5F7FA")
button.add_theme_stylebox_override("normal", _scenery_button_style(background, border, 2 if primary or destructive else 1))
button.add_theme_stylebox_override("hover", _scenery_button_style(hover_background, border.lightened(0.12), 2))
button.add_theme_stylebox_override("pressed", _scenery_button_style(pressed_background, border, 2))
button.add_theme_stylebox_override("hover_pressed", _scenery_button_style(hover_background, border.lightened(0.12), 2))
button.add_theme_stylebox_override("focus", _scenery_button_style(background, accent, 2))
button.add_theme_color_override("font_color", text_color)
button.add_theme_color_override("font_hover_color", text_color)
button.add_theme_color_override("font_pressed_color", text_color)
button.add_theme_color_override("font_focus_color", text_color)
func _style_scenery_selection_segment(button: Button, is_first: bool, is_last: bool) -> void:
var base: Color = _color("stage_panels.action.surface_color", Color("#171B24"))
var hover: Color = _color("stage_panels.action.hover_surface", Color("#303A4B"))
var pressed: Color = _color("stage_panels.action.selected_surface", Color("#12394A"))
var divider: Color = Color("#667386")
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
button.add_theme_stylebox_override("normal", _scenery_segment_style(base, divider, is_first, is_last))
button.add_theme_stylebox_override("hover", _scenery_segment_style(hover, divider, is_first, is_last))
button.add_theme_stylebox_override("pressed", _scenery_segment_style(pressed, divider, is_first, is_last))
button.add_theme_stylebox_override("hover_pressed", _scenery_segment_style(hover, divider, is_first, is_last))
button.add_theme_stylebox_override("focus", _scenery_segment_style(base, accent, is_first, is_last, true))
func _scenery_segment_style(background: Color, border: Color, is_first: bool, is_last: bool, is_focused: bool = false) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
if is_focused:
style.set_border_width_all(2)
else:
style.border_width_left = 0 if is_first else 1
style.border_width_right = 0
style.border_width_top = 0
style.border_width_bottom = 0
style.corner_radius_top_left = 12 if is_first else 0
style.corner_radius_bottom_left = 12 if is_first else 0
style.corner_radius_top_right = 12 if is_last else 0
style.corner_radius_bottom_right = 12 if is_last else 0
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 6.0
style.content_margin_bottom = 6.0
return style
func _scenery_button_style(background: Color, border: Color, border_width: int) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(border_width)
style.set_corner_radius_all(14)
style.content_margin_left = 12.0
style.content_margin_right = 12.0
style.content_margin_top = 6.0
style.content_margin_bottom = 6.0
return style
func _build_controls() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
_layout_stash = Node.new()
@@ -207,6 +496,100 @@ func _build_controls() -> void:
_cancel_button.focus_mode = Control.FOCUS_ALL
_cancel_button.pressed.connect(func() -> void: cancel_requested.emit())
_actions_row.add_child(_cancel_button)
_build_scenery_edit_controls()
func _build_scenery_edit_controls() -> void:
_scenery_edit_layout = VBoxContainer.new()
_scenery_edit_layout.name = "SceneryEditLayout"
_scenery_edit_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_scenery_edit_layout.visible = false
_layout_stash.add_child(_scenery_edit_layout)
var header_row: HBoxContainer = HBoxContainer.new()
header_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_row.add_theme_constant_override("separation", 12)
_scenery_edit_layout.add_child(header_row)
_scenery_edit_prompt = Label.new()
_scenery_edit_prompt.name = "SceneryPrompt"
_scenery_edit_prompt.text = "Tap scenery to select it"
_scenery_edit_prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_scenery_edit_prompt.max_lines_visible = 2
_scenery_edit_prompt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_scenery_edit_prompt.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_scenery_edit_prompt.mouse_filter = Control.MOUSE_FILTER_IGNORE
header_row.add_child(_scenery_edit_prompt)
_scenery_edit_count_badge = PanelContainer.new()
_scenery_edit_count_badge.name = "ScenerySelectionCountBadge"
header_row.add_child(_scenery_edit_count_badge)
_scenery_edit_count = Label.new()
_scenery_edit_count.name = "ScenerySelectionCount"
_scenery_edit_count.text = "0 selected"
_scenery_edit_count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_scenery_edit_count_badge.add_child(_scenery_edit_count)
_scenery_clear_selection_button = _make_scenery_action_button("ClearSelection", "Clear selection", &"clear_selection")
_scenery_clear_selection_button.custom_minimum_size.x = 150.0
_scenery_clear_selection_button.visible = false
header_row.add_child(_scenery_clear_selection_button)
_scenery_edit_actions = HBoxContainer.new()
_scenery_edit_actions.name = "SceneryEditActions"
_scenery_edit_actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_scenery_edit_actions.alignment = BoxContainer.ALIGNMENT_CENTER
_scenery_edit_actions.add_theme_constant_override("separation", 10)
_scenery_edit_layout.add_child(_scenery_edit_actions)
_scenery_selection_group = PanelContainer.new()
_scenery_selection_group.name = "SelectionActionsGroup"
_scenery_selection_group.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_scenery_edit_actions.add_child(_scenery_selection_group)
_scenery_selection_actions = HBoxContainer.new()
_scenery_selection_actions.add_theme_constant_override("separation", 6)
_scenery_selection_group.add_child(_scenery_selection_actions)
_scenery_select_several_button = _make_scenery_action_button("SelectSeveral", "Select several", &"select_several")
_scenery_select_several_button.custom_minimum_size.x = 158.0
_scenery_selection_actions.add_child(_scenery_select_several_button)
_scenery_select_all_button = _make_scenery_action_button("SelectAll", "Select all", &"select_all")
_scenery_select_all_button.custom_minimum_size.x = 148.0
_scenery_selection_actions.add_child(_scenery_select_all_button)
_scenery_move_group_button = _make_scenery_action_button("MoveGroup", "Move together", &"move_group")
_scenery_move_group_button.custom_minimum_size.x = 168.0
_scenery_move_group_button.visible = false
_scenery_selection_actions.add_child(_scenery_move_group_button)
_scenery_resize_group_button = _make_scenery_action_button("ResizeGroup", "Resize together", &"resize_group")
_scenery_resize_group_button.custom_minimum_size.x = 182.0
_scenery_resize_group_button.visible = false
_scenery_selection_actions.add_child(_scenery_resize_group_button)
_scenery_erase_area_button = _make_scenery_action_button("EraseArea", "Erase Area", &"erase_area")
_scenery_erase_area_button.custom_minimum_size.x = 152.0
_scenery_edit_actions.add_child(_scenery_erase_area_button)
_scenery_done_button = _make_scenery_action_button("DoneEditing", "Done editing", &"done_edit")
_scenery_done_button.custom_minimum_size.x = 160.0
_scenery_edit_actions.add_child(_scenery_done_button)
func _make_scenery_action_button(button_name: String, label: String, action_id: StringName) -> Button:
var button: Button = Button.new()
button.name = button_name
button.text = label
button.tooltip_text = label
button.focus_mode = Control.FOCUS_ALL
button.custom_minimum_size = Vector2(144.0, SCENERY_EDIT_BUTTON_HEIGHT)
button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
button.pressed.connect(func() -> void: capture_action_requested.emit(action_id))
return button
func _show_standard_capture_layout() -> void:
_scenery_edit_active = false
_scenery_capture_active = false
if _scenery_edit_layout != null and _scenery_edit_layout.get_parent() == self:
_scenery_edit_layout.visible = false
remove_child(_scenery_edit_layout)
_layout_stash.add_child(_scenery_edit_layout)
if _active_layout != null and _active_layout.get_parent() == _layout_stash:
_layout_stash.remove_child(_active_layout)
add_child(_active_layout)
_active_layout.visible = true
if is_node_ready():
apply_theme()
func _clear_custom_actions() -> void:
@@ -255,6 +638,12 @@ func _layout_capture_bar() -> void:
if not is_node_ready() or _prompt == null:
return
var viewport_size: Vector2 = get_viewport_rect().size
if _scenery_edit_active:
_layout_scenery_edit_capture_bar(viewport_size)
return
if _scenery_capture_active:
_layout_scenery_capture_bar(viewport_size)
return
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var safe_left: float = inset
var safe_right: float = viewport_size.x - inset
@@ -281,6 +670,123 @@ func _layout_capture_bar() -> void:
call_deferred("_fit_height_to_content")
func _layout_scenery_edit_capture_bar(viewport_size: Vector2) -> void:
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var safe_left: float = inset
var safe_right: float = viewport_size.x - inset
if _has_horizontal_safe_bounds:
safe_left = clampf(_safe_left, inset, maxf(inset, viewport_size.x - inset))
safe_right = clampf(_safe_right, safe_left + 1.0, maxf(safe_left + 1.0, viewport_size.x - inset))
var available_width: float = maxf(1.0, safe_right - safe_left)
var width: float = minf(SCENERY_EDIT_MAX_WIDTH, available_width)
var narrow: bool = width < 700.0
custom_minimum_size.x = width
size.x = width
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
clip_contents = false
position = Vector2(safe_left + (available_width - width) * 0.5, _top_bar_height + 56.0)
_scenery_edit_prompt.add_theme_font_size_override("font_size", 27 if width >= 700.0 else (21 if width >= 520.0 else 18))
if narrow and _scenery_erase_area_button.get_parent() == _scenery_edit_actions:
_scenery_edit_actions.remove_child(_scenery_erase_area_button)
_scenery_edit_layout.get_child(0).add_child(_scenery_erase_area_button)
elif not narrow and _scenery_erase_area_button.get_parent() != _scenery_edit_actions:
var header_row: HBoxContainer = _scenery_edit_layout.get_child(0) as HBoxContainer
header_row.remove_child(_scenery_erase_area_button)
_scenery_edit_actions.add_child(_scenery_erase_area_button)
_scenery_edit_actions.move_child(_scenery_erase_area_button, 1)
_scenery_selection_actions.add_theme_constant_override("separation", 0)
_scenery_select_several_button.custom_minimum_size.x = 134.0 if narrow else 158.0
_scenery_select_all_button.custom_minimum_size.x = 124.0 if narrow else 148.0
_scenery_move_group_button.custom_minimum_size.x = 146.0 if narrow else 168.0
_scenery_resize_group_button.custom_minimum_size.x = 158.0 if narrow else 182.0
_scenery_done_button.custom_minimum_size.x = 142.0 if narrow else 160.0
_scenery_erase_area_button.custom_minimum_size.x = 136.0 if narrow else 152.0
_scenery_clear_selection_button.custom_minimum_size.x = 128.0 if narrow else 150.0
for button: Button in [_scenery_select_several_button, _scenery_select_all_button, _scenery_move_group_button, _scenery_resize_group_button, _scenery_clear_selection_button, _scenery_erase_area_button, _scenery_done_button]:
button.add_theme_font_size_override("font_size", 19 if width >= 700.0 else 16)
button.custom_minimum_size.y = 52.0 if narrow else SCENERY_EDIT_BUTTON_HEIGHT
button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
_scenery_edit_actions.add_theme_constant_override("separation", 8 if narrow else 10)
call_deferred("_fit_scenery_edit_height")
func _layout_scenery_capture_bar(viewport_size: Vector2) -> void:
var inset: float = float(_integer("stage_panels.shell.edge_inset", 16, 12, 48))
var available_width: float = maxf(1.0, viewport_size.x - inset * 2.0)
var width: float = minf(SCENERY_CAPTURE_MAX_WIDTH, available_width)
var button_count: int = maxi(1, _custom_action_buttons.size())
var has_depth_choices: bool = button_count >= 5
var horizontal_depth_cards: bool = has_depth_choices and width >= 700.0
var compact_spacing: float = 8.0 if has_depth_choices and width < 760.0 else 12.0
var minimum_row_width: float = 40.0 + 5.0 * 84.0 + float(button_count - 1) * compact_spacing
var stack_actions: bool = width < ACTION_ROW_MIN_WIDTH or (has_depth_choices and width < minimum_row_width)
custom_minimum_size.x = width
size.x = width
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
clip_contents = false
position = Vector2((viewport_size.x - width) * 0.5, _top_bar_height + 56.0)
_prompt.add_theme_font_size_override("font_size", 25 if width >= 520.0 else 19)
_set_layout(true, stack_actions)
_actions_row.alignment = BoxContainer.ALIGNMENT_END
var available_button_width: float = maxf(1.0, width - 40.0 - compact_spacing * float(button_count - 1))
var depth_button_width: float = available_button_width
var secondary_button_width: float = available_button_width
if stack_actions:
depth_button_width = maxf(120.0, width - 72.0)
secondary_button_width = depth_button_width
elif has_depth_choices:
secondary_button_width = clampf(available_button_width * 0.16, 84.0, 132.0)
depth_button_width = maxf(84.0, (available_button_width - secondary_button_width * 2.0) / 3.0)
else:
depth_button_width = minf(ACTION_BUTTON_WIDTH, maxf(120.0, available_button_width / float(button_count)))
secondary_button_width = depth_button_width
var cue_size := Vector2(56.0, 42.0)
var choice_button_height: float = 84.0
if width >= 560.0:
cue_size = Vector2(64.0, 48.0)
choice_button_height = 88.0
if width >= 700.0:
cue_size = Vector2(66.0, 50.0)
choice_button_height = 100.0
if width >= 800.0:
cue_size = Vector2(96.0, 72.0)
choice_button_height = 112.0
for button: Button in _custom_action_buttons:
var is_depth_choice: bool = bool(button.get_meta("capture_choice", false))
button.custom_minimum_size = Vector2(depth_button_width if is_depth_choice else secondary_button_width, choice_button_height if has_depth_choices else SCENERY_CAPTURE_BUTTON_HEIGHT)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL if stack_actions else Control.SIZE_SHRINK_CENTER
if is_depth_choice:
var margin: MarginContainer = button.get_child(0) as MarginContainer if button.get_child_count() > 0 else null
var cue_layout: BoxContainer = margin.get_child(0) as BoxContainer if margin != null and margin.get_child_count() > 0 else null
if cue_layout != null and cue_layout.get_child_count() > 1:
cue_layout.vertical = not horizontal_depth_cards
cue_layout.add_theme_constant_override("separation", 8 if horizontal_depth_cards else 2)
var cue: Control = cue_layout.get_child(0) as Control
if cue != null:
cue.custom_minimum_size = cue_size
var cue_label: Label = cue_layout.get_child(1) as Label
if cue_label != null:
cue_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT if horizontal_depth_cards else HORIZONTAL_ALIGNMENT_CENTER
cue_label.add_theme_font_size_override("font_size", 20 if width >= 800.0 else (18 if width >= 620.0 else 17))
button.add_theme_font_size_override("font_size", 17 if width < 760.0 else 19)
_actions_row.add_theme_constant_override("separation", int(compact_spacing))
call_deferred("_fit_scenery_capture_height_after_layout")
func _fit_scenery_capture_height_after_layout() -> void:
await get_tree().process_frame
_fit_height_to_content()
func _fit_scenery_edit_height() -> void:
if not is_inside_tree() or not visible or not _scenery_edit_active:
return
var panel_style: StyleBox = get_theme_stylebox("panel")
var panel_height: float = panel_style.get_minimum_size().y if panel_style != null else 0.0
custom_minimum_size.y = _scenery_edit_layout.get_combined_minimum_size().y + panel_height
size.y = custom_minimum_size.y
func _set_layout(compact: bool, stack_actions: bool, wrapped: bool = false) -> void:
var target_layout: Container = _wrapped_layout if wrapped else (_vertical_layout if compact else _horizontal_layout)
var target_actions: BoxContainer = _actions_column if stack_actions else _actions_row
@@ -348,12 +854,13 @@ func _apply_compact_action_labels(compact: bool) -> void:
"cancel_group": "Cancel",
"done_tool": "Done",
"cancel_tool": "Cancel",
"cancel_single": "Cancel",
"depth_far": "Far",
"depth_near": "Near",
"depth_floor": "Floor",
"depth_far": "Back",
"depth_near": "Middle",
"depth_floor": "Front",
}
for button: Button in _custom_action_buttons:
if bool(button.get_meta("capture_choice", false)):
continue
var action_id: String = button.name.trim_prefix("Action_")
var full_label: String = button.tooltip_text
button.text = String(compact_labels.get(action_id, full_label)) if compact else full_label