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:
@@ -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):
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
class_name StageDepthChoicePreview
|
||||
extends Control
|
||||
## Small fixed-size diagram showing how scenery depth changes draw order.
|
||||
|
||||
const DEPTH_FAR := &"far"
|
||||
const DEPTH_NEAR := &"near"
|
||||
const DEPTH_FLOOR := &"floor"
|
||||
|
||||
var depth_id: StringName = DEPTH_NEAR
|
||||
var artwork_texture: Texture2D
|
||||
|
||||
|
||||
func configure(depth: StringName, texture: Texture2D, preview_size: Vector2) -> void:
|
||||
depth_id = depth if depth in [DEPTH_FAR, DEPTH_NEAR, DEPTH_FLOOR] else DEPTH_NEAR
|
||||
artwork_texture = texture
|
||||
custom_minimum_size = preview_size
|
||||
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
clip_contents = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_artwork_texture(texture: Texture2D) -> void:
|
||||
artwork_texture = texture
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var bounds := Rect2(Vector2.ZERO, size)
|
||||
draw_rect(bounds, Color("#B9D9D9"), true)
|
||||
_draw_hills()
|
||||
if depth_id != DEPTH_FAR:
|
||||
_draw_near_scenery()
|
||||
if depth_id == DEPTH_FLOOR:
|
||||
_draw_ground_tiles()
|
||||
_draw_artwork()
|
||||
if depth_id == DEPTH_FAR:
|
||||
_draw_near_scenery()
|
||||
if depth_id != DEPTH_FLOOR:
|
||||
_draw_ground_tiles()
|
||||
draw_rect(bounds, Color("#566273"), false, 1.0)
|
||||
|
||||
|
||||
func _draw_hills() -> void:
|
||||
var points := PackedVector2Array([
|
||||
Vector2(0.0, size.y * 0.68),
|
||||
Vector2(0.0, size.y * 0.52),
|
||||
Vector2(size.x * 0.18, size.y * 0.39),
|
||||
Vector2(size.x * 0.38, size.y * 0.54),
|
||||
Vector2(size.x * 0.62, size.y * 0.36),
|
||||
Vector2(size.x * 0.84, size.y * 0.49),
|
||||
Vector2(size.x, size.y * 0.41),
|
||||
Vector2(size.x, size.y * 0.68),
|
||||
])
|
||||
draw_colored_polygon(points, Color("#79AFA4"))
|
||||
|
||||
|
||||
func _draw_near_scenery() -> void:
|
||||
var canopy := Color("#277447")
|
||||
var trunk := Color("#84502B")
|
||||
var canopy_center := Vector2(size.x * 0.49, size.y * 0.56)
|
||||
var radius := maxf(4.0, size.y * 0.25)
|
||||
draw_rect(Rect2(Vector2(size.x * 0.49 - 2.0, size.y * 0.46), Vector2(4.0, size.y * 0.23)), trunk, true)
|
||||
draw_circle(canopy_center, radius, canopy)
|
||||
draw_circle(canopy_center + Vector2(-radius * 0.52, radius * 0.08), radius * 0.62, Color("#48A84E"))
|
||||
draw_circle(canopy_center + Vector2(radius * 0.48, radius * 0.02), radius * 0.55, Color("#36934A"))
|
||||
|
||||
|
||||
func _draw_ground_tiles() -> void:
|
||||
var top: float = size.y * 0.64
|
||||
var tile_height: float = size.y - top
|
||||
var tile_count: int = 5
|
||||
var tile_width: float = size.x / float(tile_count)
|
||||
for index: int in tile_count:
|
||||
var tile := Rect2(Vector2(float(index) * tile_width, top), Vector2(tile_width + 0.5, tile_height))
|
||||
draw_rect(tile, Color("#87512C"), true)
|
||||
draw_rect(tile, Color("#A96C3B"), false, 1.0)
|
||||
draw_line(Vector2(float(index) * tile_width + 2.0, top + tile_height * 0.58), Vector2(float(index + 1) * tile_width - 2.0, top + tile_height * 0.58), Color("#C9894B"), 1.0)
|
||||
draw_rect(Rect2(Vector2(0.0, top - 3.0), Vector2(size.x, 6.0)), Color("#62D34D"), true)
|
||||
draw_line(Vector2(0.0, top - 3.0), Vector2(size.x, top - 3.0), Color("#D5F27B"), 1.0)
|
||||
|
||||
|
||||
func _draw_artwork() -> void:
|
||||
if artwork_texture == null:
|
||||
var center := Vector2(size.x * 0.49, size.y * 0.59)
|
||||
draw_circle(center, maxf(3.0, minf(size.x, size.y) * 0.20), Color("#F4F1E8"))
|
||||
draw_circle(center, maxf(3.0, minf(size.x, size.y) * 0.20), Color("#58616D"), false, 1.0)
|
||||
return
|
||||
var texture_size := Vector2(artwork_texture.get_size())
|
||||
if texture_size.x <= 0.0 or texture_size.y <= 0.0:
|
||||
return
|
||||
var available := Vector2(size.x * 0.84, size.y * 0.84)
|
||||
var scale_factor: float = minf(available.x / texture_size.x, available.y / texture_size.y)
|
||||
var target_size := texture_size * scale_factor
|
||||
var target := Rect2(Vector2((size.x - target_size.x) * 0.5, size.y * 0.59 - target_size.y * 0.5), target_size)
|
||||
draw_texture_rect(artwork_texture, target, false)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cc5hn0jge7758
|
||||
@@ -116,6 +116,52 @@ func style_action_button(button: Button, prominent: bool = false) -> void:
|
||||
_style_action_button(button, prominent)
|
||||
|
||||
|
||||
func style_primary_action_button(button: Button) -> void:
|
||||
if button == null:
|
||||
return
|
||||
var primary := _color("stage_panels.action.primary_color", Color("#087DBA"))
|
||||
var hover := _color("stage_panels.action.primary_hover_color", Color("#1596D3"))
|
||||
var pressed := _color("stage_panels.action.primary_pressed_color", Color("#066B9F"))
|
||||
var accent := _color("mode_colors.edit", Color("#22C6FF"))
|
||||
button.add_theme_stylebox_override("normal", _make_style(primary, accent.lightened(0.15), 2, 10, 6))
|
||||
button.add_theme_stylebox_override("hover", _make_style(hover, accent, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("pressed", _make_style(pressed, accent, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("hover_pressed", _make_style(pressed.lightened(0.05), accent, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), _color("global.border_color", Color("#FFFFFF33")), 1, 10, 6))
|
||||
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 10, 6))
|
||||
button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
button.add_theme_color_override("font_color", Color.WHITE)
|
||||
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
button.add_theme_color_override("font_pressed_color", Color.WHITE)
|
||||
if _stage_theme != null:
|
||||
var ui_font := _stage_theme.font()
|
||||
if ui_font != null:
|
||||
button.add_theme_font_override("font", ui_font)
|
||||
|
||||
|
||||
func style_destructive_button(button: Button) -> void:
|
||||
if button == null:
|
||||
return
|
||||
var danger := _color("stage_panels.action.destructive_color", Color("#FF626B"))
|
||||
var neutral := _color("stage_panels.action.surface_color", Color("#171B24"))
|
||||
var hover := _color("stage_panels.action.destructive_hover_surface", Color("#3B252C"))
|
||||
var pressed := _color("stage_panels.action.destructive_pressed_surface", Color("#4A252D"))
|
||||
var border := _color("global.border_color", Color("#FFFFFF33"))
|
||||
var accent := _color("mode_colors.edit", Color("#22C6FF"))
|
||||
button.add_theme_stylebox_override("normal", _make_style(neutral, danger, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("hover", _make_style(hover, danger, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("pressed", _make_style(pressed, danger, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("hover_pressed", _make_style(pressed.lightened(0.06), danger, 2, 10, 6))
|
||||
button.add_theme_stylebox_override("disabled", _make_style(_color("stage_panels.choice.disabled_surface", Color("#252B38")), border, 1, 10, 6))
|
||||
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2, 10, 6))
|
||||
button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
button.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||
if _stage_theme != null:
|
||||
var ui_font := _stage_theme.font()
|
||||
if ui_font != null:
|
||||
button.add_theme_font_override("font", ui_font)
|
||||
|
||||
|
||||
func _build_shell() -> void:
|
||||
if _body != null:
|
||||
return
|
||||
@@ -145,11 +191,12 @@ func _build_shell() -> void:
|
||||
_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_header.add_child(_title)
|
||||
_close_button = Button.new()
|
||||
_close_button.text = "✕ Close"
|
||||
_close_button.text = "Close"
|
||||
_close_button.custom_minimum_size = Vector2(
|
||||
float(_integer("stage_panels.action.close_width", 88, 72, 180)),
|
||||
float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
|
||||
)
|
||||
_close_button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
_close_button.focus_mode = Control.FOCUS_ALL
|
||||
_close_button.pressed.connect(func() -> void: close_requested.emit())
|
||||
_header.add_child(_close_button)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class_name StagePanelChoiceGroup
|
||||
extends VBoxContainer
|
||||
extends BoxContainer
|
||||
## Exclusive, full-width choices with optional decorative cues.
|
||||
|
||||
signal choice_selected(id: StringName)
|
||||
@@ -8,20 +8,43 @@ var _stage_theme: SandboxTheme = null
|
||||
var _group := ButtonGroup.new()
|
||||
var _buttons: Dictionary = {}
|
||||
var _selected_id: StringName = &""
|
||||
var _vertical_layout: bool = true
|
||||
var _minimum_height_override: float = -1.0
|
||||
var _font_size_override: int = -1
|
||||
var _corner_radius_override: int = -1
|
||||
var _border_width_override: int = -1
|
||||
var _selected_surface_override: Color = Color.TRANSPARENT
|
||||
var _has_selected_surface_override: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
vertical = _vertical_layout
|
||||
_group.allow_unpress = true
|
||||
add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32))
|
||||
|
||||
|
||||
func add_choice(id: StringName, caption: String, cue: Control = null) -> Button:
|
||||
func configure_layout(vertical_layout: bool, minimum_height: float = -1.0, font_size: int = -1, corner_radius: int = -1, border_width: int = -1, selected_surface: Color = Color.TRANSPARENT) -> void:
|
||||
_vertical_layout = vertical_layout
|
||||
vertical = vertical_layout
|
||||
_minimum_height_override = minimum_height
|
||||
_font_size_override = font_size
|
||||
_corner_radius_override = corner_radius
|
||||
_border_width_override = border_width
|
||||
_selected_surface_override = selected_surface
|
||||
_has_selected_surface_override = selected_surface.a > 0.0
|
||||
for entry: Dictionary in _buttons.values():
|
||||
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label, entry.get("detail") as Label)
|
||||
|
||||
|
||||
func add_choice(id: StringName, caption: String, cue: Control = null, detail: String = "") -> Button:
|
||||
var button := Button.new()
|
||||
button.name = "Choice_%s" % String(id)
|
||||
button.toggle_mode = true
|
||||
button.button_group = _group
|
||||
button.custom_minimum_size.y = float(_integer("stage_panels.choice.minimum_height", 56, 44, 112))
|
||||
button.custom_minimum_size.y = _minimum_height_override if _minimum_height_override > 0.0 else float(_integer("stage_panels.choice.minimum_height", 56, 44, 112))
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if not _vertical_layout:
|
||||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.text = ""
|
||||
var margin := MarginContainer.new()
|
||||
@@ -42,16 +65,31 @@ func add_choice(id: StringName, caption: String, cue: Control = null) -> Button:
|
||||
var label := Label.new()
|
||||
label.text = caption
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT if not detail.is_empty() else HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
row.add_child(label)
|
||||
var detail_label: Label
|
||||
if detail.is_empty():
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
row.add_child(label)
|
||||
else:
|
||||
var text_stack := VBoxContainer.new()
|
||||
text_stack.add_theme_constant_override("separation", 0)
|
||||
text_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
text_stack.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
text_stack.add_child(label)
|
||||
detail_label = Label.new()
|
||||
detail_label.text = detail
|
||||
detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
detail_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
detail_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
text_stack.add_child(detail_label)
|
||||
row.add_child(text_stack)
|
||||
button.pressed.connect(_on_choice_pressed.bind(id, button))
|
||||
add_child(button)
|
||||
_buttons[id] = {"button": button, "label": label}
|
||||
_buttons[id] = {"button": button, "label": label, "detail": detail_label}
|
||||
if _stage_theme != null:
|
||||
_apply_choice_theme(button, label)
|
||||
_apply_choice_theme(button, label, detail_label)
|
||||
return button
|
||||
|
||||
|
||||
@@ -75,7 +113,7 @@ func apply_theme(stage_theme: SandboxTheme) -> void:
|
||||
_stage_theme = stage_theme
|
||||
add_theme_constant_override("separation", _integer("stage_panels.choice.row_gap", 6, 0, 32))
|
||||
for entry: Dictionary in _buttons.values():
|
||||
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label)
|
||||
_apply_choice_theme(entry["button"] as Button, entry["label"] as Label, entry.get("detail") as Label)
|
||||
|
||||
|
||||
func _on_choice_pressed(id: StringName, button: Button) -> void:
|
||||
@@ -87,26 +125,40 @@ func _on_choice_pressed(id: StringName, button: Button) -> void:
|
||||
choice_selected.emit(id)
|
||||
|
||||
|
||||
func _apply_choice_theme(button: Button, label: Label) -> void:
|
||||
func _apply_choice_theme(button: Button, label: Label, detail: Label = null) -> void:
|
||||
if _stage_theme == null:
|
||||
return
|
||||
var font := _stage_theme.font()
|
||||
if font != null:
|
||||
label.add_theme_font_override("font", font)
|
||||
label.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
if not _vertical_layout:
|
||||
var strong_font := FontVariation.new()
|
||||
strong_font.base_font = font
|
||||
strong_font.variation_embolden = 0.25
|
||||
label.add_theme_font_override("font", strong_font)
|
||||
else:
|
||||
label.add_theme_font_override("font", font)
|
||||
var font_size: int = _font_size_override if _font_size_override > 0 else _integer("stage_panels.type.body_size", 20, 14, 40)
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
label.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||
if detail != null:
|
||||
if font != null:
|
||||
detail.add_theme_font_override("font", font)
|
||||
detail.add_theme_font_size_override("font_size", maxi(12, font_size - 4))
|
||||
detail.add_theme_color_override("font_color", _color("stage_panels.type.muted_text_color", Color("#AAB2C0")))
|
||||
var neutral := _color("stage_panels.choice.neutral_surface", Color("#171B24"))
|
||||
var hover := _color("stage_panels.choice.hover_surface", Color("#303A4B"))
|
||||
var selected := _color("stage_panels.choice.selected_surface", Color("#12394A"))
|
||||
var selected := _selected_surface_override if _has_selected_surface_override else _color("stage_panels.choice.selected_surface", Color("#12394A"))
|
||||
var disabled := _color("stage_panels.choice.disabled_surface", Color("#252B38"))
|
||||
var border := _color("stage_panels.choice.border_color", Color("#FFFFFF33"))
|
||||
var accent := _color("mode_colors.edit", Color("#22C6FF"))
|
||||
button.add_theme_stylebox_override("normal", _make_style(neutral, border, 1))
|
||||
button.add_theme_stylebox_override("hover", _make_style(hover, border, 1))
|
||||
button.add_theme_stylebox_override("pressed", _make_style(selected, accent, 2))
|
||||
button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, 2))
|
||||
button.add_theme_stylebox_override("disabled", _make_style(disabled, border, 1))
|
||||
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, 2))
|
||||
var border_width: int = _border_width_override if _border_width_override >= 0 else 1
|
||||
var selected_border_width: int = border_width + 1 if _border_width_override >= 0 else 2
|
||||
button.add_theme_stylebox_override("normal", _make_style(neutral, border, border_width))
|
||||
button.add_theme_stylebox_override("hover", _make_style(hover, border, border_width))
|
||||
button.add_theme_stylebox_override("pressed", _make_style(selected, accent, selected_border_width))
|
||||
button.add_theme_stylebox_override("hover_pressed", _make_style(selected.lightened(0.06), accent, selected_border_width))
|
||||
button.add_theme_stylebox_override("disabled", _make_style(disabled, border, border_width))
|
||||
button.add_theme_stylebox_override("focus", _make_style(Color(0, 0, 0, 0), accent, selected_border_width))
|
||||
|
||||
|
||||
func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat:
|
||||
@@ -114,7 +166,8 @@ func _make_style(bg: Color, border: Color, width: int) -> StyleBoxFlat:
|
||||
style.bg_color = bg
|
||||
style.border_color = border
|
||||
style.set_border_width_all(width)
|
||||
style.set_corner_radius_all(_integer("stage_panels.shell.corner_radius", 10, 0, 32))
|
||||
var radius: int = _corner_radius_override if _corner_radius_override >= 0 else _integer("stage_panels.shell.corner_radius", 10, 0, 32)
|
||||
style.set_corner_radius_all(radius)
|
||||
return style
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ var _current_value: Label
|
||||
var _recent_empty_label: Label
|
||||
var _recent_swatches: ColorSwatchSelector
|
||||
var _preset_swatches: ColorSwatchSelector
|
||||
var _more_button: Button
|
||||
var _compact_mode: bool = false
|
||||
|
||||
|
||||
@@ -54,60 +53,107 @@ func set_recent_colors(colors: Array[Color]) -> void:
|
||||
|
||||
|
||||
func focus_custom_button() -> void:
|
||||
if _more_button != null:
|
||||
_more_button.grab_focus()
|
||||
if _current_chip != null:
|
||||
_current_chip.grab_focus()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_built = true
|
||||
_apply_compact_layout()
|
||||
var current_heading: Label = _make_heading("Color")
|
||||
add_child(current_heading)
|
||||
var current_parent: Control = self
|
||||
var recent_parent: Control = self
|
||||
var presets_parent: Control = self
|
||||
if _compact_mode:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", _integer("popups.asset_selector.color_group_gap", 8, 4, 32))
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_child(row)
|
||||
current_parent = _make_compact_section(132.0)
|
||||
row.add_child(current_parent)
|
||||
row.add_child(_make_compact_separator())
|
||||
recent_parent = _make_compact_section(92.0)
|
||||
row.add_child(recent_parent)
|
||||
row.add_child(_make_compact_separator())
|
||||
presets_parent = _make_compact_section(180.0)
|
||||
presets_parent.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(presets_parent)
|
||||
_build_current_color_section(current_parent)
|
||||
_build_recent_color_section(recent_parent)
|
||||
_build_palette_section(presets_parent)
|
||||
if _stage_theme != null:
|
||||
var font: Font = _stage_theme.font()
|
||||
if font != null:
|
||||
if _compact_mode:
|
||||
var strong_font := FontVariation.new()
|
||||
strong_font.base_font = font
|
||||
strong_font.variation_embolden = 0.2
|
||||
_current_value.add_theme_font_override("font", strong_font)
|
||||
else:
|
||||
_current_value.add_theme_font_override("font", font)
|
||||
_current_value.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
|
||||
|
||||
func _build_current_color_section(parent: Control) -> void:
|
||||
parent.add_child(_make_heading("Color"))
|
||||
var current_row := HBoxContainer.new()
|
||||
current_row.add_theme_constant_override("separation", _compact_gap())
|
||||
add_child(current_row)
|
||||
parent.add_child(current_row)
|
||||
_current_chip = Button.new()
|
||||
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48, 44, 96))
|
||||
if _compact_mode:
|
||||
swatch_size = minf(swatch_size, 44.0)
|
||||
swatch_size = maxf(swatch_size, 52.0)
|
||||
_current_chip.custom_minimum_size = Vector2(swatch_size, swatch_size)
|
||||
_current_chip.focus_mode = Control.FOCUS_NONE
|
||||
_current_chip.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_current_chip.focus_mode = Control.FOCUS_ALL
|
||||
_current_chip.tooltip_text = "Choose a custom color"
|
||||
_current_chip.pressed.connect(func() -> void: custom_color_requested.emit())
|
||||
current_row.add_child(_current_chip)
|
||||
_current_value = Label.new()
|
||||
_current_value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_current_value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
current_row.add_child(_current_value)
|
||||
add_child(_make_heading("Recently used"))
|
||||
|
||||
|
||||
func _build_recent_color_section(parent: Control) -> void:
|
||||
parent.add_child(_make_heading("Recently used"))
|
||||
_recent_empty_label = Label.new()
|
||||
_recent_empty_label.text = "Colors you use will appear here"
|
||||
_recent_empty_label.text = "No colors yet" if _compact_mode else "Colors you use will appear here"
|
||||
_recent_empty_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_recent_empty_label.add_theme_color_override("font_color", _color("global.muted_text_color", Color("#AAB2C0")))
|
||||
add_child(_recent_empty_label)
|
||||
if _compact_mode:
|
||||
_recent_empty_label.add_theme_font_size_override("font_size", _integer("stage_panels.type.helper_size", 16, 12, 32))
|
||||
parent.add_child(_recent_empty_label)
|
||||
_recent_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
|
||||
_recent_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if _compact_mode:
|
||||
_recent_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8)
|
||||
_recent_swatches.color_selected.connect(_on_swatch_selected)
|
||||
add_child(_recent_swatches)
|
||||
add_child(_make_heading("Colors"))
|
||||
parent.add_child(_recent_swatches)
|
||||
|
||||
|
||||
func _build_palette_section(parent: Control) -> void:
|
||||
parent.add_child(_make_heading("Colors"))
|
||||
_preset_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
|
||||
_preset_swatches.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if _compact_mode:
|
||||
_preset_swatches.set_swatch_layout_override(_integer("popups.asset_selector.compact_swatch_size", 42, 28, 52), 8)
|
||||
_preset_swatches.color_selected.connect(_on_swatch_selected)
|
||||
add_child(_preset_swatches)
|
||||
_more_button = Button.new()
|
||||
_more_button.text = "More colors…"
|
||||
var action_height: float = float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
|
||||
_more_button.custom_minimum_size.y = minf(action_height, 44.0) if _compact_mode else action_height
|
||||
_more_button.focus_mode = Control.FOCUS_ALL
|
||||
_more_button.pressed.connect(func() -> void: custom_color_requested.emit())
|
||||
add_child(_more_button)
|
||||
if _stage_theme != null:
|
||||
var font: Font = _stage_theme.font()
|
||||
if font != null:
|
||||
_current_value.add_theme_font_override("font", font)
|
||||
_more_button.add_theme_font_override("font", font)
|
||||
_current_value.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
_more_button.add_theme_font_size_override("font_size", _integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
_more_button.add_theme_color_override("font_color", _color("global.text_color", Color("#F5F7FA")))
|
||||
_more_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action"))
|
||||
_more_button.add_theme_stylebox_override("hover", _stage_theme.stylebox("stage_panels.action").duplicate())
|
||||
parent.add_child(_preset_swatches)
|
||||
|
||||
|
||||
func _make_compact_section(minimum_width: float) -> VBoxContainer:
|
||||
var section := VBoxContainer.new()
|
||||
section.custom_minimum_size.x = minimum_width
|
||||
section.add_theme_constant_override("separation", 4)
|
||||
return section
|
||||
|
||||
|
||||
func _make_compact_separator() -> ColorRect:
|
||||
var separator := ColorRect.new()
|
||||
separator.color = _color("popups.asset_selector.separator_color", Color("#AAB2C0A0"))
|
||||
separator.custom_minimum_size = Vector2(2.0, 64.0)
|
||||
separator.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
separator.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return separator
|
||||
|
||||
|
||||
func _refresh_visuals() -> void:
|
||||
@@ -124,11 +170,16 @@ func _refresh_current_color() -> void:
|
||||
_current_value.text = "#%s" % _selected.to_html(false).to_upper()
|
||||
var chip_style := StyleBoxFlat.new()
|
||||
chip_style.bg_color = _selected
|
||||
chip_style.border_color = _color("stage_panels.choice.border_color", Color("#FFFFFFAA"))
|
||||
chip_style.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 2, 0, 8))
|
||||
chip_style.border_color = _color("mode_colors.edit", Color("#22C6FF"))
|
||||
chip_style.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 3, 1, 8))
|
||||
chip_style.set_corner_radius_all(_integer("stage_surface_colors.swatch_corner_radius", 8, 0, 24))
|
||||
_current_chip.add_theme_stylebox_override("normal", chip_style)
|
||||
_current_chip.add_theme_stylebox_override("hover", chip_style.duplicate())
|
||||
_current_chip.add_theme_stylebox_override("pressed", chip_style.duplicate())
|
||||
var focus_style: StyleBoxFlat = chip_style.duplicate() as StyleBoxFlat
|
||||
focus_style.border_color = Color.WHITE
|
||||
focus_style.set_border_width_all(5)
|
||||
_current_chip.add_theme_stylebox_override("focus", focus_style)
|
||||
|
||||
|
||||
func _on_swatch_selected(color: Color) -> void:
|
||||
@@ -143,18 +194,23 @@ func _make_heading(text_value: String) -> Label:
|
||||
var heading := Label.new()
|
||||
heading.text = text_value
|
||||
var heading_size: int = _integer("stage_panels.type.section_size", 20, 12, 40)
|
||||
if _compact_mode:
|
||||
heading_size = mini(heading_size, 18)
|
||||
heading.add_theme_font_size_override("font_size", heading_size)
|
||||
heading.add_theme_color_override("font_color", _color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||
if _stage_theme != null and _stage_theme.font() != null:
|
||||
heading.add_theme_font_override("font", _stage_theme.font())
|
||||
var font: Font = _stage_theme.font()
|
||||
if _compact_mode:
|
||||
var strong_font := FontVariation.new()
|
||||
strong_font.base_font = font
|
||||
strong_font.variation_embolden = 0.2
|
||||
heading.add_theme_font_override("font", strong_font)
|
||||
else:
|
||||
heading.add_theme_font_override("font", font)
|
||||
return heading
|
||||
|
||||
|
||||
func _apply_compact_layout() -> void:
|
||||
var section_gap: int = _integer("stage_surface_colors.section_gap", 8, 0, 28)
|
||||
add_theme_constant_override("separation", mini(section_gap, 4) if _compact_mode else section_gap)
|
||||
add_theme_constant_override("separation", section_gap)
|
||||
|
||||
|
||||
func _compact_gap() -> int:
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
class_name StageSurfaceColorDialog
|
||||
extends Window
|
||||
## Bounded custom-color dialog with a live Stage surface preview.
|
||||
extends PopupPanel
|
||||
## Rounded, reusable Stage color popup with the shared surface picker view.
|
||||
|
||||
signal preview_color_changed(color: Color)
|
||||
signal color_confirmed(color: Color)
|
||||
signal cancelled
|
||||
|
||||
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
|
||||
const SURFACE_COLOR_PICKER_VIEW := preload("res://scripts/shared/stage_ui/stage_surface_color_picker_view.gd")
|
||||
const COMPACT_LAYOUT_MAX_HEIGHT := 640
|
||||
|
||||
var _surface_kind: StringName = &"wall"
|
||||
var _finish_id: StringName = &"finish_wall_paint"
|
||||
var _initial_color: Color = Color.WHITE
|
||||
var _stage_theme: SandboxTheme
|
||||
var _color_picker: ColorPicker
|
||||
var _surface_preview: StageSurfacePreview
|
||||
var _margin: MarginContainer
|
||||
var _layout: VBoxContainer
|
||||
var _heading: Label
|
||||
var _view_scroll: ScrollContainer
|
||||
var _picker_view: StageSurfaceColorPickerView
|
||||
var _actions: HBoxContainer
|
||||
var _cancel_button: Button
|
||||
var _use_button: Button
|
||||
var _built: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
exclusive = true
|
||||
_build()
|
||||
close_requested.connect(_cancel)
|
||||
get_tree().root.size_changed.connect(_on_root_size_changed)
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
|
||||
_surface_kind = surface_kind
|
||||
_finish_id = finish
|
||||
@@ -24,26 +39,20 @@ func configure(surface_kind: StringName, finish: StringName, color: Color, stage
|
||||
_stage_theme = stage_theme
|
||||
if not _built:
|
||||
_build()
|
||||
_color_picker.color = color
|
||||
_update_preview(color)
|
||||
title = "Choose a color"
|
||||
exclusive = true
|
||||
transient = true
|
||||
theme = stage_theme.control_theme() if stage_theme != null else null
|
||||
if _picker_view != null:
|
||||
_picker_view.configure(_surface_kind, _finish_id, color, _stage_theme)
|
||||
theme = _stage_theme.control_theme() if _stage_theme != null else null
|
||||
_apply_visual_style()
|
||||
|
||||
|
||||
func open_dialog() -> void:
|
||||
var max_size: Vector2i = _usable_window_size()
|
||||
var preferred_size := Vector2i(560, 640)
|
||||
var raw_size: Variant = _stage_theme.value("popups.dialog.surface_color_size", [560, 640]) if _stage_theme != null else [560, 640]
|
||||
if raw_size is Array and (raw_size as Array).size() == 2:
|
||||
preferred_size = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1]))
|
||||
var dialog_size := Vector2i(mini(preferred_size.x, max_size.x), mini(preferred_size.y, max_size.y))
|
||||
dialog_size.x = maxi(1, dialog_size.x)
|
||||
dialog_size.y = maxi(1, dialog_size.y)
|
||||
size = dialog_size
|
||||
popup_centered(dialog_size)
|
||||
_color_picker.grab_focus()
|
||||
if not _built or _picker_view == null:
|
||||
return
|
||||
var popup_size: Vector2i = _bounded_dialog_size()
|
||||
_apply_responsive_layout(popup_size)
|
||||
size = popup_size
|
||||
popup_centered(popup_size)
|
||||
call_deferred("_focus_picker")
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
@@ -53,98 +62,168 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
if _built:
|
||||
return
|
||||
_built = true
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_margin = MarginContainer.new()
|
||||
_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(_margin)
|
||||
_layout = VBoxContainer.new()
|
||||
_margin.add_child(_layout)
|
||||
_heading = Label.new()
|
||||
_heading.name = "ChooseColorHeading"
|
||||
_heading.text = "Choose a color"
|
||||
_heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_layout.add_child(_heading)
|
||||
_view_scroll = ScrollContainer.new()
|
||||
_view_scroll.name = "PickerScroll"
|
||||
_view_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
_view_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_view_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_layout.add_child(_view_scroll)
|
||||
_picker_view = SURFACE_COLOR_PICKER_VIEW.new() as StageSurfaceColorPickerView
|
||||
_picker_view.name = "SurfaceColorPickerView"
|
||||
_picker_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_picker_view.preview_color_changed.connect(_on_preview_color_changed)
|
||||
_view_scroll.add_child(_picker_view)
|
||||
_actions = HBoxContainer.new()
|
||||
_actions.name = "Actions"
|
||||
_actions.alignment = BoxContainer.ALIGNMENT_END
|
||||
_actions.add_theme_constant_override("separation", 8)
|
||||
_layout.add_child(_actions)
|
||||
_cancel_button = Button.new()
|
||||
_cancel_button.name = "CancelButton"
|
||||
_cancel_button.text = "Cancel"
|
||||
_cancel_button.focus_mode = Control.FOCUS_ALL
|
||||
_cancel_button.pressed.connect(_cancel)
|
||||
_actions.add_child(_cancel_button)
|
||||
_use_button = Button.new()
|
||||
_use_button.name = "UseColorButton"
|
||||
_use_button.text = "Use color"
|
||||
_use_button.focus_mode = Control.FOCUS_ALL
|
||||
_use_button.pressed.connect(_confirm_color)
|
||||
_actions.add_child(_use_button)
|
||||
|
||||
|
||||
func _apply_visual_style() -> void:
|
||||
if _stage_theme == null or not _built:
|
||||
return
|
||||
theme = _stage_theme.control_theme()
|
||||
var radius: int = _integer("popups.asset_selector.corner_radius", 16, 8, 32)
|
||||
var shell: StyleBoxFlat = _stage_theme.stylebox("popups.asset_selector")
|
||||
shell.set_corner_radius_all(radius)
|
||||
shell.content_margin_left = 16.0
|
||||
shell.content_margin_right = 16.0
|
||||
shell.content_margin_top = 14.0
|
||||
shell.content_margin_bottom = 14.0
|
||||
add_theme_stylebox_override("panel", shell)
|
||||
var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA"))
|
||||
var font: Font = _stage_theme.font()
|
||||
if font != null:
|
||||
_heading.add_theme_font_override("font", font)
|
||||
_cancel_button.add_theme_font_override("font", font)
|
||||
var strong_font := FontVariation.new()
|
||||
strong_font.base_font = font
|
||||
strong_font.variation_embolden = 0.55
|
||||
_use_button.add_theme_font_override("font", strong_font)
|
||||
_heading.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_title_size", 30, 22, 48))
|
||||
_heading.add_theme_color_override("font_color", text_color)
|
||||
_apply_action_button(_cancel_button, false)
|
||||
_apply_action_button(_use_button, true)
|
||||
|
||||
|
||||
func _apply_action_button(button: Button, primary: bool) -> void:
|
||||
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
|
||||
var text_color: Color = _color("stage_panels.type.text_color", Color("#F5F7FA"))
|
||||
var neutral: Color = _color("stage_panels.choice.neutral_surface", Color("#171B24"))
|
||||
var hover: Color = _color("stage_panels.choice.hover_surface", Color("#303A4B"))
|
||||
var selected: Color = _color("stage_panels.choice.selected_surface", Color("#12394A"))
|
||||
var border: Color = _color("stage_panels.choice.border_color", Color("#FFFFFF33"))
|
||||
var blue: Color = _color("popups.asset_selector.action_color", Color("#087DBA"))
|
||||
var radius: int = _integer("popups.asset_selector.corner_radius", 16, 8, 32)
|
||||
var border_width: int = _integer("popups.asset_selector.button_border_width", 2, 1, 5)
|
||||
var normal_color: Color = blue if primary else neutral
|
||||
var hover_color: Color = blue.lightened(0.12) if primary else hover
|
||||
var pressed_color: Color = blue.darkened(0.12) if primary else selected
|
||||
var edge: Color = accent.lightened(0.15) if primary else border
|
||||
button.add_theme_stylebox_override("normal", _make_button_style(normal_color, edge, border_width, radius))
|
||||
button.add_theme_stylebox_override("hover", _make_button_style(hover_color, edge, border_width + 1, radius))
|
||||
button.add_theme_stylebox_override("pressed", _make_button_style(pressed_color, accent, border_width + 1, radius))
|
||||
button.add_theme_stylebox_override("hover_pressed", _make_button_style(pressed_color.lightened(0.05), accent, border_width + 1, radius))
|
||||
button.add_theme_stylebox_override("focus", _make_button_style(Color(0, 0, 0, 0), accent, border_width + 1, radius))
|
||||
button.add_theme_color_override("font_color", Color.WHITE if primary else text_color)
|
||||
button.add_theme_color_override("font_hover_color", Color.WHITE if primary else text_color)
|
||||
button.add_theme_color_override("font_pressed_color", Color.WHITE if primary else text_color)
|
||||
button.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_button_font_size", 20, 14, 40))
|
||||
button.custom_minimum_size.y = float(_integer("popups.dialog.action_min_target_size", 48, 44, 96))
|
||||
|
||||
|
||||
func _apply_responsive_layout(popup_size: Vector2i) -> void:
|
||||
if not _built:
|
||||
return
|
||||
var compact: bool = popup_size.y < COMPACT_LAYOUT_MAX_HEIGHT
|
||||
var margin_size: int = 8 if compact else 12
|
||||
for side: String in ["left", "right", "top", "bottom"]:
|
||||
margin.add_theme_constant_override("margin_" + side, 12)
|
||||
add_child(margin)
|
||||
var layout := VBoxContainer.new()
|
||||
layout.add_theme_constant_override("separation", 8)
|
||||
margin.add_child(layout)
|
||||
var preview_heading := Label.new()
|
||||
preview_heading.text = "Preview"
|
||||
layout.add_child(preview_heading)
|
||||
_surface_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
|
||||
var preview_size := Vector2(420.0, 140.0)
|
||||
var raw_preview: Variant = _stage_theme.value("popups.dialog.surface_color_preview_size", [420, 140]) if _stage_theme != null else [420, 140]
|
||||
if raw_preview is Array and (raw_preview as Array).size() == 2:
|
||||
preview_size = Vector2(float((raw_preview as Array)[0]), float((raw_preview as Array)[1]))
|
||||
preview_size.x = minf(preview_size.x, maxf(1.0, float(_usable_window_size().x - 56)))
|
||||
_surface_preview.custom_minimum_size = preview_size
|
||||
_surface_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_surface_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
layout.add_child(_surface_preview)
|
||||
var picker_scroll := ScrollContainer.new()
|
||||
picker_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
picker_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
picker_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
layout.add_child(picker_scroll)
|
||||
_color_picker = ColorPicker.new()
|
||||
_color_picker.name = "ColorPickerControl"
|
||||
var picker_min_size := Vector2(320.0, 340.0)
|
||||
var raw_picker_size: Variant = _stage_theme.value("popups.dialog.surface_color_picker_min_size", [320, 340]) if _stage_theme != null else [320, 340]
|
||||
if raw_picker_size is Array and (raw_picker_size as Array).size() == 2:
|
||||
picker_min_size = Vector2(float((raw_picker_size as Array)[0]), float((raw_picker_size as Array)[1]))
|
||||
picker_min_size.x = minf(picker_min_size.x, maxf(1.0, float(_usable_window_size().x - 56)))
|
||||
_color_picker.custom_minimum_size = picker_min_size
|
||||
_color_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_color_picker.focus_mode = Control.FOCUS_ALL
|
||||
_color_picker.color_changed.connect(_on_picker_color_changed)
|
||||
picker_scroll.add_child(_color_picker)
|
||||
var actions := HBoxContainer.new()
|
||||
actions.alignment = BoxContainer.ALIGNMENT_END
|
||||
actions.add_theme_constant_override("separation", 8)
|
||||
layout.add_child(actions)
|
||||
var use_button := Button.new()
|
||||
use_button.text = "Use color"
|
||||
var action_width: float = maxf(44.0, minf(120.0, (float(_usable_window_size().x) - 40.0) * 0.5))
|
||||
use_button.custom_minimum_size = Vector2(action_width, float(_integer("popups.dialog.action_min_target_size", 48, 44, 96)))
|
||||
use_button.focus_mode = Control.FOCUS_ALL
|
||||
use_button.pressed.connect(_confirm_color)
|
||||
actions.add_child(use_button)
|
||||
var cancel_button := Button.new()
|
||||
cancel_button.text = "✕ Cancel"
|
||||
cancel_button.custom_minimum_size = Vector2(action_width, float(_integer("popups.dialog.action_min_target_size", 48, 44, 96)))
|
||||
cancel_button.focus_mode = Control.FOCUS_ALL
|
||||
cancel_button.pressed.connect(_cancel)
|
||||
actions.add_child(cancel_button)
|
||||
close_requested.connect(_cancel)
|
||||
if _stage_theme != null:
|
||||
var font: Font = _stage_theme.font()
|
||||
if font != null:
|
||||
preview_heading.add_theme_font_override("font", font)
|
||||
use_button.add_theme_font_override("font", font)
|
||||
cancel_button.add_theme_font_override("font", font)
|
||||
preview_heading.add_theme_font_size_override("font_size", _stage_theme.integer("stage_panels.type.section_size", 20, 12, 40))
|
||||
use_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action"))
|
||||
cancel_button.add_theme_stylebox_override("normal", _stage_theme.stylebox("stage_panels.action"))
|
||||
use_button.add_theme_color_override("font_color", _stage_theme.color("global.text_color", Color("#F5F7FA")))
|
||||
cancel_button.add_theme_color_override("font_color", _stage_theme.color("global.text_color", Color("#F5F7FA")))
|
||||
_margin.add_theme_constant_override("margin_" + side, margin_size)
|
||||
_layout.add_theme_constant_override("separation", 4 if compact else 8)
|
||||
_heading.add_theme_font_size_override("font_size", _integer("popups.asset_selector.setup_title_size", 30, 22, 48))
|
||||
var content_width: float = maxf(1.0, float(popup_size.x - margin_size * 2))
|
||||
_picker_view.set_available_size(Vector2(content_width, float(popup_size.y)))
|
||||
_apply_action_layout(content_width)
|
||||
|
||||
|
||||
func _on_picker_color_changed(color: Color) -> void:
|
||||
_update_preview(color)
|
||||
func _apply_action_layout(content_width: float) -> void:
|
||||
if _actions == null or _cancel_button == null or _use_button == null:
|
||||
return
|
||||
var stack_buttons: bool = content_width < 344.0
|
||||
_actions.vertical = stack_buttons
|
||||
_actions.alignment = BoxContainer.ALIGNMENT_END
|
||||
var action_height: float = float(_integer("popups.dialog.action_min_target_size", 48, 44, 96))
|
||||
var button_width: float = content_width if stack_buttons else maxf(112.0, (content_width - 8.0) * 0.5)
|
||||
_cancel_button.custom_minimum_size = Vector2(button_width, action_height)
|
||||
_use_button.custom_minimum_size = Vector2(button_width, action_height)
|
||||
|
||||
|
||||
func _on_preview_color_changed(color: Color) -> void:
|
||||
preview_color_changed.emit(color)
|
||||
|
||||
|
||||
func _update_preview(color: Color) -> void:
|
||||
if _surface_preview != null:
|
||||
_surface_preview.configure(_surface_kind, _finish_id, color)
|
||||
|
||||
|
||||
func _confirm_color() -> void:
|
||||
var confirmed_color: Color = _color_picker.color
|
||||
if _picker_view == null:
|
||||
return
|
||||
var confirmed_color: Color = _picker_view.get_color()
|
||||
hide()
|
||||
color_confirmed.emit(confirmed_color)
|
||||
|
||||
|
||||
func _cancel() -> void:
|
||||
_update_preview(_initial_color)
|
||||
if _picker_view != null:
|
||||
_picker_view.set_color_no_signal(_initial_color)
|
||||
hide()
|
||||
cancelled.emit()
|
||||
|
||||
|
||||
func _focus_picker() -> void:
|
||||
if visible and _picker_view != null:
|
||||
_picker_view.focus_picker()
|
||||
|
||||
|
||||
func _on_root_size_changed() -> void:
|
||||
if visible:
|
||||
open_dialog()
|
||||
|
||||
|
||||
func _bounded_dialog_size() -> Vector2i:
|
||||
var max_size: Vector2i = _usable_window_size()
|
||||
var preferred_size := Vector2i(560, 640)
|
||||
var raw_size: Variant = _stage_theme.value("popups.dialog.surface_color_size", [560, 640]) if _stage_theme != null else [560, 640]
|
||||
if raw_size is Array and (raw_size as Array).size() == 2:
|
||||
preferred_size = Vector2i(int((raw_size as Array)[0]), int((raw_size as Array)[1]))
|
||||
return Vector2i(maxi(1, mini(preferred_size.x, max_size.x)), maxi(1, mini(preferred_size.y, max_size.y)))
|
||||
|
||||
|
||||
func _usable_window_size() -> Vector2i:
|
||||
var available: Vector2i = Vector2i(get_tree().root.size)
|
||||
var usable: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen())
|
||||
@@ -156,5 +235,22 @@ func _usable_window_size() -> Vector2i:
|
||||
return available
|
||||
|
||||
|
||||
func _make_button_style(background: Color, border: Color, border_width: int, radius: int) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = background
|
||||
style.border_color = border
|
||||
style.set_border_width_all(border_width)
|
||||
style.set_corner_radius_all(radius)
|
||||
style.content_margin_left = 12.0
|
||||
style.content_margin_right = 12.0
|
||||
style.content_margin_top = 8.0
|
||||
style.content_margin_bottom = 8.0
|
||||
return style
|
||||
|
||||
|
||||
func _integer(path: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||
return fallback if _stage_theme == null else _stage_theme.integer(path, fallback, minimum, maximum)
|
||||
|
||||
|
||||
func _color(path: String, fallback: Color) -> Color:
|
||||
return fallback if _stage_theme == null else _stage_theme.color(path, fallback)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
class_name StageSurfaceColorPickerView
|
||||
extends VBoxContainer
|
||||
## Inline color picker content for the scenery chooser.
|
||||
|
||||
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
|
||||
const COMPACT_LAYOUT_MAX_HEIGHT := 640.0
|
||||
const COMPACT_PREVIEW_HEIGHT := 64.0
|
||||
const COMPACT_PICKER_MIN_HEIGHT := 260.0
|
||||
const COMPACT_SV_HEIGHT := 144
|
||||
const REGULAR_SV_HEIGHT := 224
|
||||
|
||||
signal preview_color_changed(color: Color)
|
||||
|
||||
var _surface_kind: StringName = &"wall"
|
||||
var _finish_id: StringName = &"finish_wall_paint"
|
||||
var _stage_theme: SandboxTheme
|
||||
var _color_picker: ColorPicker
|
||||
var _surface_preview: StageSurfacePreview
|
||||
var _preview_heading: Label
|
||||
var _preferred_preview_size: Vector2 = Vector2(420.0, 140.0)
|
||||
var _preferred_preview_height: float = 140.0
|
||||
var _preferred_picker_min_size: Vector2 = Vector2(320.0, 340.0)
|
||||
var _built: bool = false
|
||||
var _suppress_color_signal: bool = false
|
||||
|
||||
|
||||
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
|
||||
_surface_kind = surface_kind
|
||||
_finish_id = finish
|
||||
_stage_theme = stage_theme
|
||||
if not _built:
|
||||
_build()
|
||||
set_color_no_signal(color)
|
||||
theme = stage_theme.control_theme() if stage_theme != null else null
|
||||
_apply_preview_heading_theme()
|
||||
|
||||
|
||||
func get_color() -> Color:
|
||||
return _color_picker.color if _color_picker != null else Color.WHITE
|
||||
|
||||
|
||||
func set_color_no_signal(color: Color) -> void:
|
||||
if _color_picker == null:
|
||||
return
|
||||
_suppress_color_signal = true
|
||||
_color_picker.color = color
|
||||
_suppress_color_signal = false
|
||||
_update_preview(color)
|
||||
|
||||
|
||||
func set_available_size(available_size: Vector2) -> void:
|
||||
if not _built:
|
||||
return
|
||||
var compact: bool = available_size.y < COMPACT_LAYOUT_MAX_HEIGHT
|
||||
var preview_size: Vector2 = _surface_preview.custom_minimum_size
|
||||
preview_size.x = minf(_preferred_preview_size.x, maxf(1.0, available_size.x - 56.0))
|
||||
preview_size.y = minf(_preferred_preview_height, COMPACT_PREVIEW_HEIGHT) if compact else _preferred_preview_height
|
||||
_surface_preview.custom_minimum_size = preview_size
|
||||
var picker_size: Vector2 = _color_picker.custom_minimum_size
|
||||
picker_size.x = minf(_preferred_picker_min_size.x, maxf(1.0, available_size.x - 56.0))
|
||||
picker_size.y = minf(_preferred_picker_min_size.y, COMPACT_PICKER_MIN_HEIGHT) if compact else _preferred_picker_min_size.y
|
||||
_color_picker.custom_minimum_size = picker_size
|
||||
_color_picker.add_theme_constant_override("sv_height", COMPACT_SV_HEIGHT if compact else REGULAR_SV_HEIGHT)
|
||||
|
||||
|
||||
func focus_picker() -> void:
|
||||
if _color_picker != null:
|
||||
_color_picker.grab_focus()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_built = true
|
||||
add_theme_constant_override("separation", 8)
|
||||
_surface_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
|
||||
_preview_heading = Label.new()
|
||||
_preview_heading.name = "PreviewHeading"
|
||||
_preview_heading.text = "Preview"
|
||||
_preview_heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_preview_heading)
|
||||
var preview_size := _preferred_preview_size
|
||||
var raw_preview: Variant = _stage_theme.value("popups.dialog.surface_color_preview_size", [420, 140]) if _stage_theme != null else [420, 140]
|
||||
if raw_preview is Array and (raw_preview as Array).size() == 2:
|
||||
preview_size = Vector2(float((raw_preview as Array)[0]), float((raw_preview as Array)[1]))
|
||||
_preferred_preview_size = preview_size
|
||||
_preferred_preview_height = preview_size.y
|
||||
_surface_preview.custom_minimum_size = preview_size
|
||||
_surface_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_surface_preview.set_rounded_frame(
|
||||
float(_stage_theme.integer("popups.asset_selector.surface_corner_radius", 14, 4, 32)) if _stage_theme != null else 14.0,
|
||||
_stage_theme.color("popups.style_defaults.bg_color", Color("#202633FA")) if _stage_theme != null else Color("#202633FA")
|
||||
)
|
||||
add_child(_surface_preview)
|
||||
_color_picker = ColorPicker.new()
|
||||
_color_picker.name = "ColorPickerControl"
|
||||
var picker_size := _preferred_picker_min_size
|
||||
var raw_picker_size: Variant = _stage_theme.value("popups.dialog.surface_color_picker_min_size", [320, 340]) if _stage_theme != null else [320, 340]
|
||||
if raw_picker_size is Array and (raw_picker_size as Array).size() == 2:
|
||||
picker_size = Vector2(float((raw_picker_size as Array)[0]), float((raw_picker_size as Array)[1]))
|
||||
_preferred_picker_min_size = picker_size
|
||||
_color_picker.custom_minimum_size = picker_size
|
||||
_color_picker.presets_visible = false
|
||||
_color_picker.sampler_visible = false
|
||||
_color_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_color_picker.focus_mode = Control.FOCUS_ALL
|
||||
_color_picker.color_changed.connect(_on_picker_color_changed)
|
||||
add_child(_color_picker)
|
||||
_apply_preview_heading_theme()
|
||||
|
||||
|
||||
func _apply_preview_heading_theme() -> void:
|
||||
if _preview_heading == null:
|
||||
return
|
||||
if _stage_theme == null:
|
||||
return
|
||||
_preview_heading.add_theme_font_size_override("font_size", _stage_theme.integer("stage_panels.type.section_size", 20, 12, 40))
|
||||
_preview_heading.add_theme_color_override("font_color", _stage_theme.color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||
var font: Font = _stage_theme.font()
|
||||
if font != null:
|
||||
_preview_heading.add_theme_font_override("font", font)
|
||||
|
||||
|
||||
func _on_picker_color_changed(color: Color) -> void:
|
||||
_update_preview(color)
|
||||
if not _suppress_color_signal:
|
||||
preview_color_changed.emit(color)
|
||||
|
||||
|
||||
func _update_preview(color: Color) -> void:
|
||||
if _surface_preview != null:
|
||||
_surface_preview.configure(_surface_kind, _finish_id, color)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbvatqk4a287y
|
||||
+623
-125
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,15 @@ var _group: ButtonGroup = ButtonGroup.new()
|
||||
var _buttons: Array[Button] = []
|
||||
var _stage_theme: SandboxTheme
|
||||
var _colors: Array[Color] = []
|
||||
var _swatch_size_override: float = -1.0
|
||||
var _swatch_gap_override: int = -1
|
||||
|
||||
|
||||
func set_swatch_layout_override(size: float, gap: int) -> void:
|
||||
_swatch_size_override = size
|
||||
_swatch_gap_override = gap
|
||||
if is_inside_tree() and not _colors.is_empty():
|
||||
configure(_colors, _selected, _stage_theme)
|
||||
|
||||
|
||||
func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> void:
|
||||
@@ -25,8 +34,9 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) ->
|
||||
for child: Node in get_children():
|
||||
remove_child(child)
|
||||
child.queue_free()
|
||||
add_theme_constant_override("h_separation", _integer("stage_surface_colors.swatch_gap", 8))
|
||||
add_theme_constant_override("v_separation", _integer("stage_surface_colors.swatch_gap", 8))
|
||||
var gap: int = _swatch_gap_override if _swatch_gap_override >= 0 else _integer("stage_surface_colors.swatch_gap", 8)
|
||||
add_theme_constant_override("h_separation", gap)
|
||||
add_theme_constant_override("v_separation", gap)
|
||||
var seen: Dictionary = {}
|
||||
for color: Color in _colors:
|
||||
var color_key: String = color.to_html(true).to_upper()
|
||||
@@ -34,7 +44,7 @@ func configure(colors: Array[Color], selected: Color, theme: Variant = null) ->
|
||||
continue
|
||||
seen[color_key] = true
|
||||
var button := Button.new()
|
||||
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48))
|
||||
var swatch_size: float = _swatch_size_override if _swatch_size_override > 0.0 else float(_integer("stage_surface_colors.swatch_size", 48))
|
||||
button.custom_minimum_size = Vector2(swatch_size, swatch_size)
|
||||
button.toggle_mode = true
|
||||
button.button_group = _group
|
||||
|
||||
@@ -4,6 +4,29 @@ extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION: String = "2.0"
|
||||
const DEFAULT_PATH: String = "res://sandbox_theme.json"
|
||||
const CONFIRMATION_DEFAULTS: Dictionary = {
|
||||
"scenery_erase_size": [520, 208],
|
||||
"padding": 18,
|
||||
"body_gap": 10,
|
||||
"footer_gap": 12,
|
||||
"corner_radius": 16,
|
||||
"border_width": 2,
|
||||
"title_font_size": 26,
|
||||
"message_font_size": 20,
|
||||
"button_font_size": 20,
|
||||
"button_height": 48,
|
||||
"button_radius": 12,
|
||||
"button_border_width": 2,
|
||||
"remove_button_width": 170,
|
||||
"keep_button_width": 132,
|
||||
"surface_color": "#202633FA",
|
||||
"border_color": "#22C6FF",
|
||||
"accent_color": "#22C6FF",
|
||||
"action_color": "#087DBA",
|
||||
"danger_color": "#FF626B",
|
||||
"danger_hover_surface": "#3B252C",
|
||||
"danger_pressed_surface": "#4A252D"
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: Dictionary = {
|
||||
"version": SCHEMA_VERSION,
|
||||
@@ -13,14 +36,14 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"effects": {"impact_wobble": {"duration": 0.48, "maximum_angle_degrees": 7.0, "sideways_offset": 4.0}},
|
||||
"grid": {"snap_size": 15.0},
|
||||
"chrome": {"style_defaults": {}, "top_bar": {"height": 64}, "status_bar": {"height": 38}, "mode_switcher": {"segment_min_width": 124}, "mode_frame": {"border_width": 3}, "mode_badge": {"corner_radius": 12, "padding": 10}, "timestamp_badge": {"corner_radius": 10, "padding": 8, "bg_color": "#171B24F2"}, "tooltip": {"bg_color": "#101010E6", "border_width": 1}, "dialogs": {"queue_size":[680,520], "rule_size":[720,560], "action_size":[560,390], "confirm_size":[620,300], "font_size":22, "message_font_size":26}},
|
||||
"popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {"surface_color_size":[560,640], "surface_color_preview_size":[420,140], "surface_color_picker_min_size":[320,340], "action_min_target_size":48}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156], "surface_preview_size":[420,132], "look_cue_size":[80,42], "setup_gap":10}},
|
||||
"popups": {"style_defaults": {"bg_color": "#202633FA", "border_color": "#FFFFFF33", "corner_radius": 12, "padding": 12}, "action_menu": {"font_size": 24}, "dialog": {"surface_color_size":[560,640], "surface_color_preview_size":[420,140], "surface_color_picker_min_size":[320,340], "action_min_target_size":48}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156], "surface_preview_size":[420,132], "look_cue_size":[80,42], "setup_gap":8, "setup_title_size":30, "setup_heading_size":26, "setup_choice_font_size":22, "setup_choice_height":68, "setup_choice_corner_radius":22, "setup_choice_border_width":2, "tab_font_size":22, "tab_height":60, "filter_chip_font_size":20, "filter_chip_height":48, "filter_chip_gap":8, "setup_button_font_size":20, "button_border_width":2, "footer_gap":16, "surface_corner_radius":14, "look_corner_radius":8, "corner_radius":16, "compact_swatch_size":42, "color_group_gap":8, "action_color":"#087DBA", "separator_color":"#AAB2C0A0"}},
|
||||
"palette": {"style_defaults": {}, "dock_width": 116, "card_size": [96, 94], "icon_size": 30, "drag_threshold": 8, "selected_border_width": 3},
|
||||
"stage_panels": {
|
||||
"shell": {"preferred_width": 380, "edge_inset": 12, "padding": 12, "header_gap": 8, "section_gap": 12, "corner_radius": 10, "surface_color": "#252B38FA", "border_color": "#FFFFFF33"},
|
||||
"type": {"title_size": 24, "section_size": 20, "body_size": 20, "helper_size": 16, "text_color": "#F5F7FA", "muted_text_color": "#AAB2C0"},
|
||||
"choice": {"minimum_height": 56, "row_gap": 6, "neutral_surface": "#171B24", "hover_surface": "#303A4B", "selected_surface": "#12394A", "disabled_surface": "#252B38", "border_color": "#FFFFFF33"},
|
||||
"toggle": {"minimum_height": 64, "help_minimum_height": 72, "row_padding": 12, "switch_width": 64, "switch_height": 34, "knob_size": 26, "neutral_surface": "#202633", "hover_surface": "#2B3442", "selected_surface": "#173443", "disabled_surface": "#252B38", "border_color": "#FFFFFF33", "switch_on_color": "#22C6FF", "switch_off_color": "#4A5362", "switch_disabled_color": "#3B414D", "knob_color": "#F5F7FA"},
|
||||
"action": {"min_target_size": 48, "close_width": 88, "surface_color": "#171B24", "hover_surface": "#303A4B", "selected_surface": "#12394A"},
|
||||
"action": {"min_target_size": 48, "close_width": 88, "surface_color": "#171B24", "hover_surface": "#303A4B", "selected_surface": "#12394A", "primary_color": "#087DBA", "primary_hover_color": "#1596D3", "primary_pressed_color": "#066B9F", "destructive_color": "#FF626B", "destructive_hover_surface": "#3B252C", "destructive_pressed_surface": "#4A252D"},
|
||||
"prop": {"stickman_cue_color": "#F5F7FA", "prop_cue_color": "#B7824D", "cue_outline_color": "#171B24"},
|
||||
"spots": {"row_gap": 8, "pin_visual_size": 30, "pin_hit_size": 44, "pin_outline_width": 2, "focus_ring_width": 3, "name_pill_padding": 10, "pin_surface_color": "#F4F7FB", "pin_outline_color": "#171B24", "completion_duration": 0.6}
|
||||
},
|
||||
@@ -36,9 +59,13 @@ var _style_cache: Dictionary = {}
|
||||
var _control_theme: Theme = null
|
||||
var _warned: Dictionary = {}
|
||||
|
||||
func _init() -> void:
|
||||
_apply_confirmation_defaults()
|
||||
|
||||
func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||
invalidate_caches()
|
||||
_config = DEFAULT_CONFIG.duplicate(true)
|
||||
_apply_confirmation_defaults()
|
||||
if not FileAccess.file_exists(path):
|
||||
_warn_once(path, "SandboxTheme: '%s' was not found; using defaults." % path)
|
||||
return false
|
||||
@@ -55,6 +82,11 @@ func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||
_config = _deep_merge(_config, parsed as Dictionary)
|
||||
return true
|
||||
|
||||
func _apply_confirmation_defaults() -> void:
|
||||
var popups: Dictionary = _config.get("popups", {}).duplicate(true)
|
||||
popups["confirmation"] = CONFIRMATION_DEFAULTS.duplicate(true)
|
||||
_config["popups"] = popups
|
||||
|
||||
func raw_config() -> Dictionary:
|
||||
return _config.duplicate(true)
|
||||
|
||||
|
||||
@@ -2,14 +2,20 @@ class_name StageBackgroundGround
|
||||
extends "res://scripts/stage/scenery/stage_rect_surface.gd"
|
||||
## Non-walkable rectangular scenery surface behind the set.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
|
||||
## Keeps procedural ground finishes aligned when this surface is split by the area eraser.
|
||||
var motif_origin_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
func configure(new_size: Vector2, finish: StringName, color: Color) -> void:
|
||||
super.configure(new_size, finish if SURFACE_RENDERER.is_background_finish(finish) else &"finish_background_grass", color)
|
||||
z_index = DRAW_ORDER.BACKGROUND_GROUND
|
||||
|
||||
|
||||
func get_surface_draw_origin_world() -> Vector2:
|
||||
return global_position + motif_origin_offset
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
|
||||
_draw_surface(false)
|
||||
|
||||
@@ -2,17 +2,47 @@ class_name StageRectSurface
|
||||
extends Node2D
|
||||
## Shared rectangular geometry and transient tint preview for inert Stage scenery.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const SURFACE_CACHE := preload("res://scripts/stage/scenery/stage_surface_cache.gd")
|
||||
const PREVIEW_DRAW_BUDGET_USEC: int = 6000
|
||||
## Each rectangle needs one handle; the image itself is shared across rectangles.
|
||||
const MAX_RESIDENT_TILES: int = 1
|
||||
|
||||
var panel_size: Vector2 = Vector2(320.0, 400.0)
|
||||
var finish_id: StringName = &""
|
||||
var tint: Color = Color.WHITE
|
||||
var _preview_tint: Color = Color.WHITE
|
||||
var _has_preview_tint: bool = false
|
||||
var _preview_over_budget: bool = false
|
||||
var _surface_tiles: Dictionary = {}
|
||||
var _cache_style: Array = []
|
||||
var _cache_dirty: bool = true
|
||||
var _last_view_transform: Transform2D
|
||||
var _last_view_rect: Rect2
|
||||
var _was_covered: bool = false
|
||||
var _cover_regions_frame: int = -1
|
||||
var _cover_regions: Array[Dictionary] = []
|
||||
var _last_cover_regions: Array[Dictionary] = []
|
||||
## Placement uses adaptive vector detail; committed surfaces share material textures.
|
||||
var interactive_preview: bool = false:
|
||||
set(value):
|
||||
if interactive_preview == value:
|
||||
return
|
||||
interactive_preview = value
|
||||
_preview_over_budget = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func configure(new_size: Vector2, finish: StringName, color: Color) -> void:
|
||||
panel_size = Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y))
|
||||
var next_size := Vector2(maxf(1.0, new_size.x), maxf(1.0, new_size.y))
|
||||
if panel_size == next_size and finish_id == finish and tint == color:
|
||||
return
|
||||
if finish_id != finish:
|
||||
_preview_over_budget = false
|
||||
panel_size = next_size
|
||||
finish_id = finish
|
||||
tint = color
|
||||
_cache_dirty = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -37,3 +67,154 @@ func get_render_tint() -> Color:
|
||||
|
||||
func get_surface_draw_origin_world() -> Vector2:
|
||||
return global_position
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# Erase/Cancel retains detached geometry, never native drawing resources.
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.dispose()
|
||||
_surface_tiles.clear()
|
||||
_cache_dirty = true
|
||||
_cover_regions_frame = -1
|
||||
_last_cover_regions.clear()
|
||||
RenderingServer.canvas_item_clear(get_canvas_item())
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_visible_in_tree():
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.wanted = false
|
||||
SURFACE_CACHE.build_next()
|
||||
return
|
||||
var covered: bool = not interactive_preview and _covered_by_surface()
|
||||
var coverage_changed: bool = _get_cover_regions() != _last_cover_regions
|
||||
if coverage_changed:
|
||||
_last_cover_regions = _cover_regions.duplicate()
|
||||
if covered:
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.wanted = false
|
||||
SURFACE_CACHE.build_next()
|
||||
# Camera pans, zooms, and window resizing can reveal previously culled motifs.
|
||||
if coverage_changed or covered != _was_covered or get_global_transform_with_canvas() != _last_view_transform or get_viewport_rect() != _last_view_rect:
|
||||
queue_redraw()
|
||||
_was_covered = covered
|
||||
|
||||
|
||||
func _draw_surface(wall: bool) -> void:
|
||||
_last_view_transform = get_global_transform_with_canvas()
|
||||
_last_view_rect = get_viewport_rect()
|
||||
if not interactive_preview and DisplayServer.get_name() != "headless" and finish_id not in [&"finish_wall_paint", &"finish_ground_paint"]:
|
||||
if _covered_by_surface():
|
||||
for tile: SURFACE_CACHE in _surface_tiles.values():
|
||||
tile.wanted = false
|
||||
return
|
||||
_draw_cached_surface(wall)
|
||||
return
|
||||
var started_usec: int = Time.get_ticks_usec() if interactive_preview else 0
|
||||
if wall:
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world(), Vector2.ONE, true, interactive_preview, _preview_over_budget)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world(), Vector2.ONE, true, interactive_preview, _preview_over_budget)
|
||||
# Latch for this placement session to avoid oscillating between detail levels.
|
||||
# The complexity estimate handles large first draws before timing is available.
|
||||
if interactive_preview and Time.get_ticks_usec() - started_usec > PREVIEW_DRAW_BUDGET_USEC:
|
||||
_preview_over_budget = true
|
||||
|
||||
|
||||
func _draw_cached_surface(wall: bool) -> void:
|
||||
var visible_rect: Rect2 = get_local_bounds().intersection(SURFACE_RENDERER._visible_local_rect(self))
|
||||
var color: Color = get_render_tint()
|
||||
var style: Array = [wall, finish_id, color]
|
||||
if _cache_style != style:
|
||||
for previous: SURFACE_CACHE in _surface_tiles.values():
|
||||
previous.dispose()
|
||||
_surface_tiles.clear()
|
||||
_cache_style = style
|
||||
_cache_dirty = false
|
||||
for previous: SURFACE_CACHE in _surface_tiles.values():
|
||||
previous.wanted = false
|
||||
if visible_rect.size.x <= 0.0 or visible_rect.size.y <= 0.0:
|
||||
return
|
||||
var period: Vector2 = SURFACE_RENDERER.pattern_size_for(finish_id)
|
||||
if period.x <= 0.0 or period.y <= 0.0:
|
||||
draw_rect(visible_rect, SURFACE_RENDERER.surface_base_color(finish_id, color, wall))
|
||||
return
|
||||
var pattern: SURFACE_CACHE = _surface_tiles.get(&"pattern") as SURFACE_CACHE
|
||||
if pattern == null:
|
||||
pattern = SURFACE_CACHE.new() as SURFACE_CACHE
|
||||
pattern.attach_to(self)
|
||||
pattern.request_pattern(wall, finish_id, color, period)
|
||||
pattern.updated.connect(queue_redraw)
|
||||
_surface_tiles[&"pattern"] = pattern
|
||||
pattern.wanted = true
|
||||
pattern.last_used_frame = Engine.get_process_frames()
|
||||
pattern.ensure_queued()
|
||||
var texture: Texture2D = pattern.get_texture()
|
||||
if texture == null:
|
||||
draw_rect(visible_rect, SURFACE_RENDERER.surface_base_color(finish_id, color, wall))
|
||||
return
|
||||
texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||
# One quad samples a repeating material. Camera motion, panel resizing and
|
||||
# erase fragments change only these UVs, never the image or its mip levels.
|
||||
var sample_origin: Vector2 = visible_rect.position + get_surface_draw_origin_world()
|
||||
sample_origin = Vector2(fposmod(sample_origin.x, period.x), fposmod(sample_origin.y, period.y))
|
||||
var pixels_per_unit: Vector2 = Vector2(pattern.size) / period
|
||||
var source := Rect2(sample_origin * pixels_per_unit, visible_rect.size * pixels_per_unit)
|
||||
draw_texture_rect_region(texture, visible_rect, source, Color.WHITE, false, false)
|
||||
|
||||
|
||||
func _covered_by_surface(local_visible: Rect2 = Rect2()) -> bool:
|
||||
var visible: Rect2 = local_visible
|
||||
if visible.size.x <= 0.0 or visible.size.y <= 0.0:
|
||||
visible = get_local_bounds().intersection(SURFACE_RENDERER._visible_local_rect(self))
|
||||
if visible.size.x <= 0.0 or visible.size.y <= 0.0:
|
||||
return true
|
||||
var corners := PackedVector2Array([visible.position, Vector2(visible.end.x, visible.position.y), visible.end, Vector2(visible.position.x, visible.end.y)])
|
||||
for region: Dictionary in _get_cover_regions():
|
||||
var fully_covered: bool = true
|
||||
var cover_bounds: Rect2 = region["bounds"]
|
||||
var to_cover: Transform2D = region["transform"]
|
||||
for corner: Vector2 in corners:
|
||||
if not cover_bounds.has_point(to_cover * corner):
|
||||
fully_covered = false
|
||||
break
|
||||
if fully_covered:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _get_cover_regions() -> Array[Dictionary]:
|
||||
var frame: int = Engine.get_process_frames()
|
||||
if _cover_regions_frame == frame:
|
||||
return _cover_regions
|
||||
_cover_regions_frame = frame
|
||||
_cover_regions.clear()
|
||||
var parent: Node = get_parent()
|
||||
if parent == null or (parent is Node2D and (parent as Node2D).y_sort_enabled):
|
||||
return _cover_regions
|
||||
# Transparent ancestors and custom materials can reveal underlying layers.
|
||||
var ancestor: Node = self
|
||||
while ancestor is CanvasItem:
|
||||
if (ancestor as CanvasItem).modulate.a < 0.999 or (ancestor as CanvasItem).material != null:
|
||||
return _cover_regions
|
||||
ancestor = ancestor.get_parent()
|
||||
for sibling: Node in parent.get_children():
|
||||
if sibling == self or not (sibling is StageRectSurface):
|
||||
continue
|
||||
var cover: StageRectSurface = sibling as StageRectSurface
|
||||
if cover.z_as_relative != z_as_relative:
|
||||
continue
|
||||
if not cover.is_visible_in_tree() or cover.interactive_preview or cover.material != null or cover.modulate.a < 0.999 or cover.self_modulate.a < 0.999:
|
||||
continue
|
||||
if cover.z_index < z_index or (cover.z_index == z_index and cover.get_index() < get_index()):
|
||||
continue
|
||||
if SURFACE_RENDERER.surface_base_color(cover.finish_id, cover.get_render_tint(), SURFACE_RENDERER.is_wall_finish(cover.finish_id)).a < 0.999:
|
||||
continue
|
||||
if absf(cover.global_transform.determinant()) < 0.000001:
|
||||
continue
|
||||
_cover_regions.append({
|
||||
"bounds": cover.get_local_bounds().grow(-0.01),
|
||||
"transform": cover.global_transform.affine_inverse() * global_transform,
|
||||
})
|
||||
return _cover_regions
|
||||
|
||||
@@ -0,0 +1,929 @@
|
||||
class_name StageSurfaceCache
|
||||
extends RefCounted
|
||||
## Lightweight per-surface handle into a scene-scoped immutable bitmap cache.
|
||||
|
||||
const MAX_TEXTURE_EDGE: int = 4096
|
||||
const MAX_TEXTURE_PIXELS: float = 4194304.0
|
||||
const MAX_CACHE_BYTES: int = 64 * 1024 * 1024
|
||||
const MAX_CACHE_ENTRIES: int = 512
|
||||
const MAX_QUEUED_BUILDS: int = 512
|
||||
const MAX_QUEUE_SCAN_PER_FRAME: int = 8
|
||||
const RECENT_USE_PROTECTION_FRAMES: int = 3
|
||||
const MIN_DETAILED_SCALE: float = 0.5
|
||||
const PATTERN_CACHE_VERSION: int = 1
|
||||
const PATTERN_SOURCE_OVERSCAN: float = 4.0
|
||||
const PATTERN_PIXEL_SCALE: float = 2.0
|
||||
|
||||
static var _pool_refs: Dictionary = {}
|
||||
static var _last_service_frame: int = -1
|
||||
static var _active_pool_ref: WeakRef
|
||||
|
||||
signal updated
|
||||
|
||||
|
||||
class TileEntry extends RefCounted:
|
||||
var key: String
|
||||
var canonical_rect: Rect2
|
||||
var source_scale: Vector2
|
||||
var wall: bool
|
||||
var finish: StringName
|
||||
var tint: Color
|
||||
var detail_view_size: Vector2
|
||||
var size: Vector2i
|
||||
var byte_count: int = 0
|
||||
var texture: Texture2D
|
||||
var is_pattern: bool = false
|
||||
var period: Vector2 = Vector2.ZERO
|
||||
var watchers: Array[WeakRef] = []
|
||||
var canvas_owners: Array[WeakRef] = []
|
||||
var last_used_frame: int = 0
|
||||
var build_usec: int = 0
|
||||
var queued: bool = false
|
||||
var building: bool = false
|
||||
|
||||
|
||||
class SurfaceDrawing extends Node2D:
|
||||
const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
var source_rect: Rect2
|
||||
var wall: bool
|
||||
var finish: StringName
|
||||
var tint: Color
|
||||
var origin: Vector2 = Vector2.ZERO
|
||||
var detail_view_size: Vector2
|
||||
var low_detail: bool = false
|
||||
var active: bool = false
|
||||
var pool_ref: WeakRef
|
||||
|
||||
func _draw() -> void:
|
||||
var pool: Variant = pool_ref.get_ref() if pool_ref != null else null
|
||||
if not active or pool == null:
|
||||
return
|
||||
active = false
|
||||
var started_usec: int = Time.get_ticks_usec()
|
||||
if wall:
|
||||
RENDERER.draw_wall(self, source_rect, finish, tint, origin, Vector2.ONE, false, low_detail, low_detail, detail_view_size)
|
||||
else:
|
||||
RENDERER.draw_background(self, source_rect, finish, tint, origin, Vector2.ONE, false, low_detail, low_detail)
|
||||
pool._finish_vector_submission(Time.get_ticks_usec() - started_usec)
|
||||
|
||||
|
||||
class BakePool extends Node:
|
||||
const RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const BAKE_GUTTER_PIXELS: int = 4
|
||||
var scope_id: int = 0
|
||||
var entries: Dictionary = {}
|
||||
var queue: Array[String] = []
|
||||
var _waiting_handles: Array[WeakRef] = []
|
||||
var resident_bytes: int = 0
|
||||
var builds: int = 0
|
||||
var cache_hits: int = 0
|
||||
var evictions: int = 0
|
||||
var refusals: int = 0
|
||||
var readbacks: int = 0
|
||||
var pattern_builds: int = 0
|
||||
var pattern_readbacks: int = 0
|
||||
var pattern_resident_bytes: int = 0
|
||||
var pattern_retired_bytes: int = 0
|
||||
var last_build_usec: int = 0
|
||||
var vector_submission_usec: int = 0
|
||||
var retired_bytes: int = 0
|
||||
var budget_blocked_requests: int = 0
|
||||
var budget_pressure: bool = false
|
||||
var budget_pressure_frame: int = -1
|
||||
var _viewport: SubViewport
|
||||
var _drawing: SurfaceDrawing
|
||||
var _building_entry: TileEntry
|
||||
var _building_started_usec: int = 0
|
||||
var _reserved_bytes: int = 0
|
||||
var _retired_textures: Array[Texture2D] = []
|
||||
var _retire_callback_pending: bool = false
|
||||
var _capture_pending: bool = false
|
||||
var _disposed: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(true)
|
||||
_ensure_baker()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_prune_queue()
|
||||
StageSurfaceCache._service_pools()
|
||||
|
||||
func _ensure_baker() -> void:
|
||||
if is_instance_valid(_viewport):
|
||||
return
|
||||
_viewport = SubViewport.new()
|
||||
_viewport.name = "SurfaceBitmapBaker"
|
||||
_viewport.disable_3d = true
|
||||
_viewport.transparent_bg = true
|
||||
_viewport.render_target_clear_mode = SubViewport.CLEAR_MODE_ALWAYS
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
_viewport.world_2d = World2D.new()
|
||||
add_child(_viewport)
|
||||
_drawing = SurfaceDrawing.new()
|
||||
_drawing.name = "SurfaceVectorSource"
|
||||
_drawing.pool_ref = weakref(self)
|
||||
_viewport.add_child(_drawing)
|
||||
|
||||
func queue_handle(handle: StageSurfaceCache) -> void:
|
||||
if _disposed or handle._disposed:
|
||||
return
|
||||
var entry: TileEntry = _find_or_create_entry(handle)
|
||||
if entry == null:
|
||||
refusals += 1
|
||||
_add_waiter(handle)
|
||||
return
|
||||
_bind_handle(handle, entry)
|
||||
if not _handle_is_interested(handle):
|
||||
return
|
||||
handle.last_used_frame = Engine.get_process_frames()
|
||||
entry.last_used_frame = Engine.get_process_frames()
|
||||
if entry.texture != null or entry.queued or entry.building:
|
||||
return
|
||||
if queue.size() >= MAX_QUEUED_BUILDS:
|
||||
refusals += 1
|
||||
_add_waiter(handle)
|
||||
return
|
||||
entry.queued = true
|
||||
queue.append(entry.key)
|
||||
handle._enqueued = true
|
||||
|
||||
func _find_or_create_entry(handle: StageSurfaceCache) -> TileEntry:
|
||||
var existing: TileEntry = entries.get(handle._cache_key) as TileEntry
|
||||
if existing != null:
|
||||
cache_hits += 1
|
||||
return existing
|
||||
while entries.size() + _retired_textures.size() >= MAX_CACHE_ENTRIES:
|
||||
if not _retired_textures.is_empty():
|
||||
return null
|
||||
if not _evict_one(null):
|
||||
return null
|
||||
if not _retired_textures.is_empty():
|
||||
return null
|
||||
var arguments: Array = handle._pending_arguments
|
||||
if arguments.size() < 6:
|
||||
return null
|
||||
var entry := TileEntry.new()
|
||||
entry.key = handle._cache_key
|
||||
entry.canonical_rect = arguments[0] as Rect2
|
||||
entry.source_scale = arguments[1] as Vector2
|
||||
entry.wall = bool(arguments[2])
|
||||
entry.finish = arguments[3] as StringName
|
||||
entry.tint = arguments[4] as Color
|
||||
entry.detail_view_size = arguments[5] as Vector2
|
||||
if arguments.size() >= 8:
|
||||
entry.is_pattern = bool(arguments[6])
|
||||
entry.period = arguments[7] as Vector2
|
||||
entries[entry.key] = entry
|
||||
return entry
|
||||
|
||||
func _bind_handle(handle: StageSurfaceCache, entry: TileEntry) -> void:
|
||||
_register_canvas_owner(entry, handle)
|
||||
var current: TileEntry = handle._entry_ref.get_ref() as TileEntry if handle._entry_ref != null else null
|
||||
if current == entry:
|
||||
handle.ready_for_display = entry.texture != null
|
||||
handle._building = entry.building
|
||||
handle._enqueued = entry.queued
|
||||
handle.build_usec = entry.build_usec
|
||||
return
|
||||
if current != null:
|
||||
_unwatch_handle(current, handle)
|
||||
handle._entry_ref = weakref(entry)
|
||||
handle.ready_for_display = entry.texture != null
|
||||
handle._building = entry.building
|
||||
handle._enqueued = entry.queued
|
||||
handle.build_usec = entry.build_usec
|
||||
var found: bool = false
|
||||
for watcher_ref: WeakRef in entry.watchers:
|
||||
if watcher_ref.get_ref() == handle:
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
entry.watchers.append(weakref(handle))
|
||||
|
||||
func _unwatch_handle(entry: TileEntry, handle: StageSurfaceCache) -> void:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var watcher: Variant = entry.watchers[index].get_ref()
|
||||
if watcher == null or watcher == handle:
|
||||
entry.watchers.remove_at(index)
|
||||
|
||||
func _register_canvas_owner(entry: TileEntry, handle: StageSurfaceCache) -> void:
|
||||
var owner: Variant = handle._owner_ref.get_ref() if handle._owner_ref != null else null
|
||||
if owner == null or not is_instance_valid(owner):
|
||||
return
|
||||
for index: int in range(entry.canvas_owners.size() - 1, -1, -1):
|
||||
var registered_owner: Variant = entry.canvas_owners[index].get_ref()
|
||||
if registered_owner == null or not is_instance_valid(registered_owner):
|
||||
entry.canvas_owners.remove_at(index)
|
||||
elif registered_owner == owner:
|
||||
return
|
||||
entry.canvas_owners.append(weakref(owner))
|
||||
|
||||
func _start_next_build() -> bool:
|
||||
if _disposed or _capture_pending:
|
||||
return false
|
||||
_prune_queue()
|
||||
var attempts: int = mini(queue.size(), MAX_QUEUE_SCAN_PER_FRAME)
|
||||
while attempts > 0 and not queue.is_empty():
|
||||
attempts -= 1
|
||||
var key: String = _pop_next_build_key()
|
||||
var entry: TileEntry = entries.get(key) as TileEntry
|
||||
if entry == null or not entry.queued or entry.texture != null:
|
||||
continue
|
||||
entry.queued = false
|
||||
if not _entry_has_live_watchers(entry):
|
||||
_evict_entry(entry)
|
||||
continue
|
||||
var size: Vector2i = _baker_size_for(entry)
|
||||
var required_bytes: int = _estimated_texture_bytes(entry)
|
||||
if not _make_room(required_bytes, entry):
|
||||
refusals += 1
|
||||
if queue.size() < MAX_QUEUED_BUILDS:
|
||||
entry.queued = true
|
||||
queue.append(entry.key)
|
||||
budget_blocked_requests += 1
|
||||
budget_pressure = true
|
||||
budget_pressure_frame = Engine.get_process_frames()
|
||||
continue
|
||||
_begin_build(entry, size)
|
||||
return true
|
||||
return false
|
||||
|
||||
func _baker_rect_for(entry: TileEntry) -> Rect2:
|
||||
return entry.canonical_rect.grow(PATTERN_SOURCE_OVERSCAN) if entry.is_pattern else entry.canonical_rect
|
||||
|
||||
func _baker_size_for(entry: TileEntry) -> Vector2i:
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
return StageSurfaceCache.texture_size_for(draw_rect.size, entry.source_scale)
|
||||
|
||||
func _pattern_output_size(entry: TileEntry) -> Vector2i:
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
var baker_size: Vector2i = StageSurfaceCache.texture_size_for(draw_rect.size, entry.source_scale)
|
||||
return Vector2i(
|
||||
maxi(1, roundi(float(baker_size.x) * entry.period.x / draw_rect.size.x)),
|
||||
maxi(1, roundi(float(baker_size.y) * entry.period.y / draw_rect.size.y))
|
||||
)
|
||||
|
||||
func _estimated_texture_bytes(entry: TileEntry) -> int:
|
||||
var base_size: Vector2i = _pattern_output_size(entry) if entry.is_pattern else _baker_size_for(entry)
|
||||
return _mip_chain_byte_count(base_size) if entry.is_pattern else base_size.x * base_size.y * 4
|
||||
|
||||
func _crop_pattern_image(image: Image, entry: TileEntry) -> Image:
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
var output_size: Vector2i = _pattern_output_size(entry)
|
||||
var scale: Vector2 = Vector2(image.get_size()) / draw_rect.size
|
||||
var crop_position := Vector2i(
|
||||
roundi((entry.canonical_rect.position.x - draw_rect.position.x) * scale.x),
|
||||
roundi((entry.canonical_rect.position.y - draw_rect.position.y) * scale.y)
|
||||
)
|
||||
crop_position.x = clampi(crop_position.x, 0, maxi(0, image.get_width() - output_size.x))
|
||||
crop_position.y = clampi(crop_position.y, 0, maxi(0, image.get_height() - output_size.y))
|
||||
var safe_size := Vector2i(
|
||||
mini(output_size.x, image.get_width() - crop_position.x),
|
||||
mini(output_size.y, image.get_height() - crop_position.y)
|
||||
)
|
||||
if safe_size.x <= 0 or safe_size.y <= 0:
|
||||
return Image.new()
|
||||
return image.get_region(Rect2i(crop_position, safe_size))
|
||||
|
||||
func _mip_chain_byte_count(size: Vector2i) -> int:
|
||||
var width: int = maxi(1, size.x)
|
||||
var height: int = maxi(1, size.y)
|
||||
var total: int = 0
|
||||
while true:
|
||||
total += width * height * 4
|
||||
if width == 1 and height == 1:
|
||||
break
|
||||
width = maxi(1, floori(float(width) * 0.5))
|
||||
height = maxi(1, floori(float(height) * 0.5))
|
||||
return total
|
||||
|
||||
func _pop_next_build_key() -> String:
|
||||
var selected_index: int = 0
|
||||
var selected_scale: float = INF
|
||||
for index: int in range(queue.size()):
|
||||
var entry: TileEntry = entries.get(queue[index]) as TileEntry
|
||||
if entry == null:
|
||||
continue
|
||||
var scale_rank: float = maxf(entry.source_scale.x, entry.source_scale.y)
|
||||
if scale_rank < selected_scale:
|
||||
selected_scale = scale_rank
|
||||
selected_index = index
|
||||
return queue.pop_at(selected_index)
|
||||
|
||||
func _begin_build(entry: TileEntry, texture_size: Vector2i) -> void:
|
||||
_ensure_baker()
|
||||
RenderingServer.canvas_item_clear(_drawing.get_canvas_item())
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
_viewport.size = texture_size
|
||||
var draw_rect: Rect2 = _baker_rect_for(entry)
|
||||
_drawing.source_rect = draw_rect
|
||||
_drawing.wall = entry.wall
|
||||
_drawing.finish = entry.finish
|
||||
_drawing.tint = entry.tint
|
||||
_drawing.origin = Vector2.ZERO
|
||||
_drawing.detail_view_size = entry.period if entry.is_pattern else entry.detail_view_size
|
||||
_drawing.low_detail = not entry.is_pattern and minf(entry.source_scale.x, entry.source_scale.y) < MIN_DETAILED_SCALE
|
||||
var draw_scale := Vector2(texture_size) / draw_rect.size
|
||||
_drawing.scale = draw_scale
|
||||
_drawing.position = -draw_rect.position * draw_scale
|
||||
_drawing.active = true
|
||||
_building_entry = entry
|
||||
_building_started_usec = Time.get_ticks_usec()
|
||||
_reserved_bytes = _estimated_texture_bytes(entry)
|
||||
entry.building = true
|
||||
_capture_pending = true
|
||||
_drawing.queue_redraw()
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
if not RenderingServer.frame_post_draw.is_connected(_on_frame_post_draw):
|
||||
RenderingServer.frame_post_draw.connect(_on_frame_post_draw, CONNECT_ONE_SHOT)
|
||||
StageSurfaceCache._active_pool_ref = weakref(self)
|
||||
|
||||
func _finish_vector_submission(elapsed_usec: int) -> void:
|
||||
vector_submission_usec = elapsed_usec
|
||||
|
||||
func _on_frame_post_draw() -> void:
|
||||
if not _capture_pending or _building_entry == null:
|
||||
return
|
||||
var entry: TileEntry = _building_entry
|
||||
var image: Image
|
||||
if _entry_has_live_watchers(entry) and is_instance_valid(_viewport):
|
||||
image = _viewport.get_texture().get_image()
|
||||
if image != null and not image.is_empty():
|
||||
if entry.is_pattern:
|
||||
image = _crop_pattern_image(image, entry)
|
||||
if image != null and not image.is_empty():
|
||||
image.generate_mipmaps()
|
||||
if image != null and not image.is_empty():
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
entry.texture = texture
|
||||
entry.size = image.get_size()
|
||||
entry.byte_count = image.get_data_size()
|
||||
resident_bytes += entry.byte_count
|
||||
if entry.is_pattern:
|
||||
pattern_builds += 1
|
||||
pattern_readbacks += 1
|
||||
pattern_resident_bytes += entry.byte_count
|
||||
_reserved_bytes = 0
|
||||
builds += 1
|
||||
readbacks += 1
|
||||
budget_pressure = false
|
||||
else:
|
||||
_reserved_bytes = 0
|
||||
refusals += 1
|
||||
_notify_entry_failed(entry)
|
||||
last_build_usec = Time.get_ticks_usec() - _building_started_usec
|
||||
if entry.texture != null:
|
||||
entry.build_usec = last_build_usec
|
||||
_notify_entry_ready(entry)
|
||||
entry.building = false
|
||||
_building_entry = null
|
||||
_capture_pending = false
|
||||
if is_instance_valid(_viewport):
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_DISABLED
|
||||
if is_instance_valid(_drawing):
|
||||
_drawing.active = false
|
||||
RenderingServer.canvas_item_clear(_drawing.get_canvas_item())
|
||||
if StageSurfaceCache._active_pool_ref != null and StageSurfaceCache._active_pool_ref.get_ref() == self:
|
||||
StageSurfaceCache._active_pool_ref = null
|
||||
_wake_waiters()
|
||||
StageSurfaceCache._service_pools()
|
||||
|
||||
func _notify_entry_ready(entry: TileEntry) -> void:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
handle._on_entry_ready(entry)
|
||||
|
||||
func _notify_entry_failed(entry: TileEntry) -> void:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
handle._on_entry_failed(entry)
|
||||
|
||||
func _entry_has_live_watchers(entry: TileEntry) -> bool:
|
||||
var has_watcher: bool = false
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null or handle._disposed:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
if _handle_is_interested(handle):
|
||||
has_watcher = true
|
||||
return has_watcher
|
||||
|
||||
func _make_room(required_bytes: int, protected_entry: TileEntry) -> bool:
|
||||
if resident_bytes + retired_bytes + _reserved_bytes + required_bytes <= MAX_CACHE_BYTES:
|
||||
return true
|
||||
if retired_bytes > 0:
|
||||
return false
|
||||
if not _evict_one(protected_entry):
|
||||
return false
|
||||
# Keep the old texture alive until the frame that releases its CanvasItem RIDs.
|
||||
# The request will retry after the post-draw retirement callback.
|
||||
return resident_bytes + retired_bytes + _reserved_bytes + required_bytes <= MAX_CACHE_BYTES
|
||||
|
||||
func _evict_one(protected_entry: TileEntry) -> bool:
|
||||
var candidate: TileEntry
|
||||
for value: Variant in entries.values():
|
||||
var entry: TileEntry = value as TileEntry
|
||||
if entry == null or entry == protected_entry or entry.building:
|
||||
continue
|
||||
if _entry_is_protected(entry):
|
||||
continue
|
||||
if candidate == null or entry.last_used_frame < candidate.last_used_frame:
|
||||
candidate = entry
|
||||
if candidate == null:
|
||||
return false
|
||||
_evict_entry(candidate)
|
||||
return true
|
||||
|
||||
func _entry_is_protected(entry: TileEntry) -> bool:
|
||||
if entry.texture == null:
|
||||
return false
|
||||
var current_frame: int = Engine.get_process_frames()
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null or handle._disposed:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
if _handle_is_visible(handle) and (handle.wanted or current_frame - handle.last_used_frame <= RECENT_USE_PROTECTION_FRAMES):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _evict_entry(entry: TileEntry) -> void:
|
||||
if entry == null or entry.building:
|
||||
return
|
||||
if entry.queued:
|
||||
queue.erase(entry.key)
|
||||
entry.queued = false
|
||||
_clear_entry_canvas_owners(entry)
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null:
|
||||
entry.watchers.remove_at(index)
|
||||
continue
|
||||
handle._on_entry_evicted(entry)
|
||||
var retired_texture: Texture2D = entry.texture
|
||||
var retired_size_bytes: int = entry.byte_count
|
||||
entry.texture = null
|
||||
resident_bytes = maxi(0, resident_bytes - entry.byte_count)
|
||||
if entry.is_pattern:
|
||||
pattern_resident_bytes = maxi(0, pattern_resident_bytes - entry.byte_count)
|
||||
entry.byte_count = 0
|
||||
if retired_texture != null:
|
||||
_retired_textures.append(retired_texture)
|
||||
retired_bytes += retired_size_bytes
|
||||
if entry.is_pattern:
|
||||
pattern_retired_bytes += retired_size_bytes
|
||||
if not _retire_callback_pending:
|
||||
_retire_callback_pending = true
|
||||
RenderingServer.frame_post_draw.connect(_on_retired_frame_post_draw, CONNECT_ONE_SHOT)
|
||||
if entries.get(entry.key) == entry:
|
||||
entries.erase(entry.key)
|
||||
evictions += 1
|
||||
|
||||
func _clear_owner_canvas(owner: Variant) -> void:
|
||||
if not (owner is CanvasItem) or not is_instance_valid(owner):
|
||||
return
|
||||
var canvas_item: CanvasItem = owner as CanvasItem
|
||||
RenderingServer.canvas_item_clear(canvas_item.get_canvas_item())
|
||||
canvas_item.queue_redraw()
|
||||
|
||||
func _clear_entry_canvas_owners(entry: TileEntry) -> void:
|
||||
var cleared_owners: Dictionary = {}
|
||||
for owner_ref: WeakRef in entry.canvas_owners:
|
||||
var owner: Variant = owner_ref.get_ref()
|
||||
if owner == null or not is_instance_valid(owner):
|
||||
continue
|
||||
var owner_id: int = owner.get_instance_id()
|
||||
if cleared_owners.has(owner_id):
|
||||
continue
|
||||
_clear_owner_canvas(owner)
|
||||
cleared_owners[owner_id] = true
|
||||
entry.canvas_owners.clear()
|
||||
|
||||
func _stats() -> Dictionary:
|
||||
var queued_count: int = 0
|
||||
for value: Variant in entries.values():
|
||||
var entry: TileEntry = value as TileEntry
|
||||
if entry != null and entry.queued:
|
||||
queued_count += 1
|
||||
return {
|
||||
"bytes": resident_bytes,
|
||||
"resident_bytes": resident_bytes,
|
||||
"retired_bytes": retired_bytes,
|
||||
"entries": entries.size(),
|
||||
"queued": queued_count,
|
||||
"bakers": 1 if is_instance_valid(_viewport) else 0,
|
||||
"live_vector_bakers": 1 if _capture_pending else 0,
|
||||
"live_vector_items": 1 if _capture_pending else 0,
|
||||
"builds": builds,
|
||||
"cache_hits": cache_hits,
|
||||
"evictions": evictions,
|
||||
"refusals": refusals,
|
||||
"budget_pressure": budget_pressure,
|
||||
"budget_pressure_frame": budget_pressure_frame,
|
||||
"budget_blocked_requests": budget_blocked_requests,
|
||||
"waiting_handles": _waiting_handles.size(),
|
||||
"readbacks": readbacks,
|
||||
"pattern_builds": pattern_builds,
|
||||
"pattern_readbacks": pattern_readbacks,
|
||||
"pattern_resident_bytes": pattern_resident_bytes,
|
||||
"pattern_retired_bytes": pattern_retired_bytes,
|
||||
"last_build_usec": last_build_usec,
|
||||
"vector_submission_usec": vector_submission_usec,
|
||||
"active_build": _building_entry.key if _building_entry != null else ""
|
||||
}
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_disposed = true
|
||||
if RenderingServer.frame_post_draw.is_connected(_on_frame_post_draw):
|
||||
RenderingServer.frame_post_draw.disconnect(_on_frame_post_draw)
|
||||
if _capture_pending and _building_entry != null:
|
||||
_building_entry.building = false
|
||||
_notify_entry_failed(_building_entry)
|
||||
_building_entry = null
|
||||
_capture_pending = false
|
||||
if StageSurfaceCache._active_pool_ref != null and StageSurfaceCache._active_pool_ref.get_ref() == self:
|
||||
StageSurfaceCache._active_pool_ref = null
|
||||
for value: Variant in entries.values():
|
||||
var entry: TileEntry = value as TileEntry
|
||||
if entry == null:
|
||||
continue
|
||||
_clear_entry_canvas_owners(entry)
|
||||
for watcher_ref: WeakRef in entry.watchers:
|
||||
var handle: Variant = watcher_ref.get_ref()
|
||||
if handle != null:
|
||||
handle._on_entry_evicted(entry)
|
||||
entry.texture = null
|
||||
entries.clear()
|
||||
queue.clear()
|
||||
_waiting_handles.clear()
|
||||
_retired_textures.clear()
|
||||
resident_bytes = 0
|
||||
retired_bytes = 0
|
||||
pattern_resident_bytes = 0
|
||||
pattern_retired_bytes = 0
|
||||
if is_instance_valid(_viewport):
|
||||
_viewport.queue_free()
|
||||
_viewport = null
|
||||
_drawing = null
|
||||
StageSurfaceCache._forget_pool(scope_id, self)
|
||||
|
||||
func _prune_queue() -> void:
|
||||
if queue.is_empty():
|
||||
return
|
||||
var kept: Array[String] = []
|
||||
for key: String in queue:
|
||||
var entry: TileEntry = entries.get(key) as TileEntry
|
||||
if entry == null or not entry.queued or entry.texture != null or not _entry_has_live_watchers(entry):
|
||||
if entry != null:
|
||||
entry.queued = false
|
||||
_set_entry_handle_queue_state(entry, false)
|
||||
if entry.texture == null and not entry.building and not _entry_has_live_watchers(entry):
|
||||
_evict_entry(entry)
|
||||
continue
|
||||
kept.append(key)
|
||||
queue = kept
|
||||
if queue.size() < MAX_QUEUED_BUILDS and not _waiting_handles.is_empty():
|
||||
_wake_waiters()
|
||||
|
||||
func _entry_has_any_handles(entry: TileEntry) -> bool:
|
||||
for index: int in range(entry.watchers.size() - 1, -1, -1):
|
||||
var handle: Variant = entry.watchers[index].get_ref()
|
||||
if handle == null or handle._disposed:
|
||||
entry.watchers.remove_at(index)
|
||||
else:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _set_entry_handle_queue_state(entry: TileEntry, is_queued: bool) -> void:
|
||||
for watcher_ref: WeakRef in entry.watchers:
|
||||
var handle: Variant = watcher_ref.get_ref()
|
||||
if handle != null:
|
||||
handle._enqueued = is_queued
|
||||
|
||||
func _handle_is_interested(handle: StageSurfaceCache) -> bool:
|
||||
return handle != null and not handle._disposed and handle.wanted and _handle_is_visible(handle)
|
||||
|
||||
func _handle_is_visible(handle: StageSurfaceCache) -> bool:
|
||||
if handle == null or handle._disposed:
|
||||
return false
|
||||
var owner: Variant = handle._owner_ref.get_ref() if handle._owner_ref != null else null
|
||||
if owner == null or not is_instance_valid(owner) or not (owner is Node2D):
|
||||
return false
|
||||
if not owner.is_inside_tree() or not owner.is_visible_in_tree():
|
||||
return false
|
||||
var visible_rect: Rect2 = RENDERER._visible_local_rect(owner)
|
||||
if handle.is_pattern:
|
||||
if not owner.has_method("get_local_bounds"):
|
||||
return false
|
||||
var local_bounds: Variant = owner.call("get_local_bounds")
|
||||
if not (local_bounds is Rect2):
|
||||
return false
|
||||
var bounds: Rect2 = local_bounds
|
||||
return bounds.intersects(visible_rect)
|
||||
return handle.source_rect.intersects(visible_rect) or handle.core_rect.intersects(visible_rect)
|
||||
|
||||
func _add_waiter(handle: StageSurfaceCache) -> void:
|
||||
for waiter_ref: WeakRef in _waiting_handles:
|
||||
if waiter_ref.get_ref() == handle:
|
||||
return
|
||||
_waiting_handles.append(weakref(handle))
|
||||
|
||||
func _wake_waiters() -> void:
|
||||
if _waiting_handles.is_empty() or _disposed:
|
||||
return
|
||||
var pending: Array[WeakRef] = _waiting_handles
|
||||
_waiting_handles = []
|
||||
for waiter_ref: WeakRef in pending:
|
||||
var handle: Variant = waiter_ref.get_ref()
|
||||
if handle == null or handle._disposed or not _handle_is_interested(handle):
|
||||
continue
|
||||
queue_handle(handle)
|
||||
|
||||
func _on_retired_frame_post_draw() -> void:
|
||||
_retired_textures.clear()
|
||||
retired_bytes = 0
|
||||
pattern_retired_bytes = 0
|
||||
_retire_callback_pending = false
|
||||
_wake_waiters()
|
||||
StageSurfaceCache._service_pools()
|
||||
|
||||
|
||||
var source_rect: Rect2
|
||||
var source_scale: Vector2
|
||||
var core_rect: Rect2
|
||||
var ready_for_display: bool = false
|
||||
var wanted: bool = false
|
||||
var last_used_frame: int = 0
|
||||
var build_usec: int = 0
|
||||
var _enqueued: bool = false
|
||||
var _building: bool = false
|
||||
var _pending_arguments: Array = []
|
||||
var _owner_ref: WeakRef
|
||||
var _pool_ref: WeakRef
|
||||
var _entry_ref: WeakRef
|
||||
var _cache_key: String = ""
|
||||
var _disposed: bool = false
|
||||
var _finish: StringName
|
||||
var _tint: Color
|
||||
var _origin: Vector2
|
||||
var is_pattern: bool = false
|
||||
|
||||
var size: Vector2i:
|
||||
get:
|
||||
var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null
|
||||
return entry.size if entry != null else Vector2i.ZERO
|
||||
|
||||
var render_target_update_mode: int:
|
||||
get:
|
||||
return SubViewport.UPDATE_DISABLED
|
||||
|
||||
|
||||
func attach_to(owner_node: Node) -> void:
|
||||
_owner_ref = weakref(owner_node)
|
||||
var pool: BakePool = _get_or_create_pool(owner_node)
|
||||
_pool_ref = weakref(pool) if pool != null else null
|
||||
|
||||
|
||||
func dispose() -> void:
|
||||
wanted = false
|
||||
_disposed = true
|
||||
var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null
|
||||
var pool: BakePool = _pool_ref.get_ref() as BakePool if _pool_ref != null else null
|
||||
if entry != null and pool != null:
|
||||
pool._unwatch_handle(entry, self)
|
||||
_entry_ref = null
|
||||
ready_for_display = false
|
||||
_building = false
|
||||
_enqueued = false
|
||||
|
||||
|
||||
func get_texture() -> Texture2D:
|
||||
var entry: TileEntry = _entry_ref.get_ref() as TileEntry if _entry_ref != null else null
|
||||
return entry.texture if entry != null else null
|
||||
|
||||
|
||||
func request_build(core: Rect2, rect: Rect2, pixel_scale: Vector2, wall: bool, finish: StringName, tint: Color, origin: Vector2, detail_view_size: Vector2) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
is_pattern = false
|
||||
core_rect = core
|
||||
source_rect = rect
|
||||
source_scale = pixel_scale
|
||||
wanted = true
|
||||
_finish = finish
|
||||
_tint = tint
|
||||
_origin = origin
|
||||
var canonical_position: Vector2 = _snap_canonical_position(rect.position + origin, pixel_scale)
|
||||
var canonical_rect := Rect2(canonical_position, rect.size)
|
||||
_pending_arguments = [canonical_rect, pixel_scale, wall, finish, tint, detail_view_size]
|
||||
_cache_key = _make_cache_key(canonical_rect, pixel_scale, wall, finish, tint, detail_view_size)
|
||||
queue_if_needed()
|
||||
|
||||
|
||||
func request_pattern(wall: bool, finish: StringName, tint: Color, period: Vector2) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
var safe_period := Vector2(maxf(1.0, period.x), maxf(1.0, period.y))
|
||||
is_pattern = true
|
||||
core_rect = Rect2(Vector2.ZERO, safe_period)
|
||||
source_rect = core_rect
|
||||
source_scale = Vector2.ONE * PATTERN_PIXEL_SCALE
|
||||
wanted = true
|
||||
_finish = finish
|
||||
_tint = tint
|
||||
_origin = Vector2.ZERO
|
||||
_pending_arguments = [core_rect, source_scale, wall, finish, tint, safe_period, true, safe_period]
|
||||
_cache_key = _make_pattern_cache_key(wall, finish, tint, safe_period)
|
||||
queue_if_needed()
|
||||
|
||||
|
||||
func ensure_queued() -> void:
|
||||
if _disposed:
|
||||
return
|
||||
queue_if_needed()
|
||||
|
||||
|
||||
func queue_if_needed() -> void:
|
||||
var pool: BakePool = _pool_ref.get_ref() as BakePool if _pool_ref != null else null
|
||||
if pool == null:
|
||||
var owner: Variant = _owner_ref.get_ref() if _owner_ref != null else null
|
||||
if owner != null and is_instance_valid(owner):
|
||||
pool = _get_or_create_pool(owner)
|
||||
_pool_ref = weakref(pool) if pool != null else null
|
||||
if pool != null:
|
||||
pool.queue_handle(self)
|
||||
|
||||
|
||||
func _on_entry_ready(entry: TileEntry) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
ready_for_display = true
|
||||
_building = false
|
||||
_enqueued = false
|
||||
build_usec = entry.build_usec
|
||||
updated.emit()
|
||||
|
||||
|
||||
func _on_entry_failed(entry: TileEntry) -> void:
|
||||
if _disposed:
|
||||
return
|
||||
if _entry_ref != null and _entry_ref.get_ref() == entry:
|
||||
ready_for_display = false
|
||||
_building = false
|
||||
_enqueued = false
|
||||
updated.emit()
|
||||
|
||||
|
||||
func _on_entry_evicted(entry: TileEntry) -> void:
|
||||
if _entry_ref != null and _entry_ref.get_ref() == entry:
|
||||
_entry_ref = null
|
||||
ready_for_display = false
|
||||
_building = false
|
||||
_enqueued = false
|
||||
updated.emit()
|
||||
|
||||
|
||||
static func build_next() -> void:
|
||||
_service_pools()
|
||||
|
||||
|
||||
static func _service_pools() -> void:
|
||||
var frame: int = Engine.get_process_frames()
|
||||
if frame == _last_service_frame:
|
||||
return
|
||||
_last_service_frame = frame
|
||||
var active_pool: Variant = _active_pool_ref.get_ref() if _active_pool_ref != null else null
|
||||
if active_pool != null and is_instance_valid(active_pool):
|
||||
if not active_pool._capture_pending:
|
||||
active_pool._start_next_build()
|
||||
return
|
||||
_active_pool_ref = null
|
||||
for scope_id: Variant in _pool_refs.keys():
|
||||
var pool: Variant = _pool_refs[scope_id].get_ref()
|
||||
if pool == null or not is_instance_valid(pool):
|
||||
_pool_refs.erase(scope_id)
|
||||
continue
|
||||
if pool._start_next_build():
|
||||
return
|
||||
|
||||
|
||||
static func _get_or_create_pool(owner_node: Node) -> BakePool:
|
||||
if owner_node == null or not is_instance_valid(owner_node) or owner_node.get_tree() == null:
|
||||
return null
|
||||
var scope: Node = _get_scope_node(owner_node)
|
||||
if scope == null or not is_instance_valid(scope):
|
||||
return null
|
||||
var scope_id: int = scope.get_instance_id()
|
||||
var pool_ref: WeakRef = _pool_refs.get(scope_id) as WeakRef
|
||||
var existing: BakePool = pool_ref.get_ref() as BakePool if pool_ref != null else null
|
||||
if existing != null and is_instance_valid(existing):
|
||||
return existing
|
||||
var pool := BakePool.new()
|
||||
pool.name = "StageSurfaceBakePool"
|
||||
pool.scope_id = scope_id
|
||||
scope.add_child(pool)
|
||||
_pool_refs[scope_id] = weakref(pool)
|
||||
return pool
|
||||
|
||||
|
||||
static func _get_scope_node(owner_node: Node) -> Node:
|
||||
var tree: SceneTree = owner_node.get_tree()
|
||||
if tree == null:
|
||||
return null
|
||||
var current_scene: Node = tree.current_scene
|
||||
if current_scene != null and (current_scene == owner_node or current_scene.is_ancestor_of(owner_node)):
|
||||
return current_scene
|
||||
var root: Window = tree.root
|
||||
var scope: Node = owner_node
|
||||
while scope.get_parent() != null and scope.get_parent() != root:
|
||||
scope = scope.get_parent()
|
||||
return scope
|
||||
|
||||
|
||||
static func _forget_pool(scope_id: int, pool: BakePool) -> void:
|
||||
var pool_ref: WeakRef = _pool_refs.get(scope_id) as WeakRef
|
||||
if pool_ref != null and pool_ref.get_ref() == pool:
|
||||
_pool_refs.erase(scope_id)
|
||||
|
||||
|
||||
static func _make_cache_key(rect: Rect2, pixel_scale: Vector2, wall: bool, finish: StringName, tint: Color, detail_view_size: Vector2) -> String:
|
||||
return var_to_str([wall, finish, tint, rect.position.x, rect.position.y, rect.size.x, rect.size.y, pixel_scale.x, pixel_scale.y, detail_view_size.x, detail_view_size.y])
|
||||
|
||||
|
||||
static func _make_pattern_cache_key(wall: bool, finish: StringName, tint: Color, period: Vector2) -> String:
|
||||
return var_to_str([&"periodic_style", PATTERN_CACHE_VERSION, wall, finish, tint, period.x, period.y, PATTERN_PIXEL_SCALE, &"mipmaps"])
|
||||
|
||||
|
||||
static func _snap_canonical_position(position: Vector2, pixel_scale: Vector2) -> Vector2:
|
||||
var scale_x: float = maxf(0.001, pixel_scale.x)
|
||||
var scale_y: float = maxf(0.001, pixel_scale.y)
|
||||
return Vector2(roundf(position.x * scale_x) / scale_x, roundf(position.y * scale_y) / scale_y)
|
||||
|
||||
|
||||
static func get_stats(owner_node: Node = null) -> Dictionary:
|
||||
if owner_node != null and is_instance_valid(owner_node):
|
||||
var scope: Node = _get_scope_node(owner_node)
|
||||
if scope != null:
|
||||
var pool_ref: WeakRef = _pool_refs.get(scope.get_instance_id()) as WeakRef
|
||||
var pool: BakePool = pool_ref.get_ref() as BakePool if pool_ref != null else null
|
||||
return pool._stats() if pool != null else _empty_stats()
|
||||
var totals: Dictionary = _empty_stats()
|
||||
for scope_id: Variant in _pool_refs.keys():
|
||||
var pool: BakePool = _pool_refs[scope_id].get_ref() as BakePool
|
||||
if pool == null or not is_instance_valid(pool):
|
||||
continue
|
||||
var stats: Dictionary = pool._stats()
|
||||
for key: String in totals.keys():
|
||||
if key == "active_build":
|
||||
if not str(stats[key]).is_empty():
|
||||
totals[key] = stats[key]
|
||||
elif key == "last_build_usec" or key == "vector_submission_usec" or key == "budget_pressure_frame":
|
||||
totals[key] = maxi(int(totals[key]), int(stats[key]))
|
||||
else:
|
||||
totals[key] = int(totals[key]) + int(stats[key])
|
||||
return totals
|
||||
|
||||
|
||||
static func _empty_stats() -> Dictionary:
|
||||
return {
|
||||
"bytes": 0,
|
||||
"resident_bytes": 0,
|
||||
"retired_bytes": 0,
|
||||
"entries": 0,
|
||||
"queued": 0,
|
||||
"bakers": 0,
|
||||
"live_vector_bakers": 0,
|
||||
"live_vector_items": 0,
|
||||
"builds": 0,
|
||||
"cache_hits": 0,
|
||||
"evictions": 0,
|
||||
"refusals": 0,
|
||||
"budget_pressure": 0,
|
||||
"budget_pressure_frame": -1,
|
||||
"budget_blocked_requests": 0,
|
||||
"waiting_handles": 0,
|
||||
"readbacks": 0,
|
||||
"pattern_builds": 0,
|
||||
"pattern_readbacks": 0,
|
||||
"pattern_resident_bytes": 0,
|
||||
"pattern_retired_bytes": 0,
|
||||
"last_build_usec": 0,
|
||||
"vector_submission_usec": 0,
|
||||
"active_build": ""
|
||||
}
|
||||
|
||||
|
||||
static func texture_size_for(local_size: Vector2, pixel_scale: Vector2) -> Vector2i:
|
||||
var safe_size := Vector2(maxf(1.0, local_size.x), maxf(1.0, local_size.y))
|
||||
var safe_scale := Vector2(maxf(0.001, pixel_scale.x), maxf(0.001, pixel_scale.y))
|
||||
var pixels: Vector2 = safe_size * safe_scale
|
||||
var reduction: float = maxf(1.0, maxf(pixels.x, pixels.y) / float(MAX_TEXTURE_EDGE))
|
||||
reduction = maxf(reduction, sqrt(pixels.x * pixels.y / MAX_TEXTURE_PIXELS))
|
||||
return Vector2i(maxi(1, floori(pixels.x / reduction)), maxi(1, floori(pixels.y / reduction)))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwcf7h5yf3xi0
|
||||
@@ -7,6 +7,8 @@ const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_ren
|
||||
var surface_kind: StringName = &"wall"
|
||||
var finish_id: StringName = &"finish_wall_paint"
|
||||
var tint: Color = Color("#5A9D70")
|
||||
var corner_radius: float = 0.0
|
||||
var outside_color: Color = Color("#202633FA")
|
||||
|
||||
|
||||
func configure(kind: StringName, finish: StringName, color: Color) -> void:
|
||||
@@ -17,6 +19,12 @@ func configure(kind: StringName, finish: StringName, color: Color) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_rounded_frame(radius: float, background: Color) -> void:
|
||||
corner_radius = maxf(0.0, radius)
|
||||
outside_color = background
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect: Rect2 = Rect2(Vector2(3.0, 3.0), size - Vector2(6.0, 6.0))
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
@@ -25,4 +33,49 @@ func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
draw_rect(rect, Color("#F5F7FA"), false, 2.0)
|
||||
var radius: float = minf(corner_radius, minf(rect.size.x, rect.size.y) * 0.5)
|
||||
if radius > 0.0:
|
||||
var wall: bool = surface_kind != &"background_ground"
|
||||
var base_color: Color = SURFACE_RENDERER.surface_base_color(finish_id, tint, wall)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 0)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 1)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 2)
|
||||
_draw_rounded_corner(rect, radius, base_color, outside_color, 3)
|
||||
var outline := StyleBoxFlat.new()
|
||||
outline.bg_color = Color.TRANSPARENT
|
||||
outline.draw_center = false
|
||||
outline.border_color = Color("#F5F7FA")
|
||||
outline.set_border_width_all(2)
|
||||
outline.set_corner_radius_all(roundi(radius))
|
||||
draw_style_box(outline, rect)
|
||||
else:
|
||||
draw_rect(rect, Color("#F5F7FA"), false, 2.0)
|
||||
|
||||
|
||||
func _draw_rounded_corner(rect: Rect2, radius: float, base_color: Color, background: Color, corner: int) -> void:
|
||||
var square := Rect2()
|
||||
var center := Vector2.ZERO
|
||||
var start_angle: float = 0.0
|
||||
match corner:
|
||||
0:
|
||||
square = Rect2(rect.position, Vector2.ONE * radius)
|
||||
center = rect.position + Vector2.ONE * radius
|
||||
start_angle = PI
|
||||
1:
|
||||
square = Rect2(Vector2(rect.end.x - radius, rect.position.y), Vector2.ONE * radius)
|
||||
center = Vector2(rect.end.x - radius, rect.position.y + radius)
|
||||
start_angle = -PI * 0.5
|
||||
2:
|
||||
square = Rect2(rect.end - Vector2.ONE * radius, Vector2.ONE * radius)
|
||||
center = rect.end - Vector2.ONE * radius
|
||||
start_angle = 0.0
|
||||
_:
|
||||
square = Rect2(Vector2(rect.position.x, rect.end.y - radius), Vector2.ONE * radius)
|
||||
center = Vector2(rect.position.x + radius, rect.end.y - radius)
|
||||
start_angle = PI * 0.5
|
||||
draw_rect(square, background, true)
|
||||
var arc := PackedVector2Array([center])
|
||||
for point_index: int in 13:
|
||||
var angle: float = start_angle + PI * 0.5 * float(point_index) / 12.0
|
||||
arc.append(center + Vector2(cos(angle), sin(angle)) * radius)
|
||||
draw_colored_polygon(arc, base_color)
|
||||
|
||||
@@ -13,12 +13,37 @@ const WALL_BRICK_WIDTH: float = 100.0
|
||||
const WALL_BRICK_HEIGHT: float = 38.0
|
||||
const MAX_WALL_BRICK_CELLS: int = 1200
|
||||
const MAX_BACKGROUND_CELLS: int = 12000
|
||||
## Finite material variations repeat independently of wall size and camera travel.
|
||||
## Even row periods preserve staggered brick/plank courses across the seam.
|
||||
const BRICK_PATTERN_CELLS := Vector2i(8, 8)
|
||||
const WOOD_PATTERN_CELLS := Vector2i(3, 6)
|
||||
const GRASS_PATTERN_CELLS := Vector2i(16, 16)
|
||||
const CONCRETE_PATTERN_CELLS := Vector2i(4, 4)
|
||||
const CARPET_PATTERN_CELLS := Vector2i(16, 16)
|
||||
## Conservative work estimate to avoid an expensive first placement redraw.
|
||||
## Runtime timing in StageRectSurface also catches slower machines.
|
||||
const PREVIEW_WORK_BUDGET: float = 6000.0
|
||||
const MAX_PREVIEW_TEXTURE_CELLS: int = 128
|
||||
|
||||
|
||||
static func is_wall_finish(id: StringName) -> bool:
|
||||
return WALL_FINISHES.has(id)
|
||||
|
||||
|
||||
static func pattern_size_for(finish: StringName) -> Vector2:
|
||||
match finish:
|
||||
&"finish_wall_brick": return Vector2(BRICK_PATTERN_CELLS) * Vector2(WALL_BRICK_WIDTH, WALL_BRICK_HEIGHT)
|
||||
&"finish_wall_wood": return Vector2(WOOD_PATTERN_CELLS) * Vector2(320.0, 64.0)
|
||||
&"finish_background_grass": return Vector2(GRASS_PATTERN_CELLS) * Vector2(44.0, 36.0)
|
||||
&"finish_background_concrete": return Vector2(CONCRETE_PATTERN_CELLS) * Vector2(280.0, 190.0)
|
||||
&"finish_background_carpet": return Vector2(CARPET_PATTERN_CELLS) * Vector2(34.0, 26.0)
|
||||
_: return Vector2.ZERO
|
||||
|
||||
|
||||
static func _pattern_seed(row: int, column: int, cells: Vector2i, row_factor: int, column_factor: int, offset: int = 0) -> int:
|
||||
return posmod(row, cells.y) * row_factor + posmod(column, cells.x) * column_factor + offset
|
||||
|
||||
|
||||
static func is_background_finish(id: StringName) -> bool:
|
||||
return BACKGROUND_FINISHES.has(id)
|
||||
|
||||
@@ -38,6 +63,28 @@ static func is_ground_finish(id: StringName) -> bool:
|
||||
return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy"
|
||||
|
||||
|
||||
static func surface_base_color(finish: StringName, tint: Color, wall: bool) -> Color:
|
||||
if finish == &"finish_ground_legacy":
|
||||
return Color("#468B42")
|
||||
if finish.ends_with("brick"):
|
||||
return Color("#D2CBAF") if wall else Color("#994D35")
|
||||
if finish.ends_with("wood"):
|
||||
return Color("#9A673C") if wall else Color("#B88951")
|
||||
if finish == &"finish_ground_grass" or finish == &"finish_background_grass":
|
||||
return Color("#72A84C")
|
||||
if finish == &"finish_background_concrete":
|
||||
return Color("#A6AAA3")
|
||||
if finish == &"finish_background_carpet":
|
||||
return tint
|
||||
if finish == &"finish_ground_dirt":
|
||||
return Color("#8E633E")
|
||||
if finish == &"finish_ground_sidewalk":
|
||||
return Color("#B7B9B2")
|
||||
if finish == &"finish_ground_asphalt":
|
||||
return Color("#505A62")
|
||||
return tint
|
||||
|
||||
|
||||
static func draw_wall(
|
||||
canvas: CanvasItem,
|
||||
rect: Rect2,
|
||||
@@ -45,9 +92,12 @@ static func draw_wall(
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE,
|
||||
cull_to_viewport: bool = true
|
||||
cull_to_viewport: bool = true,
|
||||
interactive_preview: bool = false,
|
||||
force_simple_preview: bool = false,
|
||||
detail_view_size: Vector2 = Vector2.ZERO
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport)
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport, interactive_preview, force_simple_preview, detail_view_size)
|
||||
|
||||
|
||||
static func draw_ground(
|
||||
@@ -68,9 +118,11 @@ static func draw_background(
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE,
|
||||
cull_to_viewport: bool = true
|
||||
cull_to_viewport: bool = true,
|
||||
interactive_preview: bool = false,
|
||||
force_simple_preview: bool = false
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport)
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport, interactive_preview, force_simple_preview)
|
||||
|
||||
|
||||
static func _draw_surface(
|
||||
@@ -82,66 +134,47 @@ static func _draw_surface(
|
||||
drawing_scale: Vector2,
|
||||
wall: bool,
|
||||
background: bool,
|
||||
cull_to_viewport: bool
|
||||
cull_to_viewport: bool,
|
||||
interactive_preview: bool = false,
|
||||
force_simple_preview: bool = false,
|
||||
detail_view_size: Vector2 = Vector2.ZERO
|
||||
) -> void:
|
||||
var normalized := rect.abs()
|
||||
if normalized.size.x <= 0.0 or normalized.size.y <= 0.0:
|
||||
return
|
||||
var motif_scale: Vector2 = _normalized_scale(drawing_scale)
|
||||
var base: Color = tint
|
||||
if finish == &"finish_ground_legacy":
|
||||
base = Color("#468B42")
|
||||
elif finish.ends_with("brick"):
|
||||
base = Color("#D2CBAF") if wall else Color("#994D35")
|
||||
elif finish.ends_with("wood"):
|
||||
base = Color("#9A673C") if wall else Color("#B88951")
|
||||
elif finish == &"finish_ground_grass" or finish == &"finish_background_grass":
|
||||
base = Color("#72A84C")
|
||||
elif finish == &"finish_background_concrete":
|
||||
base = Color("#A6AAA3")
|
||||
elif finish == &"finish_background_carpet":
|
||||
base = tint
|
||||
elif finish == &"finish_ground_dirt":
|
||||
base = Color("#8E633E")
|
||||
elif finish == &"finish_ground_sidewalk":
|
||||
base = Color("#B7B9B2")
|
||||
elif finish == &"finish_ground_asphalt":
|
||||
base = Color("#505A62")
|
||||
var base: Color = surface_base_color(finish, tint, wall)
|
||||
canvas.draw_rect(normalized, base, true)
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
var simplified: bool = interactive_preview and (force_simple_preview or needs_light_preview(finish, pattern_rect.size, motif_scale))
|
||||
if finish.ends_with("brick"):
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
var detail_size: Vector2 = view_rect.size if not view_rect.size.is_zero_approx() else normalized.size
|
||||
if detail_view_size.x > 0.0 and detail_view_size.y > 0.0:
|
||||
detail_size = detail_view_size
|
||||
var detail_scale: float = brick_detail_scale_for_view(detail_size, motif_scale, wall)
|
||||
if wall:
|
||||
_draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint)
|
||||
_draw_wall_bricks(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint, simplified)
|
||||
else:
|
||||
_draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale)
|
||||
elif finish.ends_with("wood"):
|
||||
if wall:
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
var detail_scale: float = wood_detail_scale_for_view(pattern_rect.size, motif_scale)
|
||||
_draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint)
|
||||
var detail_size: Vector2 = detail_view_size if detail_view_size.x > 0.0 and detail_view_size.y > 0.0 else pattern_rect.size
|
||||
var detail_scale: float = wood_detail_scale_for_view(detail_size, motif_scale)
|
||||
_draw_wall_wood(canvas, pattern_rect, world_origin, motif_scale, detail_scale, tint, simplified)
|
||||
else:
|
||||
_draw_wood(canvas, normalized, world_origin, base, motif_scale)
|
||||
elif finish == &"finish_background_grass":
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
_draw_background_grass(canvas, pattern_rect, world_origin, motif_scale)
|
||||
_draw_background_grass(canvas, pattern_rect, world_origin, motif_scale, simplified)
|
||||
elif finish == &"finish_background_concrete":
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
_draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale)
|
||||
_draw_background_concrete(canvas, pattern_rect, world_origin, motif_scale, simplified)
|
||||
elif finish == &"finish_background_carpet":
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas) if cull_to_viewport else normalized
|
||||
var pattern_rect: Rect2 = normalized.intersection(view_rect) if not view_rect.size.is_zero_approx() else normalized
|
||||
if pattern_rect.size.x > 0.0 and pattern_rect.size.y > 0.0:
|
||||
_draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint)
|
||||
_draw_background_carpet(canvas, pattern_rect, world_origin, motif_scale, tint, simplified)
|
||||
elif finish == &"finish_ground_grass":
|
||||
_draw_grass(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish == &"finish_ground_dirt":
|
||||
@@ -154,6 +187,39 @@ static func _draw_surface(
|
||||
push_warning("StageSurfaceRenderer: unknown finish '%s'; drawing paint." % String(finish))
|
||||
|
||||
|
||||
static func needs_light_preview(finish: StringName, visible_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> bool:
|
||||
if visible_size.x <= 0.0 or visible_size.y <= 0.0:
|
||||
return false
|
||||
var cell_size: Vector2
|
||||
var work_per_cell: float
|
||||
match finish:
|
||||
&"finish_wall_brick":
|
||||
cell_size = Vector2(WALL_BRICK_WIDTH, WALL_BRICK_HEIGHT)
|
||||
work_per_cell = 240.0
|
||||
&"finish_wall_wood":
|
||||
cell_size = Vector2(320.0, 64.0)
|
||||
work_per_cell = 750.0
|
||||
&"finish_background_grass":
|
||||
cell_size = Vector2(44.0, 36.0)
|
||||
work_per_cell = 70.0
|
||||
&"finish_background_concrete":
|
||||
cell_size = Vector2(280.0, 190.0)
|
||||
work_per_cell = 20.0
|
||||
&"finish_background_carpet":
|
||||
cell_size = Vector2(34.0, 26.0)
|
||||
work_per_cell = 35.0
|
||||
_:
|
||||
return false
|
||||
var world_size: Vector2 = visible_size * _normalized_scale(motif_scale)
|
||||
var cells: float = (ceil(world_size.x / cell_size.x) + 1.0) * (ceil(world_size.y / cell_size.y) + 1.0)
|
||||
return cells * work_per_cell > PREVIEW_WORK_BUDGET
|
||||
|
||||
|
||||
static func _preview_texture_stride(size: Vector2, motif_scale: Vector2, cell_size: Vector2) -> int:
|
||||
var cells: Vector2 = size * motif_scale / cell_size + Vector2(3.0, 3.0)
|
||||
return maxi(1, ceili(sqrt(cells.x * cells.y / float(MAX_PREVIEW_TEXTURE_CELLS))))
|
||||
|
||||
|
||||
static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE, wall: bool = false) -> float:
|
||||
var scale: float = 1.0
|
||||
var safe_size: Vector2 = Vector2(maxf(1.0, view_size.x), maxf(1.0, view_size.y))
|
||||
@@ -185,7 +251,7 @@ static func wood_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2
|
||||
return scale
|
||||
|
||||
|
||||
static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
|
||||
static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color, simplified: bool = false) -> void:
|
||||
const BOARD_HEIGHT: float = 64.0
|
||||
const PLANK_WIDTH: float = 320.0
|
||||
var safe_scale: Vector2 = _normalized_scale(motif_scale)
|
||||
@@ -234,14 +300,15 @@ static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, mo
|
||||
Vector2(plank_local_left, board_local_top + board_world_height / safe_scale.y),
|
||||
_color_with_alpha(seam_color, 0.88), 2.0 / safe_scale.x
|
||||
)
|
||||
_draw_wall_wood_grain(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural
|
||||
)
|
||||
_draw_wall_wood_knot(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, detail_scale, tint, natural
|
||||
)
|
||||
if not simplified:
|
||||
_draw_wall_wood_grain(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, grain_color, highlight_color, detail_scale, tint, natural
|
||||
)
|
||||
_draw_wall_wood_knot(
|
||||
canvas, rect, plank_world_left, board_world_top, plank_world_width, board_world_height,
|
||||
origin, safe_scale, row, column, detail_scale, tint, natural
|
||||
)
|
||||
_draw_clipped_line(
|
||||
canvas, rect,
|
||||
Vector2(rect.position.x, board_local_top), Vector2(rect.end.x, board_local_top),
|
||||
@@ -274,7 +341,7 @@ static func _draw_wall_wood_grain(
|
||||
) -> void:
|
||||
const TRACK_COUNT: int = 5
|
||||
const SAMPLES: int = 32
|
||||
var seed: int = row * 73856093 + column * 19349663
|
||||
var seed: int = _pattern_seed(row, column, WOOD_PATTERN_CELLS, 73856093, 19349663)
|
||||
for track: int in TRACK_COUNT:
|
||||
var track_seed: int = seed + track * 83492791
|
||||
var lane: float = 0.16 + _wood_hash(track_seed) * 0.68
|
||||
@@ -312,7 +379,7 @@ static func _draw_wall_wood_knot(
|
||||
tint: Color,
|
||||
natural: Color
|
||||
) -> void:
|
||||
var seed: int = row * 92821 + column * 68917 + 31
|
||||
var seed: int = _pattern_seed(row, column, WOOD_PATTERN_CELLS, 92821, 68917, 31)
|
||||
if _wood_hash(seed) > 0.24:
|
||||
return
|
||||
var center_world := Vector2(
|
||||
@@ -376,7 +443,7 @@ static func _material_color(source: Color, selected: Color, natural: Color) -> C
|
||||
return Color.from_hsv(selected.h, saturation, value, source.a)
|
||||
|
||||
|
||||
static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, simplified: bool = false) -> void:
|
||||
const CELL_WIDTH: float = 44.0
|
||||
const CELL_HEIGHT: float = 36.0
|
||||
var world_left: float = rect.position.x * motif_scale.x + origin.x
|
||||
@@ -388,12 +455,16 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect
|
||||
var first_row: int = floori(world_top / CELL_HEIGHT) - 1
|
||||
var last_row: int = floori(world_bottom / CELL_HEIGHT) + 1
|
||||
var remaining: int = MAX_BACKGROUND_CELLS
|
||||
for row: int in range(first_row, last_row + 1):
|
||||
for column: int in range(first_column, last_column + 1):
|
||||
var stride: int = _preview_texture_stride(rect.size, motif_scale, Vector2(CELL_WIDTH, CELL_HEIGHT)) if simplified else 1
|
||||
# Sample the same world cells at a lower density, including in negative coordinates.
|
||||
first_row = floori(float(first_row) / float(stride)) * stride
|
||||
first_column = floori(float(first_column) / float(stride)) * stride
|
||||
for row: int in range(first_row, last_row + 1, stride):
|
||||
for column: int in range(first_column, last_column + 1, stride):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = row * 73856093 + column * 19349663 + 197
|
||||
var seed: int = _pattern_seed(row, column, GRASS_PATTERN_CELLS, 73856093, 19349663, 197)
|
||||
var center_world := Vector2((float(column) + 0.5) * CELL_WIDTH, (float(row) + 0.5) * CELL_HEIGHT)
|
||||
center_world += Vector2((_wood_hash(seed) - 0.5) * 34.0, (_wood_hash(seed + 1) - 0.5) * 26.0)
|
||||
var center := Vector2((center_world.x - origin.x) / motif_scale.x, (center_world.y - origin.y) / motif_scale.y)
|
||||
@@ -403,7 +474,7 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect
|
||||
var highlight: Color = Color("#C3DA74").darkened(_wood_hash(seed + 3) * 0.24)
|
||||
var radius: float = 1.8 + _wood_hash(seed + 4) * 1.6
|
||||
canvas.draw_circle(center, radius, _color_with_alpha(tuft_color, 0.56))
|
||||
for blade: int in 4:
|
||||
for blade: int in (1 if simplified else 4):
|
||||
var blade_seed: int = seed + blade * 47 + 11
|
||||
var x_offset: float = (_wood_hash(blade_seed) - 0.5) * 9.0 / motif_scale.x
|
||||
var y_offset: float = (_wood_hash(blade_seed + 1) - 0.5) * 7.0 / motif_scale.y
|
||||
@@ -413,7 +484,7 @@ static func _draw_background_grass(canvas: CanvasItem, rect: Rect2, origin: Vect
|
||||
_draw_clipped_line(canvas, rect, start, tip, _color_with_alpha(highlight, 0.78), 1.25 / motif_scale.y)
|
||||
|
||||
|
||||
static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2) -> void:
|
||||
static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, simplified: bool = false) -> void:
|
||||
const SLAB_WIDTH: float = 280.0
|
||||
const SLAB_HEIGHT: float = 190.0
|
||||
var world_left: float = rect.position.x * motif_scale.x + origin.x
|
||||
@@ -431,11 +502,13 @@ static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: V
|
||||
var x: float = float(column) * SLAB_WIDTH
|
||||
var local_x: float = (x - origin.x) / motif_scale.x
|
||||
_draw_clipped_line(canvas, rect, Vector2(local_x, rect.position.y), Vector2(local_x, rect.end.y), Color("#68757A", 0.32), column_gap)
|
||||
if simplified:
|
||||
continue
|
||||
for row: int in range(first_row, mini(last_row + 1, first_row + MAX_BACKGROUND_CELLS)):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = column * 92821 + row * 68917 + 51
|
||||
var seed: int = _pattern_seed(row, column, CONCRETE_PATTERN_CELLS, 68917, 92821, 51)
|
||||
var world_point := Vector2(x + 16.0 + _wood_hash(seed) * (SLAB_WIDTH - 32.0), float(row) * SLAB_HEIGHT + 14.0 + _wood_hash(seed + 1) * (SLAB_HEIGHT - 28.0))
|
||||
var point := Vector2((world_point.x - origin.x) / motif_scale.x, (world_point.y - origin.y) / motif_scale.y)
|
||||
if rect.grow(3.0).has_point(point):
|
||||
@@ -447,25 +520,28 @@ static func _draw_background_concrete(canvas: CanvasItem, rect: Rect2, origin: V
|
||||
_draw_clipped_line(canvas, rect, Vector2(rect.position.x, local_y), Vector2(rect.end.x, local_y), Color("#68757A", 0.32), row_gap)
|
||||
|
||||
|
||||
static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color) -> void:
|
||||
static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, tint: Color, simplified: bool = false) -> void:
|
||||
const CELL_WIDTH: float = 34.0
|
||||
const CELL_HEIGHT: float = 26.0
|
||||
var world_left: float = rect.position.x * motif_scale.x + origin.x
|
||||
var world_right: float = rect.end.x * motif_scale.x + origin.x
|
||||
var world_top: float = rect.position.y * motif_scale.y + origin.y
|
||||
var world_bottom: float = rect.end.y * motif_scale.y + origin.y
|
||||
var first_column: int = floori(world_left / CELL_WIDTH)
|
||||
var first_column: int = floori(world_left / CELL_WIDTH) - 1
|
||||
var last_column: int = floori(world_right / CELL_WIDTH)
|
||||
var first_row: int = floori(world_top / CELL_HEIGHT)
|
||||
var last_row: int = floori(world_bottom / CELL_HEIGHT)
|
||||
var remaining: int = MAX_BACKGROUND_CELLS
|
||||
var natural: Color = default_tint(&"finish_background_carpet")
|
||||
for row: int in range(first_row, last_row + 1):
|
||||
for column: int in range(first_column, last_column + 1):
|
||||
var stride: int = _preview_texture_stride(rect.size, motif_scale, Vector2(CELL_WIDTH, CELL_HEIGHT)) if simplified else 1
|
||||
first_row = floori(float(first_row) / float(stride)) * stride
|
||||
first_column = floori(float(first_column) / float(stride)) * stride
|
||||
for row: int in range(first_row, last_row + 1, stride):
|
||||
for column: int in range(first_column, last_column + 1, stride):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = row * 92821 + column * 68917 + 71
|
||||
var seed: int = _pattern_seed(row, column, CARPET_PATTERN_CELLS, 92821, 68917, 71)
|
||||
var cell_left: float = float(column) * CELL_WIDTH
|
||||
var cell_top: float = float(row) * CELL_HEIGHT
|
||||
for fiber: int in 2:
|
||||
@@ -479,7 +555,7 @@ static func _draw_background_carpet(canvas: CanvasItem, rect: Rect2, origin: Vec
|
||||
_draw_clipped_line(canvas, rect, start, finish, _color_with_alpha(fiber_color, 0.42), 0.85 / motif_scale.y)
|
||||
|
||||
|
||||
static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
|
||||
static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color, interactive_preview: bool = false) -> void:
|
||||
var brick_world_w: float = WALL_BRICK_WIDTH * detail_scale
|
||||
var brick_world_h: float = WALL_BRICK_HEIGHT * detail_scale
|
||||
var brick_w: float = brick_world_w / motif_scale.x
|
||||
@@ -508,7 +584,7 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2,
|
||||
var column_count: int = int(ceil(rect.size.x * motif_scale.x / brick_world_w)) + 2
|
||||
for column_offset: int in column_count:
|
||||
var column: int = first_column + column_offset
|
||||
var seed: int = row * 92821 + column * 68917 + 31
|
||||
var seed: int = _pattern_seed(row, column, BRICK_PATTERN_CELLS, 92821, 68917, 31)
|
||||
var brick_world_left: float = float(column) * brick_world_w + stagger_world
|
||||
var brick_local_left: float = (brick_world_left - origin.x) / motif_scale.x
|
||||
var width_variation: float = 0.92 + _wood_hash(seed + 1) * 0.14
|
||||
@@ -526,6 +602,15 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2,
|
||||
continue
|
||||
if face_rect.end.y < rect.position.y or face_rect.position.y > rect.end.y:
|
||||
continue
|
||||
var color_index: int = int(_wood_hash(seed + 5) * float(palette.size()))
|
||||
var face_color: Color = palette[color_index]
|
||||
if interactive_preview:
|
||||
# Keep the committed pattern's world phase, dimensions, jitter, and palette.
|
||||
# Fine outlines and texture are expensive to regenerate on every drag frame.
|
||||
var visible_face: Rect2 = face_rect.intersection(rect)
|
||||
if visible_face.size.x > 0.0 and visible_face.size.y > 0.0:
|
||||
canvas.draw_rect(visible_face, face_color)
|
||||
continue
|
||||
var roughness := Vector2(1.7 * detail_scale / motif_scale.x, 1.2 * detail_scale / motif_scale.y)
|
||||
var outline: PackedVector2Array = _rough_brick_outline(face_rect, seed, roughness)
|
||||
var shadow_offset := Vector2(1.3 * detail_scale / motif_scale.x, 1.8 * detail_scale / motif_scale.y)
|
||||
@@ -535,8 +620,6 @@ static func _draw_wall_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2,
|
||||
var face_points: PackedVector2Array = _clip_polygon_to_rect(outline, rect)
|
||||
if face_points.size() < 3:
|
||||
continue
|
||||
var color_index: int = int(_wood_hash(seed + 5) * float(palette.size()))
|
||||
var face_color: Color = palette[color_index]
|
||||
canvas.draw_colored_polygon(face_points, face_color)
|
||||
var closed_outline := face_points.duplicate()
|
||||
closed_outline.append(face_points[0])
|
||||
@@ -628,12 +711,34 @@ static func _offset_polygon(points: PackedVector2Array, offset: Vector2) -> Pack
|
||||
|
||||
|
||||
static func _clip_polygon_to_rect(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
var fully_inside: bool = true
|
||||
for point: Vector2 in points:
|
||||
if not rect.has_point(point):
|
||||
fully_inside = false
|
||||
break
|
||||
if fully_inside:
|
||||
return points
|
||||
var clipped := points
|
||||
for edge: int in 4:
|
||||
clipped = _clip_polygon_to_edge(clipped, rect, edge)
|
||||
if clipped.is_empty():
|
||||
return clipped
|
||||
return clipped
|
||||
# Clipping at a corner can produce repeated vertices or a zero-area sliver.
|
||||
# These cannot be triangulated, even when the array still has three entries.
|
||||
var cleaned := PackedVector2Array()
|
||||
for point: Vector2 in clipped:
|
||||
if cleaned.is_empty() or cleaned[cleaned.size() - 1].distance_squared_to(point) > 0.000001:
|
||||
cleaned.append(point)
|
||||
if cleaned.size() > 1 and cleaned[0].distance_squared_to(cleaned[cleaned.size() - 1]) <= 0.000001:
|
||||
cleaned.remove_at(cleaned.size() - 1)
|
||||
if cleaned.size() < 3:
|
||||
return PackedVector2Array()
|
||||
var area: float = 0.0
|
||||
for index: int in range(1, cleaned.size() - 1):
|
||||
area += (cleaned[index] - cleaned[0]).cross(cleaned[index + 1] - cleaned[0])
|
||||
if absf(area) <= 0.001 or Geometry2D.triangulate_polygon(cleaned).is_empty():
|
||||
return PackedVector2Array()
|
||||
return cleaned
|
||||
|
||||
|
||||
static func _clip_polygon_to_edge(points: PackedVector2Array, rect: Rect2, edge: int) -> PackedVector2Array:
|
||||
|
||||
@@ -2,7 +2,6 @@ class_name StageWallPanel
|
||||
extends "res://scripts/stage/scenery/stage_rect_surface.gd"
|
||||
## Finite decorative wall panel with world-scale finish motifs.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
## Saved source-wall anchor offset, retained to preserve erased-fragment metadata.
|
||||
## Motif drawing uses the fragment's current world origin so its brick courses stay aligned.
|
||||
@@ -23,4 +22,4 @@ func get_surface_draw_origin_world() -> Vector2:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
|
||||
_draw_surface(true)
|
||||
|
||||
@@ -307,6 +307,8 @@ static func _validate_root(data: Dictionary) -> Dictionary:
|
||||
return {"ok": false, "error": "Background ground size or finish is invalid."}
|
||||
if not _valid_color(entry.get("color", "")):
|
||||
return {"ok": false, "error": "Background ground color is invalid."}
|
||||
if entry.has("motif_origin_offset") and not is_vector_data(entry.get("motif_origin_offset")):
|
||||
return {"ok": false, "error": "Background ground motif origin is invalid."}
|
||||
elif String(entry.get("type", "")) == "scenery_artwork":
|
||||
var asset_id: Variant = entry.get("asset_id", "")
|
||||
if not asset_id is String or String(asset_id).strip_edges().is_empty():
|
||||
|
||||
Reference in New Issue
Block a user