Add Stage Surface Color Dialog and Background Ground Functionality
- Implemented StageSurfaceColorDialog for custom color selection with live preview. - Created StageBackgroundGround class for non-walkable scenery behind the set. - Introduced StageRectSurface for shared rectangular geometry and tint preview. - Added StageSurfacePreview for rendering surface previews. - Developed comprehensive tests for surface color selection, rendering, and serialization.
This commit is contained in:
@@ -53,9 +53,15 @@ const STAGE_SCENERY_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/st
|
||||
const STAGE_SKY_VIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_sky_view.gd")
|
||||
const STAGE_DRAW_ORDER_SCRIPT := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
const STAGE_WALL_PANEL_SCRIPT := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const STAGE_BACKGROUND_GROUND_SCRIPT := preload("res://scripts/stage/scenery/stage_background_ground.gd")
|
||||
const STAGE_RECT_SURFACE_SCRIPT := preload("res://scripts/stage/scenery/stage_rect_surface.gd")
|
||||
const STAGE_SCENERY_ARTWORK_SCRIPT := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
const STAGE_SURFACE_RENDERER_SCRIPT := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const STAGE_INSPECTOR_PANEL_SCRIPT := preload("res://scripts/shared/stage_ui/stage_inspector_panel.gd")
|
||||
const STAGE_PANEL_CHOICE_GROUP_SCRIPT := preload("res://scripts/shared/stage_ui/stage_panel_choice_group.gd")
|
||||
const STAGE_SURFACE_COLOR_CONTROLS_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd")
|
||||
const STAGE_SURFACE_COLOR_DIALOG_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd")
|
||||
const STAGE_SURFACE_PREVIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
|
||||
const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||
|
||||
# Phase 3c editor tools.
|
||||
@@ -125,6 +131,7 @@ const SPEECH_DRAFT_DEBOUNCE: float = STAGE_SPEECH_SESSION_SCRIPT.DRAFT_DEBOUNCE
|
||||
const SPEECH_DRAFT_SCHEMA: int = STAGE_SPEECH_SESSION_SCRIPT.DRAFT_SCHEMA
|
||||
const SPEECH_BUBBLE_DRAG_THRESHOLD: float = STAGE_SPEECH_SESSION_SCRIPT.BUBBLE_DRAG_THRESHOLD
|
||||
const RECENT_SCENE_LIMIT: int = STAGE_DOCUMENT_CONTROLLER_SCRIPT.RECENT_LIMIT
|
||||
const RECENT_SURFACE_COLOR_LIMIT: int = 8
|
||||
const DEFAULT_SCENE_NAME: String = "Untitled"
|
||||
const DEFAULT_CAMERA_POSITION: Vector2 = Vector2(0.0, -400.0)
|
||||
const DEFAULT_CAMERA_ZOOM: float = 0.5
|
||||
@@ -248,6 +255,7 @@ var _suppressed_contact_pairs: Dictionary:
|
||||
var _spawner: StageSpawner
|
||||
var _selection: StageSelection
|
||||
var _gizmos: StageGizmos
|
||||
var _stage_input_ready: bool = false
|
||||
var _grid: StageGrid
|
||||
var _spatial_index: StageSpatialIndex
|
||||
var _terrain_painter: StageTerrainPainter
|
||||
@@ -268,6 +276,13 @@ var _scenery_group_start: Vector2 = Vector2.ZERO
|
||||
var _scenery_placed_count: int = 0
|
||||
var _scenery_inspector: StageInspectorPanel
|
||||
var _scenery_repair_message: Label
|
||||
var _scenery_surface_section: VBoxContainer
|
||||
var _scenery_wall_look_heading: Label
|
||||
var _scenery_wall_look_group: StagePanelChoiceGroup
|
||||
var _scenery_background_ground_look_heading: Label
|
||||
var _scenery_background_ground_look_group: StagePanelChoiceGroup
|
||||
var _scenery_surface_helper: Label
|
||||
var _scenery_surface_colors: StageSurfaceColorControls
|
||||
var _scenery_depth_section: VBoxContainer
|
||||
var _scenery_depth_heading: Label
|
||||
var _scenery_depth_group: StagePanelChoiceGroup
|
||||
@@ -276,6 +291,7 @@ 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_piece_button: Button
|
||||
var _scenery_find_button: Button
|
||||
var _scenery_choose_button: Button
|
||||
var _scenery_keep_button: Button
|
||||
@@ -284,6 +300,11 @@ 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_targets: Array[WeakRef] = []
|
||||
var _surface_color_dialog: StageSurfaceColorDialog
|
||||
var _surface_color_session: Dictionary = {}
|
||||
var _recent_surface_colors: Array[Color] = []
|
||||
var _group_preview_save_dialog: ConfirmationDialog
|
||||
var _pending_group_save_path: String = ""
|
||||
var _rule_engine: StageRuleEngine
|
||||
@@ -1330,6 +1351,7 @@ func _ready() -> void:
|
||||
_update_scene_chrome()
|
||||
_handle_cli_startup(OS.get_cmdline_user_args())
|
||||
_check_speech_recovery()
|
||||
_stage_input_ready = true
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -1400,7 +1422,7 @@ func _update_throw_preview_cache() -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _hosted_prop_editor != null:
|
||||
if _hosted_prop_editor != null or not _stage_input_ready:
|
||||
return
|
||||
if _speech_session != null:
|
||||
_speech_session.set_camera_panning(_panning)
|
||||
@@ -1530,6 +1552,17 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
_exit_scenery_edit_mode()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.keycode in [KEY_DELETE, KEY_BACKSPACE] and _scenery_edit_mode and current_mode == StageMode.EDIT:
|
||||
var selected_scenery: Array[Node2D] = _selection.get_selected() if _selection != null else []
|
||||
var has_surface: bool = false
|
||||
for selected_node: Node2D in selected_scenery:
|
||||
if selected_node.get_script() == STAGE_WALL_PANEL_SCRIPT or selected_node.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT:
|
||||
has_surface = true
|
||||
break
|
||||
if has_surface:
|
||||
_request_erase_selected_scenery()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
match event.keycode:
|
||||
KEY_DELETE, KEY_BACKSPACE:
|
||||
if current_mode == StageMode.EDIT:
|
||||
@@ -1795,6 +1828,7 @@ func set_mode(mode: StageMode) -> void:
|
||||
_show_toast("Finish or cancel speech editing first.")
|
||||
_sync_mode_buttons()
|
||||
return
|
||||
_cancel_surface_color_preview()
|
||||
if _scenery_controller != null:
|
||||
if _scenery_controller.is_group_preview_pending():
|
||||
_scenery_controller.cancel_group()
|
||||
@@ -2555,6 +2589,10 @@ func _validate_queues_for_save() -> String:
|
||||
|
||||
func _reset_scene_content() -> void:
|
||||
set_mode(StageMode.EDIT)
|
||||
_cancel_surface_color_preview()
|
||||
if _scenery_erase_piece_dialog != null:
|
||||
_scenery_erase_piece_dialog.hide()
|
||||
_clear_erase_scenery_targets()
|
||||
if _scenery_repair_confirm_dialog != null:
|
||||
_scenery_repair_confirm_dialog.hide()
|
||||
_cancel_scenery_repair_preview()
|
||||
@@ -3033,6 +3071,8 @@ func _build_ui() -> void:
|
||||
_asset_browser.make_artwork_requested.connect(_open_stage_artwork_editor)
|
||||
_asset_browser.build_ui(ui, _ui_font, _emoji_font, _sandbox_theme)
|
||||
_asset_browser.edit_scenery_requested.connect(_enter_scenery_edit_mode)
|
||||
_asset_browser.surface_color_confirmed.connect(_record_recent_surface_color)
|
||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||
_build_ground_finish_popup(ui)
|
||||
|
||||
_modal_views.build_file_dialogs(ui)
|
||||
@@ -3122,6 +3162,43 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
|
||||
_scenery_repair_message.name = "MissingArtworkMessage"
|
||||
_scenery_repair_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
body.add_child(_scenery_repair_message)
|
||||
_scenery_surface_section = VBoxContainer.new()
|
||||
_scenery_surface_section.name = "ScenerySurfaceSettings"
|
||||
_scenery_surface_section.add_theme_constant_override("separation", _stage_panel_integer("shell.section_gap", 12, 0, 32))
|
||||
_scenery_wall_look_heading = _make_scenery_section_heading("Look")
|
||||
_scenery_surface_section.add_child(_scenery_wall_look_heading)
|
||||
_scenery_wall_look_group = STAGE_PANEL_CHOICE_GROUP_SCRIPT.new() as StagePanelChoiceGroup
|
||||
_scenery_wall_look_group.name = "WallPanelLooks"
|
||||
_scenery_wall_look_group.add_choice(&"finish_wall_paint", "Paint", _make_surface_choice_cue(&"wall", &"finish_wall_paint"))
|
||||
_scenery_wall_look_group.add_choice(&"finish_wall_brick", "Brick", _make_surface_choice_cue(&"wall", &"finish_wall_brick"))
|
||||
_scenery_wall_look_group.add_choice(&"finish_wall_wood", "Wood", _make_surface_choice_cue(&"wall", &"finish_wall_wood"))
|
||||
_scenery_wall_look_group.apply_theme(_sandbox_theme as SandboxTheme)
|
||||
_scenery_wall_look_group.choice_selected.connect(_on_selected_surface_look_choice)
|
||||
_scenery_surface_section.add_child(_scenery_wall_look_group)
|
||||
_scenery_background_ground_look_heading = _make_scenery_section_heading("Look")
|
||||
_scenery_surface_section.add_child(_scenery_background_ground_look_heading)
|
||||
_scenery_background_ground_look_group = STAGE_PANEL_CHOICE_GROUP_SCRIPT.new() as StagePanelChoiceGroup
|
||||
_scenery_background_ground_look_group.name = "BackgroundGroundLooks"
|
||||
_scenery_background_ground_look_group.add_choice(&"finish_background_grass", "Grass", _make_surface_choice_cue(&"background_ground", &"finish_background_grass"))
|
||||
_scenery_background_ground_look_group.add_choice(&"finish_background_concrete", "Concrete", _make_surface_choice_cue(&"background_ground", &"finish_background_concrete"))
|
||||
_scenery_background_ground_look_group.add_choice(&"finish_background_carpet", "Carpet", _make_surface_choice_cue(&"background_ground", &"finish_background_carpet"))
|
||||
_scenery_background_ground_look_group.apply_theme(_sandbox_theme as SandboxTheme)
|
||||
_scenery_background_ground_look_group.choice_selected.connect(_on_selected_surface_look_choice)
|
||||
_scenery_surface_section.add_child(_scenery_background_ground_look_group)
|
||||
_scenery_surface_helper = Label.new()
|
||||
_scenery_surface_helper.name = "ScenerySurfaceHelper"
|
||||
_scenery_surface_helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_scenery_surface_helper.add_theme_font_size_override("font_size", _stage_panel_integer("type.helper_size", 16, 12, 32))
|
||||
_scenery_surface_helper.add_theme_color_override("font_color", _stage_panel_color("type.muted_text_color", Color("#AAB2C0")))
|
||||
_apply_ui_font(_scenery_surface_helper)
|
||||
_scenery_surface_section.add_child(_scenery_surface_helper)
|
||||
_scenery_surface_colors = STAGE_SURFACE_COLOR_CONTROLS_SCRIPT.new() as StageSurfaceColorControls
|
||||
_scenery_surface_colors.name = "ScenerySurfaceColors"
|
||||
_scenery_surface_colors.configure(Color.WHITE, _recent_surface_colors, _surface_color_presets(), _sandbox_theme as SandboxTheme)
|
||||
_scenery_surface_colors.color_selected.connect(_on_selected_surface_color_selected)
|
||||
_scenery_surface_colors.custom_color_requested.connect(_open_selected_surface_color_dialog)
|
||||
_scenery_surface_section.add_child(_scenery_surface_colors)
|
||||
body.add_child(_scenery_surface_section)
|
||||
_scenery_depth_section = VBoxContainer.new()
|
||||
_scenery_depth_section.name = "SceneryDepthSection"
|
||||
_scenery_depth_section.add_theme_constant_override("separation", 6)
|
||||
@@ -3162,6 +3239,9 @@ func _build_scenery_inspector(ui: CanvasLayer) -> void:
|
||||
_scenery_erase_wall_area_button = _make_scenery_repair_button("EraseWallArea", "Erase wall area", true)
|
||||
_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_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)
|
||||
_scenery_find_button.pressed.connect(func() -> void: _begin_scenery_repair(true))
|
||||
body.add_child(_scenery_find_button)
|
||||
@@ -3192,10 +3272,53 @@ 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)
|
||||
_surface_color_dialog = STAGE_SURFACE_COLOR_DIALOG_SCRIPT.new() as StageSurfaceColorDialog
|
||||
_surface_color_dialog.name = "ScenerySurfaceColorDialog"
|
||||
ui.add_child(_surface_color_dialog)
|
||||
_surface_color_dialog.preview_color_changed.connect(_preview_selected_surface_color)
|
||||
_surface_color_dialog.color_confirmed.connect(_confirm_selected_surface_color)
|
||||
_surface_color_dialog.cancelled.connect(_on_surface_color_dialog_cancelled)
|
||||
if not get_viewport().size_changed.is_connected(_refresh_scenery_inspector_layout):
|
||||
get_viewport().size_changed.connect(_refresh_scenery_inspector_layout)
|
||||
|
||||
|
||||
func _make_scenery_section_heading(label_text: String) -> Label:
|
||||
var heading := Label.new()
|
||||
heading.text = label_text
|
||||
heading.add_theme_font_size_override("font_size", _stage_panel_integer("type.section_size", 20, 12, 40))
|
||||
heading.add_theme_color_override("font_color", _stage_panel_color("type.text_color", Color("#F5F7FA")))
|
||||
_apply_ui_font(heading)
|
||||
return heading
|
||||
|
||||
|
||||
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.configure(kind, finish, STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish))
|
||||
return cue
|
||||
|
||||
|
||||
func _surface_color_presets() -> Array[Color]:
|
||||
var colors: Array[Color] = []
|
||||
var raw_presets: Variant = _sandbox_theme.value("widgets.swatches", []) if _sandbox_theme != null else []
|
||||
if raw_presets is Array:
|
||||
for raw_color: Variant in raw_presets as Array:
|
||||
if raw_color is String and STAGE_SCENE_IO._valid_color(raw_color):
|
||||
colors.append(STAGE_SCENE_IO.color_from_data(raw_color, Color.WHITE))
|
||||
return colors
|
||||
|
||||
|
||||
func _make_scenery_repair_button(node_name: String, label: String, prominent: bool) -> Button:
|
||||
var button: Button = Button.new()
|
||||
button.name = node_name
|
||||
@@ -4221,6 +4344,8 @@ func _open_selector(kind: String) -> void:
|
||||
_scenery_group_mode = &""
|
||||
for pid: String in _palette_buttons:
|
||||
(_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind)
|
||||
if kind == "scenery":
|
||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||
var selector_state: Dictionary = _asset_browser.open_selector(kind, _spawner.get_selected_stickman_path(), _sky_id)
|
||||
if kind == "stickman":
|
||||
var selected_stickman_path: String = String(selector_state.get("selected_stickman_path", _spawner.get_selected_stickman_path()))
|
||||
@@ -4260,12 +4385,25 @@ func _on_asset_selected(entry: Dictionary) -> void:
|
||||
if background_kind == "wall":
|
||||
_close_selector()
|
||||
if _scenery_controller != null:
|
||||
var finish_id: StringName = StringName(String(entry.get("finish_id", "finish_wall_paint")))
|
||||
var tint_value: Variant = entry.get("color", STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish_id))
|
||||
var tint: Color = tint_value if tint_value is Color else STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish_id)
|
||||
_scenery_controller.begin_wall(
|
||||
StringName(String(entry.get("finish_id", "finish_wall_paint"))),
|
||||
_viewport_to_world(get_viewport().get_mouse_position())
|
||||
finish_id,
|
||||
_viewport_to_world(get_viewport().get_mouse_position()),
|
||||
tint
|
||||
)
|
||||
_refresh_scenery_capture_bar()
|
||||
return
|
||||
if background_kind == "background_ground":
|
||||
_close_selector()
|
||||
if _scenery_controller != null:
|
||||
var finish_id: StringName = StringName(String(entry.get("finish_id", "finish_background_grass")))
|
||||
var tint_value: Variant = entry.get("color", STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish_id))
|
||||
var tint: Color = tint_value if tint_value is Color else STAGE_SURFACE_RENDERER_SCRIPT.default_tint(finish_id)
|
||||
_scenery_controller.begin_background_ground(finish_id, _viewport_to_world(get_viewport().get_mouse_position()), tint)
|
||||
_refresh_scenery_capture_bar()
|
||||
return
|
||||
var art_path: String = String(entry.get("path", entry.get("id", "")))
|
||||
_close_selector()
|
||||
if _scenery_controller == null or not _scenery_controller.begin_artwork(art_path, &"near"):
|
||||
@@ -4362,8 +4500,13 @@ func _refresh_scenery_inspector() -> void:
|
||||
_scenery_depth_section.visible = false
|
||||
if _scenery_depth_group != null:
|
||||
_scenery_depth_group.clear_no_signal()
|
||||
if _scenery_wall_look_group != null:
|
||||
_scenery_wall_look_group.clear_no_signal()
|
||||
if _scenery_background_ground_look_group != null:
|
||||
_scenery_background_ground_look_group.clear_no_signal()
|
||||
var selected: Array[Node2D] = _selection.get_selected() if _selection != null else []
|
||||
if not _scenery_edit_mode or selected.size() != 1 or not _is_scenery_node(selected[0]):
|
||||
_cancel_surface_color_preview()
|
||||
if _scenery_repair_candidate != null:
|
||||
_cancel_scenery_repair_preview()
|
||||
_scenery_repair_target = null
|
||||
@@ -4371,15 +4514,45 @@ func _refresh_scenery_inspector() -> void:
|
||||
return
|
||||
var node: Node2D = selected[0]
|
||||
var is_artwork: bool = node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT
|
||||
var is_wall: bool = node.get_script() == STAGE_WALL_PANEL_SCRIPT
|
||||
var is_background_ground: bool = node.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT
|
||||
var surface: StageRectSurface = node as StageRectSurface if is_wall or is_background_ground else null
|
||||
var artwork: StageSceneryArtwork = node as StageSceneryArtwork if is_artwork else null
|
||||
var is_missing: bool = artwork != null and artwork.missing
|
||||
var surface_is_selected_dialog_target: bool = _surface_color_session_targets(surface)
|
||||
if not _surface_color_session.is_empty() and not surface_is_selected_dialog_target:
|
||||
_cancel_surface_color_preview()
|
||||
_scenery_surface_section.visible = surface != null
|
||||
_scenery_wall_look_heading.visible = is_wall
|
||||
_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_colors.visible = false
|
||||
if surface != null:
|
||||
if is_wall:
|
||||
_scenery_wall_look_group.select_no_signal(surface.finish_id)
|
||||
_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:
|
||||
_scenery_surface_colors.set_selected_no_signal(surface.tint)
|
||||
_scenery_surface_colors.set_recent_colors(_recent_surface_colors)
|
||||
_scenery_depth_section.visible = is_artwork
|
||||
if artwork != null:
|
||||
_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 = not is_artwork
|
||||
_scenery_erase_wall_area_button.visible = is_wall
|
||||
_scenery_erase_piece_button.visible = surface != null
|
||||
if is_background_ground:
|
||||
_scenery_erase_piece_button.text = "Erase background ground"
|
||||
else:
|
||||
_scenery_erase_piece_button.text = "Erase wall panel"
|
||||
_scenery_find_button.visible = is_missing
|
||||
_scenery_choose_button.visible = is_missing
|
||||
_scenery_keep_button.visible = is_missing
|
||||
@@ -4390,7 +4563,7 @@ func _refresh_scenery_inspector() -> void:
|
||||
_scenery_repair_message.text = "“%s” is missing its picture file. Choose artwork to put it back in this spot." % artwork.display_name
|
||||
else:
|
||||
_scenery_repair_target = null
|
||||
var scenery_name: String = (artwork.display_name if artwork != null else "Wall panel")
|
||||
var scenery_name: String = "Wall panel" if is_wall else "Background ground" if is_background_ground else artwork.display_name
|
||||
_scenery_inspector.set_header(scenery_name, "🖼️" if artwork != null else "🧱")
|
||||
_scenery_repair_message.text = "Make this scenery smaller or bigger. The other scenery stays where it is."
|
||||
_scenery_inspector.set_safe_rect(_prop_settings_safe_rect())
|
||||
@@ -4419,7 +4592,184 @@ func _on_selected_scenery_depth_choice(id: StringName) -> void:
|
||||
|
||||
|
||||
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)
|
||||
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 _on_selected_surface_look_choice(finish: StringName) -> void:
|
||||
var surface: StageRectSurface = _selected_rect_surface()
|
||||
if surface == null:
|
||||
_refresh_scenery_inspector()
|
||||
return
|
||||
var is_wall: bool = surface.get_script() == STAGE_WALL_PANEL_SCRIPT
|
||||
var valid_finish: bool = STAGE_SURFACE_RENDERER_SCRIPT.is_wall_finish(finish) if is_wall else STAGE_SURFACE_RENDERER_SCRIPT.is_background_finish(finish)
|
||||
if not valid_finish:
|
||||
_refresh_scenery_inspector()
|
||||
return
|
||||
if surface.finish_id != finish:
|
||||
surface.configure(surface.panel_size, finish, surface.tint)
|
||||
_mark_scene_dirty()
|
||||
_refresh_scenery_inspector()
|
||||
|
||||
|
||||
func _selected_rect_surface() -> StageRectSurface:
|
||||
if not _scenery_edit_mode or current_mode != StageMode.EDIT:
|
||||
return null
|
||||
var selected: Array[Node2D] = _selection.get_selected() if _selection != null else []
|
||||
if selected.size() != 1 or not (selected[0].get_script() == STAGE_WALL_PANEL_SCRIPT or selected[0].get_script() == STAGE_BACKGROUND_GROUND_SCRIPT):
|
||||
return null
|
||||
return selected[0] as StageRectSurface
|
||||
|
||||
|
||||
func _on_selected_surface_color_selected(color: Color) -> void:
|
||||
var surface: StageRectSurface = _selected_rect_surface()
|
||||
if surface == null:
|
||||
_refresh_scenery_inspector()
|
||||
return
|
||||
var color_changed: bool = not surface.tint.is_equal_approx(color)
|
||||
if color_changed:
|
||||
surface.clear_preview_tint()
|
||||
surface.configure(surface.panel_size, surface.finish_id, color)
|
||||
_mark_scene_dirty()
|
||||
_record_recent_surface_color(color)
|
||||
_scenery_surface_colors.set_selected_no_signal(surface.tint)
|
||||
|
||||
|
||||
func _open_selected_surface_color_dialog() -> void:
|
||||
var surface: StageRectSurface = _selected_rect_surface()
|
||||
if surface == null or _surface_color_dialog == null:
|
||||
return
|
||||
if surface.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT and surface.finish_id != &"finish_background_carpet":
|
||||
return
|
||||
_cancel_surface_color_preview()
|
||||
var kind: StringName = &"background_ground" if surface.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT else &"wall"
|
||||
_surface_color_session = {
|
||||
"target": weakref(surface),
|
||||
"original_color": surface.tint,
|
||||
"tentative_color": surface.tint,
|
||||
"surface_kind": kind,
|
||||
"finish_id": surface.finish_id
|
||||
}
|
||||
_surface_color_dialog.configure(kind, surface.finish_id, surface.tint, _sandbox_theme as SandboxTheme)
|
||||
_surface_color_dialog.open_dialog()
|
||||
|
||||
|
||||
func _surface_color_session_targets(surface: StageRectSurface) -> bool:
|
||||
if _surface_color_session.is_empty() or surface == null:
|
||||
return false
|
||||
var target_ref: WeakRef = _surface_color_session.get("target") as WeakRef
|
||||
return target_ref != null and target_ref.get_ref() == surface
|
||||
|
||||
|
||||
func _preview_selected_surface_color(color: Color) -> void:
|
||||
if _surface_color_session.is_empty():
|
||||
return
|
||||
var target_ref: WeakRef = _surface_color_session.get("target") as WeakRef
|
||||
var surface: StageRectSurface = target_ref.get_ref() as StageRectSurface if target_ref != null else null
|
||||
if surface == null or not _is_surface_live_world_child(surface):
|
||||
_cancel_surface_color_preview()
|
||||
return
|
||||
_surface_color_session["tentative_color"] = color
|
||||
surface.set_preview_tint(color)
|
||||
|
||||
|
||||
func _confirm_selected_surface_color(color: Color) -> void:
|
||||
if _surface_color_session.is_empty():
|
||||
return
|
||||
var target_ref: WeakRef = _surface_color_session.get("target") as WeakRef
|
||||
var surface: StageRectSurface = target_ref.get_ref() as StageRectSurface if target_ref != null else null
|
||||
if surface == null or not _is_surface_live_world_child(surface):
|
||||
_cancel_surface_color_preview()
|
||||
return
|
||||
var color_changed: bool = not surface.tint.is_equal_approx(color)
|
||||
surface.clear_preview_tint()
|
||||
if color_changed:
|
||||
surface.configure(surface.panel_size, surface.finish_id, color)
|
||||
_surface_color_session.clear()
|
||||
_record_recent_surface_color(color)
|
||||
if color_changed:
|
||||
_mark_scene_dirty()
|
||||
_refresh_scenery_inspector()
|
||||
call_deferred("_focus_surface_color_control")
|
||||
|
||||
|
||||
func _on_surface_color_dialog_cancelled() -> void:
|
||||
_cancel_surface_color_preview()
|
||||
call_deferred("_focus_surface_color_control")
|
||||
|
||||
|
||||
func _focus_surface_color_control() -> void:
|
||||
if _scenery_inspector != null and _scenery_inspector.visible and _scenery_surface_colors != null and _scenery_surface_colors.visible:
|
||||
_scenery_surface_colors.focus_custom_button()
|
||||
|
||||
|
||||
func _cancel_surface_color_preview() -> void:
|
||||
if _surface_color_session.is_empty():
|
||||
if _surface_color_dialog != null and _surface_color_dialog.visible:
|
||||
_surface_color_dialog.hide()
|
||||
return
|
||||
var target_ref: WeakRef = _surface_color_session.get("target") as WeakRef
|
||||
var surface: StageRectSurface = target_ref.get_ref() as StageRectSurface if target_ref != null else null
|
||||
if surface != null and is_instance_valid(surface):
|
||||
surface.clear_preview_tint()
|
||||
_surface_color_session.clear()
|
||||
if _surface_color_dialog != null and _surface_color_dialog.visible:
|
||||
_surface_color_dialog.hide()
|
||||
if _scenery_inspector != null and is_instance_valid(_scenery_inspector):
|
||||
_scenery_inspector.queue_sort()
|
||||
|
||||
|
||||
func _is_surface_live_world_child(surface: StageRectSurface) -> bool:
|
||||
return surface != null and is_instance_valid(surface) and _world != null and is_instance_valid(_world) and surface.get_parent() == _world and _scenery_edit_mode and current_mode == StageMode.EDIT
|
||||
|
||||
|
||||
func _request_erase_selected_scenery() -> void:
|
||||
var selected: Array[Node2D] = _selection.get_selected() if _selection != null else []
|
||||
if selected.is_empty() or not _scenery_edit_mode or current_mode != StageMode.EDIT:
|
||||
return
|
||||
var targets: Array[Node2D] = []
|
||||
for node: Node2D in selected:
|
||||
if _is_scenery_node(node) and node.get_parent() == _world:
|
||||
targets.append(node)
|
||||
if targets.is_empty():
|
||||
return
|
||||
_clear_erase_scenery_targets()
|
||||
for node: Node2D in targets:
|
||||
_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."
|
||||
else:
|
||||
_scenery_erase_piece_dialog.dialog_text = "Erase these %d scenery pieces? You can’t undo this." % count
|
||||
_scenery_erase_piece_dialog.popup_centered()
|
||||
|
||||
|
||||
func _confirm_erase_selected_scenery() -> void:
|
||||
var erased: Array[Node2D] = []
|
||||
if not _scenery_edit_mode or current_mode != StageMode.EDIT or _world == null or not is_instance_valid(_world):
|
||||
_clear_erase_scenery_targets()
|
||||
return
|
||||
for target_ref: WeakRef in _scenery_erase_piece_targets:
|
||||
var node: Node2D = target_ref.get_ref() as Node2D if target_ref != null else null
|
||||
if node == null or not is_instance_valid(node) or node.get_parent() != _world or not _is_scenery_node(node):
|
||||
continue
|
||||
_clear_object_state(node)
|
||||
node.queue_free()
|
||||
erased.append(node)
|
||||
_clear_erase_scenery_targets()
|
||||
if erased.is_empty():
|
||||
return
|
||||
if _selection != null:
|
||||
_selection.clear_selection()
|
||||
_selection.clear_hover()
|
||||
_director_visuals.mark_dirty()
|
||||
object_deleted.emit(erased)
|
||||
_mark_scene_dirty()
|
||||
_refresh_scenery_inspector()
|
||||
_refresh_status()
|
||||
|
||||
|
||||
func _clear_erase_scenery_targets() -> void:
|
||||
_scenery_erase_piece_targets.clear()
|
||||
|
||||
|
||||
func _step_selected_scenery_size(factor: float) -> void:
|
||||
@@ -4546,6 +4896,10 @@ func _enter_scenery_edit_mode() -> void:
|
||||
|
||||
|
||||
func _exit_scenery_edit_mode() -> void:
|
||||
_cancel_surface_color_preview()
|
||||
if _scenery_erase_piece_dialog != null:
|
||||
_scenery_erase_piece_dialog.hide()
|
||||
_clear_erase_scenery_targets()
|
||||
if _scenery_repair_confirm_dialog != null:
|
||||
_scenery_repair_confirm_dialog.hide()
|
||||
_cancel_scenery_repair_preview()
|
||||
@@ -4605,7 +4959,16 @@ func _refresh_scenery_capture_bar() -> void:
|
||||
else:
|
||||
actions.append({"id": "done_tool", "label": "Done", "primary": true})
|
||||
actions.append({"id": "cancel_tool", "label": "✕ Cancel"})
|
||||
_use_target_capture_bar.set_capture_actions("Drag to draw wall · Stickmen can pass through", actions)
|
||||
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)
|
||||
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)
|
||||
return
|
||||
if _scenery_controller != null and _scenery_controller.is_group_preview_pending():
|
||||
_use_target_capture_bar.clear_horizontal_safe_bounds()
|
||||
@@ -4622,6 +4985,8 @@ func _refresh_scenery_capture_bar() -> void:
|
||||
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"
|
||||
@@ -4737,7 +5102,7 @@ func _select_all_scenery() -> void:
|
||||
_selection.clear_selection()
|
||||
for child: Node in _world.get_children():
|
||||
var node: Node2D = child as Node2D
|
||||
if node != null and (node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT):
|
||||
if node != null and (node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT or node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT):
|
||||
_selection.add_to_selection(node)
|
||||
_refresh_scenery_capture_bar()
|
||||
|
||||
@@ -6920,7 +7285,7 @@ func _world_children_selectable() -> Array[Node2D]:
|
||||
continue
|
||||
if node.is_in_group("stickman_ragdoll_container") or bool(node.get_meta("stickman_ragdoll_container", false)):
|
||||
continue
|
||||
if node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT:
|
||||
if node.get_script() == STAGE_WALL_PANEL_SCRIPT or node.get_script() == STAGE_BACKGROUND_GROUND_SCRIPT or node.get_script() == STAGE_SCENERY_ARTWORK_SCRIPT:
|
||||
continue
|
||||
result.append(node)
|
||||
return result
|
||||
@@ -6984,8 +7349,24 @@ func _load_settings() -> void:
|
||||
_grid_size = clampf(_grid_size, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
||||
_snap_enabled = bool(d.get("snap_to_grid", false))
|
||||
_show_grid = bool(d.get("show_grid", true))
|
||||
var loaded_recent_scenes: Array[String] = []
|
||||
var settings_migrated: bool = false
|
||||
_recent_surface_colors.clear()
|
||||
var raw_surface_colors: Variant = d.get("recent_surface_colors", [])
|
||||
var normalized_surface_colors: Array[String] = []
|
||||
if raw_surface_colors is Array:
|
||||
for raw_color: Variant in raw_surface_colors as Array:
|
||||
if not raw_color is String or not STAGE_SCENE_IO._valid_color(raw_color):
|
||||
continue
|
||||
var parsed_color: Color = STAGE_SCENE_IO.color_from_data(raw_color, Color.WHITE)
|
||||
var canonical_color: String = _canonical_surface_color(parsed_color)
|
||||
if not normalized_surface_colors.has(canonical_color):
|
||||
normalized_surface_colors.append(canonical_color)
|
||||
_recent_surface_colors.append(Color.from_string(canonical_color, Color.WHITE))
|
||||
if normalized_surface_colors.size() >= RECENT_SURFACE_COLOR_LIMIT:
|
||||
break
|
||||
if normalized_surface_colors != (raw_surface_colors as Array):
|
||||
settings_migrated = true
|
||||
var loaded_recent_scenes: Array[String] = []
|
||||
var raw_recents: Variant = d.get("recent_scenes", [])
|
||||
if raw_recents is Array:
|
||||
for raw: Variant in raw_recents:
|
||||
@@ -7009,8 +7390,49 @@ func _save_settings() -> void:
|
||||
"snap_to_grid": _snap_enabled,
|
||||
"show_grid": _show_grid,
|
||||
"recent_scenes": _recent_scenes,
|
||||
"recent_surface_colors": _surface_color_history_strings(),
|
||||
}
|
||||
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(JSON.stringify(data, "\t", false))
|
||||
file.close()
|
||||
|
||||
|
||||
func _canonical_surface_color(color: Color) -> String:
|
||||
var opaque: Color = Color(color.r, color.g, color.b, 1.0)
|
||||
return "#" + opaque.to_html(false).to_upper()
|
||||
|
||||
|
||||
func _surface_color_history_strings() -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
for color: Color in _recent_surface_colors:
|
||||
result.append(_canonical_surface_color(color))
|
||||
return result
|
||||
|
||||
|
||||
func _record_recent_surface_color(color: Color) -> void:
|
||||
var opaque: Color = Color(color.r, color.g, color.b, 1.0)
|
||||
var canonical: String = _canonical_surface_color(opaque)
|
||||
var retained: Array[Color] = []
|
||||
retained.append(opaque)
|
||||
for existing: Color in _recent_surface_colors:
|
||||
if _canonical_surface_color(existing) == canonical:
|
||||
continue
|
||||
retained.append(existing)
|
||||
if retained.size() >= RECENT_SURFACE_COLOR_LIMIT:
|
||||
break
|
||||
if _surface_color_history_strings() == _colors_to_strings(retained):
|
||||
return
|
||||
_recent_surface_colors = retained
|
||||
_save_settings()
|
||||
if _asset_browser != null:
|
||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||
if _scenery_surface_colors != null:
|
||||
_scenery_surface_colors.set_recent_colors(_recent_surface_colors)
|
||||
|
||||
|
||||
func _colors_to_strings(colors: Array[Color]) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
for color: Color in colors:
|
||||
result.append(_canonical_surface_color(color))
|
||||
return result
|
||||
|
||||
@@ -8,6 +8,7 @@ signal selector_cancel_requested
|
||||
signal refresh_requested
|
||||
signal make_artwork_requested
|
||||
signal edit_scenery_requested
|
||||
signal surface_color_confirmed(color: Color)
|
||||
|
||||
const ASSET_SELECTOR_SCENE := preload("res://scenes/stage/asset_selector.tscn")
|
||||
const STICKMAN_LIBRARY_SCRIPT := preload("res://scripts/characters/stickman/stickman_library.gd")
|
||||
@@ -59,6 +60,7 @@ func build_ui(ui_layer: CanvasLayer, ui_font: Font, emoji_font: Font, sandbox_th
|
||||
selector.refresh_requested.connect(func() -> void: refresh_requested.emit())
|
||||
selector.make_artwork_requested.connect(func() -> void: make_artwork_requested.emit())
|
||||
selector.edit_scenery_requested.connect(func() -> void: edit_scenery_requested.emit())
|
||||
selector.surface_color_confirmed.connect(func(color: Color) -> void: surface_color_confirmed.emit(color))
|
||||
ui_layer.add_child(selector)
|
||||
# apply_font AFTER add_child so the selector's @onready nodes resolve first.
|
||||
selector.apply_font(ui_font, emoji_font)
|
||||
@@ -84,6 +86,11 @@ func scan_entries(kind: String) -> Array[Dictionary]:
|
||||
return PROP_LIBRARY_SCRIPT.scan()
|
||||
|
||||
|
||||
func set_surface_color_history(colors: Array[Color]) -> void:
|
||||
if selector != null:
|
||||
selector.set_surface_color_history(colors)
|
||||
|
||||
|
||||
func _starter_scene_entries() -> Array[Dictionary]:
|
||||
return [
|
||||
{
|
||||
@@ -148,9 +155,8 @@ func _scenery_background_entries() -> Array[Dictionary]:
|
||||
{"id": "sky_cloudy_day", "name": "Cloudy Day", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "☁️"},
|
||||
{"id": "sky_sunset", "name": "Sunset", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌅"},
|
||||
{"id": "sky_starry_night", "name": "Starry Night", "category": "Sky", "gallery_tab": "backgrounds", "background_kind": "sky", "icon": "🌙"},
|
||||
{"id": "finish_wall_paint", "name": "Wall panel · Paint", "category": "Wall finish", "gallery_tab": "backgrounds", "background_kind": "wall", "finish_id": "finish_wall_paint", "icon": "🟩"},
|
||||
{"id": "finish_wall_brick", "name": "Wall panel · Brick", "category": "Wall finish", "gallery_tab": "backgrounds", "background_kind": "wall", "finish_id": "finish_wall_brick", "icon": "🧱"},
|
||||
{"id": "finish_wall_wood", "name": "Wall panel · Wood", "category": "Wall finish", "gallery_tab": "backgrounds", "background_kind": "wall", "finish_id": "finish_wall_wood", "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": "🌿"}
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ signal reset_requested
|
||||
|
||||
const TOP_BAR_HEIGHT: float = 64.0
|
||||
const STATUS_BAR_HEIGHT: float = 38.0
|
||||
const COMPACT_TOP_BAR_WIDTH: float = 900.0
|
||||
|
||||
var _stage_theme: SandboxTheme
|
||||
var _current_mode: int = 0
|
||||
@@ -43,6 +44,8 @@ var _mode_buttons: Dictionary = {}
|
||||
var _palette_buttons: Dictionary = {}
|
||||
var _palette_dock: PanelContainer
|
||||
var _file_menu: MenuButton
|
||||
var _mode_center: CenterContainer
|
||||
var _right_controls: HBoxContainer
|
||||
var _status_label: Label
|
||||
var _status_cursor_coords: Label
|
||||
var _direct_hint_label: Label
|
||||
@@ -405,10 +408,13 @@ func _build_top_bar() -> void:
|
||||
right_group.offset_bottom = TOP_BAR_HEIGHT - 8.0
|
||||
right_group.alignment = BoxContainer.ALIGNMENT_END
|
||||
right_group.add_theme_constant_override("separation", 8)
|
||||
_right_controls = right_group
|
||||
chrome.add_child(right_group)
|
||||
_build_file_menu(left_group)
|
||||
_build_mode_switcher(chrome)
|
||||
_build_grid_controls(right_group)
|
||||
get_viewport().size_changed.connect(_apply_responsive_top_bar_layout)
|
||||
_apply_responsive_top_bar_layout()
|
||||
|
||||
|
||||
func _build_file_menu(parent: HBoxContainer) -> void:
|
||||
@@ -442,6 +448,7 @@ func _build_mode_switcher(parent: Control) -> void:
|
||||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
center.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
center.offset_bottom = TOP_BAR_HEIGHT
|
||||
_mode_center = center
|
||||
parent.add_child(center)
|
||||
var switcher := HBoxContainer.new()
|
||||
switcher.name = "ModeSwitcher"
|
||||
@@ -492,6 +499,52 @@ func _build_grid_controls(parent: HBoxContainer) -> void:
|
||||
parent.add_child(_grid_size_spin)
|
||||
|
||||
|
||||
func _apply_responsive_top_bar_layout() -> void:
|
||||
if _file_menu == null or _mode_center == null or _right_controls == null:
|
||||
return
|
||||
var viewport_width: float = get_viewport().get_visible_rect().size.x
|
||||
var compact: bool = viewport_width <= COMPACT_TOP_BAR_WIDTH
|
||||
if compact:
|
||||
_file_menu.custom_minimum_size = Vector2(64.0, 48.0)
|
||||
_file_menu.add_theme_font_size_override("font_size", 18)
|
||||
_mode_center.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
||||
_mode_center.offset_left = 76.0
|
||||
_mode_center.offset_top = 8.0
|
||||
_mode_center.offset_right = 342.0
|
||||
_mode_center.offset_bottom = TOP_BAR_HEIGHT - 8.0
|
||||
_right_controls.add_theme_constant_override("separation", 4)
|
||||
_grid_check.add_theme_font_size_override("font_size", 16)
|
||||
_snap_check.add_theme_font_size_override("font_size", 16)
|
||||
_grid_size_label.visible = false
|
||||
_grid_size_spin.custom_minimum_size = Vector2(56.0, 0.0)
|
||||
_grid_size_spin.add_theme_font_size_override("font_size", 16)
|
||||
else:
|
||||
_file_menu.custom_minimum_size = Vector2(78.0, 48.0)
|
||||
_file_menu.add_theme_font_size_override(
|
||||
"font_size", _theme_integer("fonts.sizes.file_menu", 22, 12, 48)
|
||||
)
|
||||
_mode_center.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
||||
_mode_center.offset_bottom = TOP_BAR_HEIGHT
|
||||
_right_controls.add_theme_constant_override("separation", 8)
|
||||
_grid_check.remove_theme_font_size_override("font_size")
|
||||
_snap_check.remove_theme_font_size_override("font_size")
|
||||
_grid_size_label.visible = true
|
||||
_grid_size_spin.custom_minimum_size = Vector2.ZERO
|
||||
_grid_size_spin.remove_theme_font_size_override("font_size")
|
||||
for mode: int in _mode_buttons:
|
||||
var button: Button = _mode_buttons[mode] as Button
|
||||
if compact:
|
||||
button.custom_minimum_size = Vector2(86.0, 48.0)
|
||||
button.add_theme_font_size_override("font_size", 18)
|
||||
else:
|
||||
button.custom_minimum_size = Vector2(
|
||||
_theme_number("chrome.mode_switcher.segment_min_width", 124.0, 90.0, 220.0), 48.0
|
||||
)
|
||||
button.add_theme_font_size_override(
|
||||
"font_size", _theme_integer("fonts.sizes.mode_switcher", 24, 12, 48)
|
||||
)
|
||||
|
||||
|
||||
func _build_palette(entries: Array[Dictionary]) -> void:
|
||||
_palette_dock = PanelContainer.new()
|
||||
_palette_dock.name = "PaletteDock"
|
||||
|
||||
@@ -11,6 +11,7 @@ const PROP_LIBRARY := preload("res://scripts/props/prop_library.gd")
|
||||
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
||||
const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd")
|
||||
const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const STAGE_BACKGROUND_GROUND := preload("res://scripts/stage/scenery/stage_background_ground.gd")
|
||||
const STAGE_SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
const STAGE_DRAW_ORDER := preload("res://scripts/stage/stage_draw_order.gd")
|
||||
|
||||
@@ -24,7 +25,7 @@ func collect(
|
||||
) -> Dictionary:
|
||||
var objects: Array[Dictionary] = []
|
||||
var runtime_to_file: Dictionary = {}
|
||||
var counters: Dictionary = {"terrain": 0, "prop": 0, "stickman": 0, "area": 0, "wall_panel": 0, "scenery_artwork": 0}
|
||||
var counters: Dictionary = {"terrain": 0, "prop": 0, "stickman": 0, "area": 0, "wall_panel": 0, "background_ground": 0, "scenery_artwork": 0}
|
||||
for node: Node in world.get_children():
|
||||
if not node is Node2D:
|
||||
continue
|
||||
@@ -54,6 +55,11 @@ func collect(
|
||||
if stage_node is TerrainBlock and String(stage_node.get("spawn_id")) == "ground":
|
||||
var ground_finish: StringName = (stage_node as TerrainBlock).finish_id
|
||||
entry["finish_id"] = String(ground_finish) if not ground_finish.is_empty() else "finish_ground_legacy"
|
||||
elif stage_node.get_script() == STAGE_BACKGROUND_GROUND:
|
||||
var background: StageBackgroundGround = stage_node as StageBackgroundGround
|
||||
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)
|
||||
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)
|
||||
@@ -168,6 +174,16 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
|
||||
wall.motif_origin_offset = STAGE_SCENE_IO.data_to_vector(entry.get("motif_origin_offset"), Vector2.ZERO)
|
||||
world.add_child(wall)
|
||||
node = wall
|
||||
"background_ground":
|
||||
var background := STAGE_BACKGROUND_GROUND.new() as StageBackgroundGround
|
||||
background.position = position
|
||||
background.configure(
|
||||
STAGE_SCENE_IO.data_to_vector(entry.get("size"), Vector2(800.0, 300.0)),
|
||||
StringName(String(entry.get("finish_id", "finish_background_grass"))),
|
||||
STAGE_SCENE_IO.color_from_data(entry.get("color", "#72A84CFF"), Color("#72A84C"))
|
||||
)
|
||||
world.add_child(background)
|
||||
node = background
|
||||
"scenery_artwork":
|
||||
var artwork := STAGE_SCENERY_ARTWORK.new() as StageSceneryArtwork
|
||||
var raw_bounds: Variant = entry.get("last_known_local_bounds", {})
|
||||
@@ -285,6 +301,8 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
|
||||
func _object_type(node: Node2D) -> String:
|
||||
if node.get_script() == STAGE_WALL_PANEL:
|
||||
return "wall_panel"
|
||||
if node.get_script() == STAGE_BACKGROUND_GROUND:
|
||||
return "background_ground"
|
||||
if node.get_script() == STAGE_SCENERY_ARTWORK:
|
||||
return "scenery_artwork"
|
||||
if node is TerrainBlock:
|
||||
|
||||
@@ -8,6 +8,8 @@ signal preview_state_changed(active: bool)
|
||||
signal single_gesture_changed(active: bool)
|
||||
|
||||
const WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const BACKGROUND_GROUND := preload("res://scripts/stage/scenery/stage_background_ground.gd")
|
||||
const RECT_SURFACE := preload("res://scripts/stage/scenery/stage_rect_surface.gd")
|
||||
const SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
const PROP_FILE := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
||||
@@ -15,6 +17,7 @@ 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
|
||||
@@ -112,6 +115,12 @@ func get_active_artwork_name() -> String:
|
||||
return _asset_path.get_file().get_basename()
|
||||
|
||||
|
||||
func get_active_surface_label() -> String:
|
||||
if _active_tool == &"background_ground":
|
||||
return "background ground"
|
||||
return "wall"
|
||||
|
||||
|
||||
func handle_key(event: InputEventKey) -> bool:
|
||||
if is_active() and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
|
||||
exit_tool()
|
||||
@@ -144,17 +153,31 @@ func begin_artwork(path: String, depth: StringName = &"near") -> bool:
|
||||
|
||||
|
||||
func begin_wall(finish: StringName, initial_world_position: Vector2, tint: Color = Color("#5A9D70")) -> void:
|
||||
_begin_rect_surface(&"wall", finish, initial_world_position, tint)
|
||||
|
||||
|
||||
func begin_background_ground(finish: StringName, initial_world_position: Vector2, tint: Color) -> void:
|
||||
_begin_rect_surface(&"background_ground", finish, initial_world_position, tint)
|
||||
|
||||
|
||||
func _begin_rect_surface(tool: StringName, finish: StringName, initial_world_position: Vector2, tint: Color) -> void:
|
||||
if _world == null or _ghost_holder == null:
|
||||
return
|
||||
exit_tool()
|
||||
_active_tool = &"wall"
|
||||
_active_tool = tool
|
||||
_wall_finish = finish
|
||||
_wall_tint = tint
|
||||
_wall_anchor = Vector2.INF
|
||||
_wall_dragging = false
|
||||
_wall_anchor_active = false
|
||||
var panel := WALL_PANEL.new() as StageWallPanel
|
||||
panel.configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint)
|
||||
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.modulate = Color(1.0, 1.0, 1.0, 0.45)
|
||||
_ghost_holder.add_child(panel)
|
||||
_ghost = panel
|
||||
@@ -204,14 +227,14 @@ func update_pointer(world_position: Vector2) -> void:
|
||||
if not is_active() or _ghost == null or not is_instance_valid(_ghost):
|
||||
return
|
||||
var snapped_position: Vector2 = _snap(world_position)
|
||||
if _active_tool == &"wall" and _wall_dragging:
|
||||
if _is_rect_placement_tool() and _wall_dragging:
|
||||
var size: Vector2 = Vector2(absf(snapped_position.x - _wall_drag_start.x), absf(snapped_position.y - _wall_drag_start.y))
|
||||
size.x = maxf(MIN_PANEL_SIZE, size.x)
|
||||
size.y = maxf(MIN_PANEL_SIZE, size.y)
|
||||
(_ghost as StageWallPanel).configure(size, _wall_finish, _wall_tint)
|
||||
(_ghost as StageRectSurface).configure(size, _wall_finish, _wall_tint)
|
||||
var top_left: Vector2 = Vector2(minf(_wall_drag_start.x, snapped_position.x), minf(_wall_drag_start.y, snapped_position.y))
|
||||
_ghost.global_position = top_left + size * 0.5
|
||||
elif _active_tool == &"wall":
|
||||
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":
|
||||
@@ -234,7 +257,7 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
|
||||
exit_tool()
|
||||
return true
|
||||
var completes_drag: bool = not event.pressed and event.button_index == MOUSE_BUTTON_LEFT and (
|
||||
(_active_tool == &"wall" and _wall_dragging)
|
||||
(_is_rect_placement_tool() and _wall_dragging)
|
||||
or (_active_tool == &"wall_eraser" and _wall_erase_dragging)
|
||||
)
|
||||
if (over_ui and not completes_drag) or event.button_index != MOUSE_BUTTON_LEFT:
|
||||
@@ -243,7 +266,7 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
|
||||
if event.pressed:
|
||||
_commit_artwork(world_position)
|
||||
return true
|
||||
if _active_tool == &"wall":
|
||||
if _is_rect_placement_tool():
|
||||
if event.pressed:
|
||||
# Refresh from the press coordinates in case no mouse-motion event was
|
||||
# delivered after the palette closed. The visible ghost corner is the
|
||||
@@ -262,12 +285,12 @@ func handle_mouse_button(event: InputEventMouseButton, world_position: Vector2,
|
||||
if size.x < MIN_PANEL_SIZE or size.y < MIN_PANEL_SIZE:
|
||||
_wall_anchor_active = false
|
||||
_wall_anchor = Vector2.INF
|
||||
_restore_wall_ghost(world_position)
|
||||
_restore_rect_surface_ghost(world_position)
|
||||
return true
|
||||
_commit_wall(_wall_drag_start, end)
|
||||
_commit_rect_surface(_wall_drag_start, end)
|
||||
_wall_anchor = Vector2.INF
|
||||
_wall_anchor_active = false
|
||||
_restore_wall_ghost(world_position)
|
||||
_restore_rect_surface_ghost(world_position)
|
||||
return true
|
||||
if _active_tool == &"wall_eraser":
|
||||
if event.pressed:
|
||||
@@ -304,8 +327,8 @@ func cancel_active_gesture() -> void:
|
||||
_wall_anchor_active = false
|
||||
_wall_erase_dragging = false
|
||||
_wall_erase_start = Vector2.INF
|
||||
if _active_tool == &"wall" and _ghost != null and is_instance_valid(_ghost):
|
||||
(_ghost as StageWallPanel).configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint)
|
||||
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):
|
||||
_set_wall_eraser_preview_rect(Rect2(_wall_erase_pointer, WALL_ERASER_CURSOR_SIZE))
|
||||
|
||||
@@ -334,10 +357,10 @@ func begin_group_transform(kind: StringName, members: Array[Node2D]) -> bool:
|
||||
return false
|
||||
var snapshot: Dictionary = {"node": member, "position": member.global_position, "rotation": member.global_rotation, "scale": member.scale}
|
||||
var bounds: Rect2
|
||||
if member.get_script() == WALL_PANEL:
|
||||
var wall: StageWallPanel = member as StageWallPanel
|
||||
snapshot["panel_size"] = wall.panel_size
|
||||
bounds = member.global_transform * wall.get_local_bounds()
|
||||
if member is StageRectSurface:
|
||||
var surface: StageRectSurface = member as StageRectSurface
|
||||
snapshot["panel_size"] = surface.panel_size
|
||||
bounds = member.global_transform * surface.get_local_bounds()
|
||||
elif member.get_script() == SCENERY_ARTWORK:
|
||||
var artwork: StageSceneryArtwork = member as StageSceneryArtwork
|
||||
bounds = STAGE_SELECTION.get_world_aabb(member)
|
||||
@@ -369,7 +392,7 @@ func preview_group(delta: Vector2, factor: float) -> bool:
|
||||
if member == null or not is_instance_valid(member):
|
||||
return false
|
||||
if _group_kind == &"resize":
|
||||
if member.get_script() == WALL_PANEL:
|
||||
if member is StageRectSurface:
|
||||
var next_size: Vector2 = (snapshot["panel_size"] as Vector2) * factor
|
||||
if next_size.x < MIN_PANEL_SIZE or next_size.y < MIN_PANEL_SIZE:
|
||||
return false
|
||||
@@ -389,9 +412,9 @@ func preview_group(delta: Vector2, factor: float) -> bool:
|
||||
var new_scale: Vector2 = art_scale * factor
|
||||
member.global_position = anchor + ((snapshot["position"] as Vector2) - anchor) * factor
|
||||
member.global_rotation = float(snapshot["rotation"])
|
||||
if member.get_script() == WALL_PANEL:
|
||||
var wall: StageWallPanel = member as StageWallPanel
|
||||
wall.configure((snapshot["panel_size"] as Vector2) * factor, wall.finish_id, wall.tint)
|
||||
if member is StageRectSurface:
|
||||
var surface: StageRectSurface = member as StageRectSurface
|
||||
surface.configure((snapshot["panel_size"] as Vector2) * factor, surface.finish_id, surface.tint)
|
||||
else:
|
||||
member.scale = new_scale
|
||||
return true
|
||||
@@ -410,7 +433,7 @@ func commit_group() -> void:
|
||||
func begin_single_transform(node: Node2D, kind: StringName, pointer: Vector2, dragged_corner: Vector2 = Vector2.INF) -> bool:
|
||||
if _single_active or _group_pending or node == null or not is_instance_valid(node) or kind not in [&"move", &"resize"]:
|
||||
return false
|
||||
if node.get_script() != WALL_PANEL and node.get_script() != SCENERY_ARTWORK:
|
||||
if not (node is StageRectSurface) and node.get_script() != SCENERY_ARTWORK:
|
||||
return false
|
||||
if not pointer.is_finite():
|
||||
return false
|
||||
@@ -434,8 +457,8 @@ func begin_single_transform(node: Node2D, kind: StringName, pointer: Vector2, dr
|
||||
"dragged_corner": corners[corner_index],
|
||||
"pivot": _opposite_corner(bounds, corner_index),
|
||||
}
|
||||
if node.get_script() == WALL_PANEL:
|
||||
snapshot["panel_size"] = (node as StageWallPanel).panel_size
|
||||
if node is StageRectSurface:
|
||||
snapshot["panel_size"] = (node as StageRectSurface).panel_size
|
||||
else:
|
||||
var initial_scale: Vector2 = node.scale
|
||||
if not initial_scale.is_finite() or initial_scale.x <= 0.0 or initial_scale.y <= 0.0:
|
||||
@@ -502,7 +525,7 @@ func cancel_single_transform() -> void:
|
||||
func step_single_size(node: Node2D, factor: float) -> bool:
|
||||
if node == null or not is_instance_valid(node) or not is_finite(factor) or factor <= 0.0 or _single_active or _group_pending:
|
||||
return false
|
||||
if node.get_script() != WALL_PANEL and node.get_script() != SCENERY_ARTWORK:
|
||||
if not (node is StageRectSurface) and node.get_script() != SCENERY_ARTWORK:
|
||||
return false
|
||||
var bounds: Rect2 = _world_bounds(node)
|
||||
if bounds.size.x <= 0.0 or bounds.size.y <= 0.0:
|
||||
@@ -518,7 +541,7 @@ func step_single_size(node: Node2D, factor: float) -> bool:
|
||||
"scale": node.scale,
|
||||
"bounds": bounds,
|
||||
"pivot": bounds.position,
|
||||
"panel_size": (node as StageWallPanel).panel_size if node.get_script() == WALL_PANEL else Vector2.ZERO,
|
||||
"panel_size": (node as StageRectSurface).panel_size if node is StageRectSurface else Vector2.ZERO,
|
||||
}
|
||||
var pivot: Vector2 = bounds.position
|
||||
if not _can_apply_single_scale(snapshot, factor):
|
||||
@@ -540,9 +563,9 @@ func cancel_group() -> void:
|
||||
member.global_position = snapshot["position"] as Vector2
|
||||
member.global_rotation = float(snapshot["rotation"])
|
||||
member.scale = snapshot["scale"] as Vector2
|
||||
if member.get_script() == WALL_PANEL and snapshot.has("panel_size"):
|
||||
var wall: StageWallPanel = member as StageWallPanel
|
||||
wall.configure(snapshot["panel_size"] as Vector2, wall.finish_id, wall.tint)
|
||||
if member is StageRectSurface and snapshot.has("panel_size"):
|
||||
var surface: StageRectSurface = member as StageRectSurface
|
||||
surface.configure(snapshot["panel_size"] as Vector2, surface.finish_id, surface.tint)
|
||||
_group_pending = false
|
||||
_group_snapshots.clear()
|
||||
_group_kind = &""
|
||||
@@ -553,7 +576,7 @@ static func bounds_for(members: Array[Node2D]) -> Rect2:
|
||||
var result: Rect2 = Rect2()
|
||||
var has_rect: bool = false
|
||||
for member: Node2D in members:
|
||||
var rect: Rect2 = STAGE_SELECTION.get_world_aabb(member) if member.get_script() == SCENERY_ARTWORK else (member.global_transform * (member as StageWallPanel).get_local_bounds())
|
||||
var rect: Rect2 = (member as StageRectSurface).global_transform * (member as StageRectSurface).get_local_bounds() if member is StageRectSurface else STAGE_SELECTION.get_world_aabb(member)
|
||||
result = rect if not has_rect else result.merge(rect)
|
||||
has_rect = true
|
||||
return result
|
||||
@@ -572,9 +595,13 @@ func _commit_artwork(position: Vector2) -> void:
|
||||
document_change_requested.emit()
|
||||
|
||||
|
||||
func _commit_wall(first: Vector2, second: Vector2) -> void:
|
||||
func _commit_rect_surface(first: Vector2, second: Vector2) -> void:
|
||||
var size: Vector2 = Vector2(maxf(MIN_PANEL_SIZE, absf(second.x - first.x)), maxf(MIN_PANEL_SIZE, absf(second.y - first.y)))
|
||||
var panel := WALL_PANEL.new() as StageWallPanel
|
||||
var panel: StageRectSurface
|
||||
if _active_tool == &"background_ground":
|
||||
panel = BACKGROUND_GROUND.new() as StageBackgroundGround
|
||||
else:
|
||||
panel = WALL_PANEL.new() as StageWallPanel
|
||||
panel.configure(size, _wall_finish, _wall_tint)
|
||||
panel.global_position = (first + second) * 0.5
|
||||
_world.add_child(panel)
|
||||
@@ -582,13 +609,21 @@ func _commit_wall(first: Vector2, second: Vector2) -> void:
|
||||
document_change_requested.emit()
|
||||
|
||||
|
||||
func _restore_wall_ghost(world_position: Vector2) -> void:
|
||||
if _ghost == null or not is_instance_valid(_ghost) or _ghost.get_script() != WALL_PANEL:
|
||||
func _restore_rect_surface_ghost(world_position: Vector2) -> void:
|
||||
if _ghost == null or not is_instance_valid(_ghost) or not (_ghost is StageRectSurface):
|
||||
return
|
||||
(_ghost as StageWallPanel).configure(DEFAULT_PANEL_SIZE, _wall_finish, _wall_tint)
|
||||
(_ghost as StageRectSurface).configure(_default_size_for_active_surface(), _wall_finish, _wall_tint)
|
||||
update_pointer(world_position)
|
||||
|
||||
|
||||
func _is_rect_placement_tool() -> bool:
|
||||
return _active_tool in [&"wall", &"background_ground"]
|
||||
|
||||
|
||||
func _default_size_for_active_surface() -> Vector2:
|
||||
return DEFAULT_BACKGROUND_SIZE if _active_tool == &"background_ground" else DEFAULT_PANEL_SIZE
|
||||
|
||||
|
||||
func _erase_wall_region(world_rect: Rect2) -> int:
|
||||
if _world == null or world_rect.size.x <= 0.0 or world_rect.size.y <= 0.0:
|
||||
return 0
|
||||
@@ -695,7 +730,7 @@ func _can_apply_single_scale(snapshot: Dictionary, factor: float) -> bool:
|
||||
var next_position: Vector2 = (snapshot["pivot"] as Vector2) + ((snapshot["position"] as Vector2) - (snapshot["pivot"] as Vector2)) * factor
|
||||
if not next_position.is_finite():
|
||||
return false
|
||||
if node.get_script() == WALL_PANEL:
|
||||
if node is StageRectSurface:
|
||||
var next_size: Vector2 = (snapshot["panel_size"] as Vector2) * factor
|
||||
return next_size.is_finite() and next_size.x >= MIN_PANEL_SIZE and next_size.y >= MIN_PANEL_SIZE
|
||||
var start_scale: Vector2 = snapshot["scale"] as Vector2
|
||||
@@ -707,10 +742,10 @@ func _apply_single_scale_to_snapshot(snapshot: Dictionary, factor: float, pivot:
|
||||
var node: Node2D = snapshot["node"] as Node2D
|
||||
node.global_position = pivot + ((snapshot["position"] as Vector2) - pivot) * factor
|
||||
node.global_rotation = float(snapshot["rotation"])
|
||||
if node.get_script() == WALL_PANEL:
|
||||
var wall: StageWallPanel = node as StageWallPanel
|
||||
wall.scale = snapshot["scale"] as Vector2
|
||||
wall.configure((snapshot["panel_size"] as Vector2) * factor, wall.finish_id, wall.tint)
|
||||
if node is StageRectSurface:
|
||||
var surface: StageRectSurface = node as StageRectSurface
|
||||
surface.scale = snapshot["scale"] as Vector2
|
||||
surface.configure((snapshot["panel_size"] as Vector2) * factor, surface.finish_id, surface.tint)
|
||||
else:
|
||||
node.scale = (snapshot["scale"] as Vector2) * factor
|
||||
|
||||
@@ -722,9 +757,9 @@ func _restore_single_snapshot() -> void:
|
||||
node.global_position = _single_snapshot["position"] as Vector2
|
||||
node.global_rotation = float(_single_snapshot["rotation"])
|
||||
node.scale = _single_snapshot["scale"] as Vector2
|
||||
if node.get_script() == WALL_PANEL:
|
||||
var wall: StageWallPanel = node as StageWallPanel
|
||||
wall.configure(_single_snapshot["panel_size"] as Vector2, wall.finish_id, wall.tint)
|
||||
if node is StageRectSurface:
|
||||
var surface: StageRectSurface = node as StageRectSurface
|
||||
surface.configure(_single_snapshot["panel_size"] as Vector2, surface.finish_id, surface.tint)
|
||||
|
||||
|
||||
func _single_transform_changed() -> bool:
|
||||
@@ -739,14 +774,14 @@ func _snapshot_matches_current(snapshot: Dictionary) -> bool:
|
||||
return false
|
||||
if not is_equal_approx(node.global_rotation, float(snapshot["rotation"])) or not node.scale.is_equal_approx(snapshot["scale"] as Vector2):
|
||||
return false
|
||||
if node.get_script() == WALL_PANEL:
|
||||
return (node as StageWallPanel).panel_size.is_equal_approx(snapshot["panel_size"] as Vector2)
|
||||
if node is StageRectSurface:
|
||||
return (node as StageRectSurface).panel_size.is_equal_approx(snapshot["panel_size"] as Vector2)
|
||||
return true
|
||||
|
||||
|
||||
func _world_bounds(node: Node2D) -> Rect2:
|
||||
if node.get_script() == WALL_PANEL:
|
||||
return node.global_transform * (node as StageWallPanel).get_local_bounds()
|
||||
if node is StageRectSurface:
|
||||
return node.global_transform * (node as StageRectSurface).get_local_bounds()
|
||||
if node.get_script() == SCENERY_ARTWORK:
|
||||
return (node as StageSceneryArtwork).get_visual_world_bounds()
|
||||
return STAGE_SELECTION.get_world_aabb(node)
|
||||
@@ -789,7 +824,7 @@ func _snap_visual_center(node: Node2D, desired_center: Vector2) -> Vector2:
|
||||
|
||||
|
||||
func _move_node_visible_top_left(node: Node2D, desired_top_left: Vector2) -> Vector2:
|
||||
if node.get_script() not in [WALL_PANEL, SCENERY_ARTWORK]:
|
||||
if not (node is StageRectSurface) and node.get_script() != SCENERY_ARTWORK:
|
||||
return desired_top_left
|
||||
var bounds: Rect2 = _world_bounds(node)
|
||||
return node.global_position + desired_top_left - bounds.position
|
||||
|
||||
@@ -12,9 +12,11 @@ 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
|
||||
|
||||
var _stage_theme: SandboxTheme
|
||||
var _top_bar_height: float = 64.0
|
||||
var _layout_stash: Node
|
||||
var _horizontal_layout: HBoxContainer
|
||||
var _vertical_layout: VBoxContainer
|
||||
var _wrapped_layout: VBoxContainer
|
||||
@@ -76,6 +78,7 @@ func set_capture_actions(prompt: String, actions: Array[Dictionary]) -> void:
|
||||
var button := Button.new()
|
||||
button.name = "Action_%s" % String(action_id)
|
||||
button.text = String(action.get("label", action_id))
|
||||
button.tooltip_text = button.text
|
||||
button.custom_minimum_size.x = ACTION_BUTTON_WIDTH
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.pressed.connect(func() -> void: capture_action_requested.emit(action_id))
|
||||
@@ -154,15 +157,22 @@ func apply_theme() -> void:
|
||||
|
||||
func _build_controls() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_layout_stash = Node.new()
|
||||
_layout_stash.name = "LayoutStash"
|
||||
add_child(_layout_stash)
|
||||
_horizontal_layout = HBoxContainer.new()
|
||||
_horizontal_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_horizontal_layout.add_theme_constant_override("separation", 20)
|
||||
_vertical_layout = VBoxContainer.new()
|
||||
_vertical_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_vertical_layout.add_theme_constant_override("separation", 12)
|
||||
_vertical_layout.visible = false
|
||||
_wrapped_layout = VBoxContainer.new()
|
||||
_wrapped_layout.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_wrapped_layout.add_theme_constant_override("separation", 10)
|
||||
_wrapped_layout.visible = false
|
||||
_layout_stash.add_child(_vertical_layout)
|
||||
_layout_stash.add_child(_wrapped_layout)
|
||||
_active_layout = _horizontal_layout
|
||||
add_child(_active_layout)
|
||||
_prompt = Label.new()
|
||||
@@ -178,8 +188,11 @@ func _build_controls() -> void:
|
||||
_actions_row.alignment = BoxContainer.ALIGNMENT_END
|
||||
_actions_column = VBoxContainer.new()
|
||||
_actions_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_actions_column.visible = false
|
||||
_layout_stash.add_child(_actions_column)
|
||||
_actions_parent = _actions_row
|
||||
_active_layout.add_child(_actions_parent)
|
||||
_actions_row.visible = true
|
||||
_change_prop_button = Button.new()
|
||||
_change_prop_button.name = "ChangeProp"
|
||||
_change_prop_button.text = "Change prop"
|
||||
@@ -250,8 +263,13 @@ func _layout_capture_bar() -> void:
|
||||
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(MAX_BAR_WIDTH, minf(viewport_size.x * VIEWPORT_WIDTH_RATIO, available_width))
|
||||
var narrow_compact: bool = width < NARROW_COMPACT_WIDTH
|
||||
custom_minimum_size.x = width
|
||||
size.x = width
|
||||
clip_contents = true
|
||||
_prompt.add_theme_font_size_override("font_size", 18 if narrow_compact else clampi(_integer("stage_panels.type.title_size", 24, 18, 36) + 4, 24, 34))
|
||||
_prompt.max_lines_visible = 2 if narrow_compact else 3
|
||||
_set_compact_panel_style(narrow_compact)
|
||||
var left: float = safe_left + (available_width - width) * 0.5 if not _has_horizontal_safe_bounds else safe_left
|
||||
position = Vector2(left, _top_bar_height + 56.0)
|
||||
var many_actions: bool = _custom_action_buttons.size() > 3
|
||||
@@ -259,6 +277,7 @@ func _layout_capture_bar() -> void:
|
||||
var wrapped: bool = (_has_horizontal_safe_bounds and width < HORIZONTAL_LAYOUT_MIN_WIDTH and width >= ACTION_ROW_MIN_WIDTH) or (many_actions and not stack_actions)
|
||||
_set_layout(width < HORIZONTAL_LAYOUT_MIN_WIDTH or wrapped, stack_actions, wrapped)
|
||||
_fit_custom_action_widths(width, wrapped)
|
||||
_apply_compact_action_labels(narrow_compact)
|
||||
call_deferred("_fit_height_to_content")
|
||||
|
||||
|
||||
@@ -273,8 +292,12 @@ func _set_layout(compact: bool, stack_actions: bool, wrapped: bool = false) -> v
|
||||
previous_actions.remove_child(_change_prop_button)
|
||||
previous_actions.remove_child(_cancel_button)
|
||||
_active_layout.remove_child(previous_actions)
|
||||
previous_actions.visible = false
|
||||
_layout_stash.add_child(previous_actions)
|
||||
_actions_parent = target_actions
|
||||
_layout_stash.remove_child(_actions_parent)
|
||||
_active_layout.add_child(_actions_parent)
|
||||
_actions_parent.visible = true
|
||||
_actions_parent.add_child(_change_prop_button)
|
||||
_actions_parent.add_child(_cancel_button)
|
||||
for button: Button in _custom_action_buttons:
|
||||
@@ -282,9 +305,13 @@ func _set_layout(compact: bool, stack_actions: bool, wrapped: bool = false) -> v
|
||||
if _active_layout != target_layout:
|
||||
_active_layout.remove_child(_prompt)
|
||||
_active_layout.remove_child(_actions_parent)
|
||||
_active_layout.visible = false
|
||||
remove_child(_active_layout)
|
||||
_layout_stash.add_child(_active_layout)
|
||||
_active_layout = target_layout
|
||||
_layout_stash.remove_child(_active_layout)
|
||||
add_child(_active_layout)
|
||||
_active_layout.visible = true
|
||||
_active_layout.add_child(_prompt)
|
||||
_active_layout.add_child(_actions_parent)
|
||||
_change_prop_button.custom_minimum_size.x = 0.0 if stack_actions else ACTION_BUTTON_WIDTH
|
||||
@@ -297,18 +324,60 @@ func _fit_custom_action_widths(width: float, wrapped: bool) -> void:
|
||||
var button_width: float = ACTION_BUTTON_WIDTH
|
||||
var body_size: int = clampi(_integer("stage_panels.type.body_size", 20, 16, 32) + 2, 20, 30)
|
||||
var action_font_size: int = maxi(18, body_size - (4 if wrapped else 0))
|
||||
if width < NARROW_COMPACT_WIDTH:
|
||||
button_width = 0.0
|
||||
action_font_size = 16
|
||||
if wrapped and not _custom_action_buttons.is_empty():
|
||||
var gaps: float = float(maxi(0, _custom_action_buttons.size() - 1) * 12)
|
||||
button_width = minf(ACTION_BUTTON_WIDTH, maxf(144.0, (width - 44.0 - gaps) / float(_custom_action_buttons.size())))
|
||||
for button: Button in _custom_action_buttons:
|
||||
button.custom_minimum_size.x = button_width
|
||||
button.add_theme_font_size_override("font_size", action_font_size)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL if width < NARROW_COMPACT_WIDTH else Control.SIZE_FILL
|
||||
|
||||
|
||||
func _apply_compact_action_labels(compact: bool) -> void:
|
||||
var compact_labels: Dictionary = {
|
||||
"select_several": "Multi",
|
||||
"select_all": "All",
|
||||
"clear_selection": "Clear",
|
||||
"done_edit": "Done",
|
||||
"move_group": "Move",
|
||||
"resize_group": "Size",
|
||||
"keep_group": "Keep",
|
||||
"cancel_group": "Cancel",
|
||||
"done_tool": "Done",
|
||||
"cancel_tool": "Cancel",
|
||||
"cancel_single": "Cancel",
|
||||
"depth_far": "Far",
|
||||
"depth_near": "Near",
|
||||
"depth_floor": "Floor",
|
||||
}
|
||||
for button: Button in _custom_action_buttons:
|
||||
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
|
||||
|
||||
|
||||
func _set_compact_panel_style(compact: bool) -> void:
|
||||
var panel_style: StyleBoxFlat = get_theme_stylebox("panel") as StyleBoxFlat
|
||||
if panel_style == null:
|
||||
return
|
||||
panel_style.border_width_left = 4 if compact else 8
|
||||
panel_style.content_margin_left = 8.0 if compact else 20.0
|
||||
panel_style.content_margin_right = 8.0 if compact else 18.0
|
||||
panel_style.content_margin_top = 8.0 if compact else 14.0
|
||||
panel_style.content_margin_bottom = 8.0 if compact else 14.0
|
||||
|
||||
|
||||
func _fit_height_to_content() -> void:
|
||||
if not is_inside_tree() or not visible:
|
||||
return
|
||||
custom_minimum_size.y = get_combined_minimum_size().y
|
||||
var target_height: float = _active_layout.get_combined_minimum_size().y
|
||||
var panel_style: StyleBox = get_theme_stylebox("panel")
|
||||
if panel_style != null:
|
||||
target_height += panel_style.get_minimum_size().y
|
||||
custom_minimum_size.y = target_height
|
||||
size.y = custom_minimum_size.y
|
||||
|
||||
|
||||
|
||||
@@ -166,8 +166,8 @@ func _build_shell() -> void:
|
||||
|
||||
|
||||
func _fit_to_content() -> void:
|
||||
_fit_pending = false
|
||||
if not is_inside_tree() or _body == null:
|
||||
_fit_pending = false
|
||||
return
|
||||
var rect := _safe_rect
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
@@ -179,7 +179,16 @@ func _fit_to_content() -> void:
|
||||
size.x = card_width
|
||||
await get_tree().process_frame
|
||||
if not is_inside_tree():
|
||||
_fit_pending = false
|
||||
return
|
||||
# A resize can arrive while the first layout pass is waiting for the body
|
||||
# minimum size. Re-read the safe rectangle so this pass uses the new bounds.
|
||||
rect = _safe_rect
|
||||
if rect.size.x <= 0.0 or rect.size.y <= 0.0:
|
||||
rect = get_viewport_rect()
|
||||
card_width = minf(preferred_width, maxf(1.0, rect.size.x - inset * 2.0))
|
||||
custom_minimum_size.x = card_width
|
||||
size.x = card_width
|
||||
var padding := float(_integer("stage_panels.shell.padding", 12, 4, 28))
|
||||
var gap := float(_integer("stage_panels.shell.header_gap", 8, 0, 32))
|
||||
var header_height := maxf(44.0, _header.get_combined_minimum_size().y)
|
||||
@@ -189,10 +198,13 @@ func _fit_to_content() -> void:
|
||||
var natural_height := fixed_height + body_height
|
||||
var card_height := minf(natural_height, available_height)
|
||||
var scroll_height := maxf(0.0, card_height - fixed_height)
|
||||
position = Vector2(rect.position.x + rect.size.x - inset - card_width, rect.position.y + inset)
|
||||
size = Vector2(card_width, card_height)
|
||||
# Update the scroll minimum before shrinking the PanelContainer. Otherwise its
|
||||
# previous full-content minimum can clamp the new height until another layout.
|
||||
_scroll.custom_minimum_size.y = scroll_height
|
||||
_scroll.update_minimum_size()
|
||||
position = Vector2(rect.position.x + rect.size.x - inset - card_width, rect.position.y + inset)
|
||||
size = Vector2(card_width, card_height)
|
||||
_fit_pending = false
|
||||
|
||||
|
||||
func _style_action_button(button: Button, prominent: bool) -> void:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
class_name StageSurfaceColorControls
|
||||
extends VBoxContainer
|
||||
## Shared wrapping Color history and preset controls for scenery surfaces.
|
||||
|
||||
signal color_selected(color: Color)
|
||||
signal custom_color_requested
|
||||
|
||||
const COLOR_SWATCH_SELECTOR := preload("res://scripts/shared/ui/color_swatch_selector.gd")
|
||||
|
||||
var _stage_theme: SandboxTheme
|
||||
var _selected: Color = Color.WHITE
|
||||
var _recents: Array[Color] = []
|
||||
var _presets: Array[Color] = []
|
||||
var _built: bool = false
|
||||
var _current_chip: Button
|
||||
var _current_value: Label
|
||||
var _recent_empty_label: Label
|
||||
var _recent_swatches: ColorSwatchSelector
|
||||
var _preset_swatches: ColorSwatchSelector
|
||||
var _more_button: Button
|
||||
|
||||
|
||||
func configure(selected: Color, recents: Array[Color], presets: Array[Color], stage_theme: SandboxTheme) -> void:
|
||||
_stage_theme = stage_theme
|
||||
_selected = selected
|
||||
_recents = recents.duplicate()
|
||||
_presets = presets.duplicate()
|
||||
if not _built:
|
||||
_build()
|
||||
_refresh_visuals()
|
||||
|
||||
|
||||
func set_selected_no_signal(color: Color) -> void:
|
||||
_selected = color
|
||||
_refresh_current_color()
|
||||
_recent_swatches.set_selected(color)
|
||||
_preset_swatches.set_selected(color)
|
||||
|
||||
|
||||
func set_recent_colors(colors: Array[Color]) -> void:
|
||||
if _recents == colors:
|
||||
return
|
||||
_recents = colors.duplicate()
|
||||
_recent_empty_label.visible = _recents.is_empty()
|
||||
_recent_swatches.visible = not _recents.is_empty()
|
||||
_recent_swatches.configure(_recents, _selected, _stage_theme)
|
||||
|
||||
|
||||
func focus_custom_button() -> void:
|
||||
if _more_button != null:
|
||||
_more_button.grab_focus()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_built = true
|
||||
add_theme_constant_override("separation", _integer("stage_surface_colors.section_gap", 8, 0, 28))
|
||||
var current_heading: Label = _make_heading("Color")
|
||||
add_child(current_heading)
|
||||
var current_row := HBoxContainer.new()
|
||||
current_row.add_theme_constant_override("separation", _integer("stage_surface_colors.section_gap", 8, 0, 28))
|
||||
add_child(current_row)
|
||||
_current_chip = Button.new()
|
||||
var swatch_size: float = float(_integer("stage_surface_colors.swatch_size", 48, 44, 96))
|
||||
_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_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"))
|
||||
_recent_empty_label = Label.new()
|
||||
_recent_empty_label.text = "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)
|
||||
_recent_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
|
||||
_recent_swatches.color_selected.connect(_on_swatch_selected)
|
||||
add_child(_recent_swatches)
|
||||
add_child(_make_heading("Colors"))
|
||||
_preset_swatches = COLOR_SWATCH_SELECTOR.new() as ColorSwatchSelector
|
||||
_preset_swatches.color_selected.connect(_on_swatch_selected)
|
||||
add_child(_preset_swatches)
|
||||
_more_button = Button.new()
|
||||
_more_button.text = "More colors…"
|
||||
_more_button.custom_minimum_size.y = float(_integer("stage_panels.action.min_target_size", 48, 44, 96))
|
||||
_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())
|
||||
|
||||
|
||||
func _refresh_visuals() -> void:
|
||||
if not _built:
|
||||
return
|
||||
_refresh_current_color()
|
||||
_recent_empty_label.visible = _recents.is_empty()
|
||||
_recent_swatches.visible = not _recents.is_empty()
|
||||
_recent_swatches.configure(_recents, _selected, _stage_theme)
|
||||
_preset_swatches.configure(_presets, _selected, _stage_theme)
|
||||
|
||||
|
||||
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.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())
|
||||
|
||||
|
||||
func _on_swatch_selected(color: Color) -> void:
|
||||
_selected = color
|
||||
_refresh_current_color()
|
||||
_recent_swatches.set_selected(color)
|
||||
_preset_swatches.set_selected(color)
|
||||
color_selected.emit(color)
|
||||
|
||||
|
||||
func _make_heading(text_value: String) -> Label:
|
||||
var heading := Label.new()
|
||||
heading.text = text_value
|
||||
heading.add_theme_font_size_override("font_size", _integer("stage_panels.type.section_size", 20, 12, 40))
|
||||
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())
|
||||
return heading
|
||||
|
||||
|
||||
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 @@
|
||||
uid://d37coqfbju18u
|
||||
@@ -0,0 +1,160 @@
|
||||
class_name StageSurfaceColorDialog
|
||||
extends Window
|
||||
## Bounded custom-color dialog with a live Stage surface preview.
|
||||
|
||||
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")
|
||||
|
||||
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 _built: bool = false
|
||||
|
||||
|
||||
func configure(surface_kind: StringName, finish: StringName, color: Color, stage_theme: SandboxTheme) -> void:
|
||||
_surface_kind = surface_kind
|
||||
_finish_id = finish
|
||||
_initial_color = color
|
||||
_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
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if visible and event is InputEventKey and (event as InputEventKey).pressed and not (event as InputEventKey).echo and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
_cancel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
_built = true
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
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")))
|
||||
|
||||
|
||||
func _on_picker_color_changed(color: Color) -> void:
|
||||
_update_preview(color)
|
||||
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
|
||||
hide()
|
||||
color_confirmed.emit(confirmed_color)
|
||||
|
||||
|
||||
func _cancel() -> void:
|
||||
_update_preview(_initial_color)
|
||||
hide()
|
||||
cancelled.emit()
|
||||
|
||||
|
||||
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())
|
||||
if usable.size.x > 0 and usable.size.y > 0:
|
||||
available.x = mini(available.x, usable.size.x)
|
||||
available.y = mini(available.y, usable.size.y)
|
||||
available.x = maxi(1, available.x - 32)
|
||||
available.y = maxi(1, available.y - 32)
|
||||
return available
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c56y4dpi7w2pu
|
||||
@@ -50,7 +50,7 @@ func configure(payload: Dictionary, icon_texture: Texture2D = null, theme: Varia
|
||||
if not description.is_empty():
|
||||
_description = Label.new()
|
||||
_description.text = description
|
||||
_description.custom_minimum_size = Vector2(0.0, 34.0)
|
||||
_description.custom_minimum_size = Vector2(0.0, float(_payload.get("description_min_height", 34.0)))
|
||||
_description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_description.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
|
||||
@@ -7,16 +7,27 @@ signal browse_requested()
|
||||
signal refresh_requested()
|
||||
signal make_artwork_requested()
|
||||
signal edit_scenery_requested()
|
||||
signal surface_color_confirmed(color: Color)
|
||||
|
||||
const COLUMNS := 4
|
||||
const ROWS := 3
|
||||
const PAGE_SIZE := COLUMNS * ROWS
|
||||
const GRID_H_SEPARATION: float = 8.0
|
||||
const GRID_V_SEPARATION: float = 8.0
|
||||
const POPUP_SIDE_PADDING: float = 40.0
|
||||
const POPUP_VERTICAL_PADDING: float = 32.0
|
||||
const POPUP_SAFE_SCREEN_MARGIN: float = 24.0
|
||||
|
||||
const CELL_MIN_SIZE := Vector2(190.0, 220.0)
|
||||
const THUMB_SIZE := Vector2(156.0, 156.0)
|
||||
const ASSET_CARD := preload("res://scripts/shared/ui/asset_card.gd")
|
||||
const BACKGROUND_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_background_card_preview.gd")
|
||||
const STARTER_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_starter_card_preview.gd")
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const SURFACE_PREVIEW := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
|
||||
const SURFACE_COLOR_CONTROLS := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd")
|
||||
const SURFACE_COLOR_DIALOG := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd")
|
||||
const STAGE_PANEL_CHOICE_GROUP := preload("res://scripts/shared/stage_ui/stage_panel_choice_group.gd")
|
||||
|
||||
var kind: String = ""
|
||||
|
||||
@@ -32,9 +43,24 @@ var _gallery_tab: String = "backgrounds"
|
||||
var _tag_id: String = "all"
|
||||
var _current_sky_id: String = ""
|
||||
var _sky_card_group: ButtonGroup
|
||||
var _active_columns: int = COLUMNS
|
||||
var _page_capacity: int = PAGE_SIZE
|
||||
var _effective_cell_size: Vector2 = CELL_MIN_SIZE
|
||||
var _surface_color_history: Array[Color] = []
|
||||
var _setup_view: VBoxContainer
|
||||
var _setup_kind: StringName = &""
|
||||
var _setup_finish: StringName = &""
|
||||
var _setup_color: Color = Color.WHITE
|
||||
var _setup_original_color: Color = Color.WHITE
|
||||
var _setup_color_chosen: bool = false
|
||||
var _setup_preview: StageSurfacePreview
|
||||
var _setup_color_controls: StageSurfaceColorControls
|
||||
var _setup_color_dialog: StageSurfaceColorDialog
|
||||
|
||||
@onready var _title_label: Label = %TitleLabel
|
||||
@onready var _grid: GridContainer = %GridContainer
|
||||
@onready var _content_stack: VBoxContainer = %ContentStack
|
||||
@onready var _grid_scroll: ScrollContainer = %GridScroll
|
||||
@onready var _empty_label: Label = %EmptyLabel
|
||||
@onready var _page_label: Label = %PageLabel
|
||||
@onready var _prev_button: Button = %PrevButton
|
||||
@@ -48,10 +74,13 @@ var _sky_card_group: ButtonGroup
|
||||
@onready var _tag_filter: OptionButton = %TagFilter
|
||||
@onready var _make_artwork_button: Button = %MakeArtworkButton
|
||||
@onready var _edit_scenery_button: Button = %EditSceneryButton
|
||||
@onready var _title_bar: Control = $MarginContainer/VBoxContainer/TitleBar
|
||||
@onready var _footer: Control = $MarginContainer/VBoxContainer/Footer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
exclusive = true
|
||||
_build_surface_setup_view()
|
||||
if _tag_filter.item_count == 0:
|
||||
for label: String in ["All artwork", "Nature", "Buildings", "Walls", "Roads", "Objects"]:
|
||||
_tag_filter.add_item(label)
|
||||
@@ -70,11 +99,15 @@ func _ready() -> void:
|
||||
|
||||
func _on_root_size_changed() -> void:
|
||||
if visible:
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
popup_centered()
|
||||
|
||||
|
||||
func open(p_kind: String, entries: Array[Dictionary], current_sky_id: String = "") -> void:
|
||||
kind = p_kind
|
||||
_setup_kind = &""
|
||||
_setup_view.visible = false
|
||||
_current_sky_id = current_sky_id if kind == "scenery" else ""
|
||||
_title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop"))
|
||||
_tabs.visible = kind == "scenery"
|
||||
@@ -100,9 +133,16 @@ func set_entries(entries: Array[Dictionary]) -> void:
|
||||
_thumbnails.clear()
|
||||
_cell_texrects.clear()
|
||||
_page = 0
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
|
||||
|
||||
func set_surface_color_history(colors: Array[Color]) -> void:
|
||||
_surface_color_history = colors.duplicate()
|
||||
if _setup_color_controls != null and is_instance_valid(_setup_color_controls):
|
||||
_setup_color_controls.set_recent_colors(_surface_color_history)
|
||||
|
||||
|
||||
func set_thumbnail(entry: Dictionary, tex: Texture2D) -> void:
|
||||
var key := _entry_key(entry)
|
||||
_thumbnails[key] = tex
|
||||
@@ -144,6 +184,221 @@ func apply_theme(theme: Variant) -> void:
|
||||
var thumbnail_size: Variant = theme.value("popups.asset_selector.thumbnail_size", THUMB_SIZE)
|
||||
if thumbnail_size is Array and (thumbnail_size as Array).size() == 2:
|
||||
_thumbnail_size = Vector2(float(thumbnail_size[0]), float(thumbnail_size[1]))
|
||||
var wall_choices: StagePanelChoiceGroup = get_meta("wall_look_group", null) as StagePanelChoiceGroup
|
||||
var ground_choices: StagePanelChoiceGroup = get_meta("ground_look_group", null) as StagePanelChoiceGroup
|
||||
if wall_choices != null:
|
||||
wall_choices.apply_theme(theme as SandboxTheme)
|
||||
if ground_choices != null:
|
||||
ground_choices.apply_theme(theme as SandboxTheme)
|
||||
if _setup_color_controls != null:
|
||||
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), theme as SandboxTheme)
|
||||
|
||||
|
||||
func _build_surface_setup_view() -> void:
|
||||
_setup_view = VBoxContainer.new()
|
||||
_setup_view.name = "SurfaceSetupView"
|
||||
_setup_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_setup_view.add_theme_constant_override("separation", _theme_integer("popups.asset_selector.setup_gap", 10, 0, 32))
|
||||
_setup_view.visible = false
|
||||
_content_stack.add_child(_setup_view)
|
||||
var heading_row := HBoxContainer.new()
|
||||
heading_row.add_theme_constant_override("separation", 8)
|
||||
_setup_view.add_child(heading_row)
|
||||
var back_button := Button.new()
|
||||
back_button.text = "‹ Back"
|
||||
back_button.custom_minimum_size = Vector2(float(_theme_integer("stage_panels.action.close_width", 92, 72, 180)), float(_theme_integer("stage_panels.action.min_target_size", 48, 44, 96)))
|
||||
back_button.focus_mode = Control.FOCUS_ALL
|
||||
back_button.pressed.connect(_back_from_surface_setup)
|
||||
heading_row.add_child(back_button)
|
||||
var heading := Label.new()
|
||||
heading.name = "SetupHeading"
|
||||
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
heading.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.title_size", 24, 12, 48))
|
||||
heading_row.add_child(heading)
|
||||
var look_heading := Label.new()
|
||||
look_heading.text = "Look"
|
||||
look_heading.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.section_size", 20, 12, 40))
|
||||
_setup_view.add_child(look_heading)
|
||||
var wall_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup
|
||||
wall_choices.name = "WallLookChoices"
|
||||
wall_choices.add_choice(&"finish_wall_paint", "Paint", _make_look_cue(&"wall", &"finish_wall_paint"))
|
||||
wall_choices.add_choice(&"finish_wall_brick", "Brick", _make_look_cue(&"wall", &"finish_wall_brick"))
|
||||
wall_choices.add_choice(&"finish_wall_wood", "Wood", _make_look_cue(&"wall", &"finish_wall_wood"))
|
||||
wall_choices.choice_selected.connect(_on_setup_look_choice)
|
||||
_setup_view.add_child(wall_choices)
|
||||
var ground_choices: StagePanelChoiceGroup = STAGE_PANEL_CHOICE_GROUP.new() as StagePanelChoiceGroup
|
||||
ground_choices.name = "BackgroundGroundLookChoices"
|
||||
ground_choices.add_choice(&"finish_background_grass", "Grass", _make_look_cue(&"background_ground", &"finish_background_grass"))
|
||||
ground_choices.add_choice(&"finish_background_concrete", "Concrete", _make_look_cue(&"background_ground", &"finish_background_concrete"))
|
||||
ground_choices.add_choice(&"finish_background_carpet", "Carpet", _make_look_cue(&"background_ground", &"finish_background_carpet"))
|
||||
ground_choices.choice_selected.connect(_on_setup_look_choice)
|
||||
_setup_view.add_child(ground_choices)
|
||||
set_meta("wall_look_group", wall_choices)
|
||||
set_meta("ground_look_group", ground_choices)
|
||||
var preview_frame := PanelContainer.new()
|
||||
preview_frame.name = "SurfacePreviewFrame"
|
||||
var preview_size: Vector2 = _theme_vector2("popups.asset_selector.surface_preview_size", Vector2(420.0, 132.0))
|
||||
preview_frame.custom_minimum_size.y = preview_size.y + 10.0
|
||||
preview_frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_setup_preview = SURFACE_PREVIEW.new() as StageSurfacePreview
|
||||
_setup_preview.custom_minimum_size = preview_size
|
||||
_setup_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
preview_frame.add_child(_setup_preview)
|
||||
_setup_view.add_child(preview_frame)
|
||||
_setup_color_controls = SURFACE_COLOR_CONTROLS.new() as StageSurfaceColorControls
|
||||
_setup_color_controls.name = "SurfaceColorControls"
|
||||
_setup_color_controls.color_selected.connect(_on_setup_color_selected)
|
||||
_setup_color_controls.custom_color_requested.connect(_open_setup_color_dialog)
|
||||
_setup_view.add_child(_setup_color_controls)
|
||||
var helper := Label.new()
|
||||
helper.name = "SurfaceRoleHelper"
|
||||
helper.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
helper.add_theme_color_override("font_color", _theme_color("global.muted_text_color", Color("#AAB2C0")))
|
||||
_setup_view.add_child(helper)
|
||||
var draw_button := Button.new()
|
||||
draw_button.name = "DrawSurfaceButton"
|
||||
draw_button.custom_minimum_size.y = float(_theme_integer("stage_panels.choice.minimum_height", 56, 44, 112))
|
||||
draw_button.focus_mode = Control.FOCUS_ALL
|
||||
draw_button.pressed.connect(_draw_selected_surface)
|
||||
_setup_view.add_child(draw_button)
|
||||
set_meta("setup_heading", heading)
|
||||
set_meta("setup_helper", helper)
|
||||
set_meta("setup_draw_button", draw_button)
|
||||
_apply_font_to(back_button)
|
||||
_apply_font_to(heading)
|
||||
_apply_font_to(look_heading)
|
||||
_apply_font_to(helper)
|
||||
_apply_font_to(draw_button)
|
||||
if _sandbox_theme != null:
|
||||
draw_button.add_theme_stylebox_override("normal", _sandbox_theme.stylebox("stage_panels.action"))
|
||||
draw_button.add_theme_stylebox_override("hover", _sandbox_theme.stylebox("stage_panels.action"))
|
||||
draw_button.add_theme_color_override("font_color", _theme_color("stage_panels.type.text_color", Color("#F5F7FA")))
|
||||
draw_button.add_theme_font_size_override("font_size", _theme_integer("stage_panels.type.body_size", 20, 14, 40))
|
||||
for choice: StagePanelChoiceGroup in [wall_choices, ground_choices]:
|
||||
choice.apply_theme(_sandbox_theme as SandboxTheme)
|
||||
_apply_font_to(choice)
|
||||
|
||||
|
||||
func _make_look_cue(kind: StringName, finish: StringName) -> Control:
|
||||
var preview: StageSurfacePreview = SURFACE_PREVIEW.new() as StageSurfacePreview
|
||||
preview.custom_minimum_size = _theme_vector2("popups.asset_selector.look_cue_size", Vector2(80.0, 42.0))
|
||||
preview.configure(kind, finish, SURFACE_RENDERER.default_tint(finish))
|
||||
return preview
|
||||
|
||||
|
||||
func _open_surface_setup(kind: StringName) -> void:
|
||||
_setup_kind = kind
|
||||
_setup_color_chosen = false
|
||||
_setup_finish = &"finish_wall_paint" if kind == &"wall" else &"finish_background_grass"
|
||||
_setup_color = SURFACE_RENDERER.default_tint(_setup_finish)
|
||||
_setup_original_color = _setup_color
|
||||
var wall_choices: StagePanelChoiceGroup = get_meta("wall_look_group") as StagePanelChoiceGroup
|
||||
var ground_choices: StagePanelChoiceGroup = get_meta("ground_look_group") as StagePanelChoiceGroup
|
||||
wall_choices.visible = kind == &"wall"
|
||||
ground_choices.visible = kind == &"background_ground"
|
||||
wall_choices.select_no_signal(_setup_finish)
|
||||
ground_choices.select_no_signal(_setup_finish)
|
||||
(get_meta("setup_heading") as Label).text = "Wall panel" if kind == &"wall" else "Background ground"
|
||||
(get_meta("setup_helper") as Label).text = "Looks like a wall · Stickmen can pass through." if kind == &"wall" else "Decorative only · Stickmen don’t walk here."
|
||||
(get_meta("setup_draw_button") as Button).text = "Draw wall panel" if kind == &"wall" else "Draw background ground"
|
||||
_setup_color_controls.visible = _supports_setup_color()
|
||||
_setup_color_controls.configure(_setup_color, _surface_color_history, _preset_surface_colors(), _sandbox_theme as SandboxTheme)
|
||||
_update_setup_preview()
|
||||
_page = 0
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
back_button_focus()
|
||||
|
||||
|
||||
func back_button_focus() -> void:
|
||||
var back_button: Button = _setup_view.get_child(0).get_child(0) as Button
|
||||
if back_button != null:
|
||||
back_button.grab_focus()
|
||||
|
||||
|
||||
func _back_from_surface_setup() -> void:
|
||||
_setup_kind = &""
|
||||
_setup_view.visible = false
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
_close_button.grab_focus()
|
||||
|
||||
|
||||
func _on_setup_look_choice(id: StringName) -> void:
|
||||
var supported: bool = SURFACE_RENDERER.is_wall_finish(id) if _setup_kind == &"wall" else SURFACE_RENDERER.is_background_finish(id)
|
||||
if not supported:
|
||||
return
|
||||
_setup_finish = id
|
||||
if not _setup_color_chosen:
|
||||
_setup_color = SURFACE_RENDERER.default_tint(_setup_finish)
|
||||
_setup_color_controls.visible = _supports_setup_color()
|
||||
_setup_color_controls.set_selected_no_signal(_setup_color)
|
||||
_update_setup_preview()
|
||||
_update_popup_layout()
|
||||
|
||||
|
||||
func _supports_setup_color() -> bool:
|
||||
return _setup_kind == &"wall" or _setup_finish == &"finish_background_carpet"
|
||||
|
||||
|
||||
func _on_setup_color_selected(color: Color) -> void:
|
||||
_setup_color = Color(color.r, color.g, color.b, 1.0)
|
||||
_setup_color_chosen = true
|
||||
_update_setup_preview()
|
||||
surface_color_confirmed.emit(_setup_color)
|
||||
|
||||
|
||||
func _update_setup_preview() -> void:
|
||||
if _setup_preview == null:
|
||||
return
|
||||
_setup_preview.configure(_setup_kind, _setup_finish, _setup_color)
|
||||
_setup_color_controls.set_selected_no_signal(_setup_color)
|
||||
|
||||
|
||||
func _preset_surface_colors() -> Array[Color]:
|
||||
if _sandbox_theme == null:
|
||||
return [Color.WHITE, Color.BLACK, Color("#4C9AFF"), Color("#29A36A"), Color("#FFD84D"), Color("#D94F70")]
|
||||
return (_sandbox_theme as SandboxTheme).swatches()
|
||||
|
||||
|
||||
func _open_setup_color_dialog() -> void:
|
||||
if _setup_color_dialog == null or not is_instance_valid(_setup_color_dialog):
|
||||
_setup_color_dialog = SURFACE_COLOR_DIALOG.new() as StageSurfaceColorDialog
|
||||
add_child(_setup_color_dialog)
|
||||
_setup_color_dialog.preview_color_changed.connect(_on_setup_color_preview)
|
||||
_setup_color_dialog.color_confirmed.connect(_on_setup_color_confirmed)
|
||||
_setup_color_dialog.cancelled.connect(_on_setup_color_cancelled)
|
||||
_setup_original_color = _setup_color
|
||||
_setup_color_dialog.configure(_setup_kind, _setup_finish, _setup_original_color, _sandbox_theme as SandboxTheme)
|
||||
_setup_color_dialog.open_dialog()
|
||||
|
||||
|
||||
func _on_setup_color_preview(color: Color) -> void:
|
||||
_setup_color = Color(color.r, color.g, color.b, 1.0)
|
||||
_update_setup_preview()
|
||||
|
||||
|
||||
func _on_setup_color_confirmed(color: Color) -> void:
|
||||
_setup_color = Color(color.r, color.g, color.b, 1.0)
|
||||
_setup_color_chosen = true
|
||||
_update_setup_preview()
|
||||
surface_color_confirmed.emit(_setup_color)
|
||||
_setup_color_controls.focus_custom_button()
|
||||
|
||||
|
||||
func _on_setup_color_cancelled() -> void:
|
||||
_setup_color = _setup_original_color
|
||||
_setup_color_controls.set_selected_no_signal(_setup_color)
|
||||
_update_setup_preview()
|
||||
_setup_color_controls.focus_custom_button()
|
||||
|
||||
|
||||
func _draw_selected_surface() -> void:
|
||||
if _setup_kind.is_empty():
|
||||
return
|
||||
var background_kind: String = "wall" if _setup_kind == &"wall" else "background_ground"
|
||||
item_selected.emit({"background_kind": background_kind, "finish_id": String(_setup_finish), "color": _setup_color})
|
||||
|
||||
var _cell_min_size: Vector2 = CELL_MIN_SIZE
|
||||
var _thumbnail_size: Vector2 = THUMB_SIZE
|
||||
@@ -176,10 +431,13 @@ func _on_tag_filter_selected(index: int) -> void:
|
||||
var ids: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"]
|
||||
_tag_id = ids[clampi(index, 0, ids.size() - 1)]
|
||||
_page = 0
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
|
||||
|
||||
func _set_gallery_tab(tab: String) -> void:
|
||||
_setup_kind = &""
|
||||
_setup_view.visible = false
|
||||
_gallery_tab = tab
|
||||
_backgrounds_tab.set_pressed_no_signal(tab == "backgrounds")
|
||||
_artwork_tab.set_pressed_no_signal(tab == "artwork")
|
||||
@@ -190,6 +448,7 @@ func _set_gallery_tab(tab: String) -> void:
|
||||
_refresh_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork")
|
||||
_empty_label.text = "No artwork in this group yet." if tab == "artwork" else "Choose a Sky look or wall finish."
|
||||
_page = 0
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
|
||||
|
||||
@@ -211,12 +470,14 @@ func _filtered_entries() -> Array[Dictionary]:
|
||||
func _on_prev() -> void:
|
||||
if _page > 0:
|
||||
_page -= 1
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
|
||||
|
||||
func _on_next() -> void:
|
||||
if (_page + 1) * PAGE_SIZE < _filtered_entries().size():
|
||||
if (_page + 1) * _page_capacity < _filtered_entries().size():
|
||||
_page += 1
|
||||
_update_popup_layout()
|
||||
_rebuild()
|
||||
|
||||
|
||||
@@ -224,7 +485,55 @@ func _page_count() -> int:
|
||||
var entries: Array[Dictionary] = _filtered_entries()
|
||||
if entries.is_empty():
|
||||
return 1
|
||||
return int(ceil(float(entries.size()) / float(PAGE_SIZE)))
|
||||
return int(ceil(float(entries.size()) / float(_page_capacity)))
|
||||
|
||||
|
||||
func _usable_screen_size() -> Vector2:
|
||||
var root_window: Window = get_tree().root
|
||||
var available: Vector2 = Vector2(root_window.size)
|
||||
var screen_index: int = DisplayServer.window_get_current_screen()
|
||||
var usable_rect: Rect2i = DisplayServer.screen_get_usable_rect(screen_index)
|
||||
if usable_rect.size.x > 0 and usable_rect.size.y > 0:
|
||||
available.x = minf(available.x, float(usable_rect.size.x))
|
||||
available.y = minf(available.y, float(usable_rect.size.y))
|
||||
return available
|
||||
|
||||
|
||||
func _update_popup_layout() -> void:
|
||||
var available: Vector2 = _usable_screen_size()
|
||||
var max_width: float = maxf(1.0, minf(available.x * 0.94, available.x - POPUP_SAFE_SCREEN_MARGIN))
|
||||
var max_height: float = maxf(1.0, minf(available.y * 0.90, available.y - POPUP_SAFE_SCREEN_MARGIN))
|
||||
var preferred_width: float = CELL_MIN_SIZE.x * COLUMNS + GRID_H_SEPARATION * (COLUMNS - 1) + POPUP_SIDE_PADDING
|
||||
var popup_width: float = minf(preferred_width, max_width)
|
||||
var grid_width: float = maxf(1.0, popup_width - POPUP_SIDE_PADDING)
|
||||
_active_columns = clampi(floori((grid_width + GRID_H_SEPARATION) / (CELL_MIN_SIZE.x + GRID_H_SEPARATION)), 1, COLUMNS)
|
||||
_page_capacity = _active_columns * ROWS
|
||||
_grid.columns = _active_columns
|
||||
var card_width: float = minf(CELL_MIN_SIZE.x, (grid_width - GRID_H_SEPARATION * (_active_columns - 1)) / float(_active_columns))
|
||||
var card_height_limit: float = 248.0 if kind == "starters" else CELL_MIN_SIZE.y
|
||||
var card_height: float = minf(card_height_limit, card_width + (58.0 if kind == "starters" else 30.0))
|
||||
_effective_cell_size = Vector2(maxf(96.0, card_width), maxf(180.0, card_height))
|
||||
var filtered_count: int = _filtered_entries().size()
|
||||
var page_count: int = maxi(1, int(ceil(float(filtered_count) / float(_page_capacity))))
|
||||
_page = clampi(_page, 0, page_count - 1)
|
||||
var page_start: int = _page * _page_capacity
|
||||
var page_items: int = clampi(filtered_count - page_start, 0, _page_capacity)
|
||||
var grid_rows: int = int(ceil(float(page_items) / float(_active_columns))) if page_items > 0 else 0
|
||||
var body_height: float
|
||||
if not _setup_kind.is_empty():
|
||||
body_height = maxf(180.0, _setup_view.get_combined_minimum_size().y)
|
||||
else:
|
||||
body_height = maxf(120.0, float(grid_rows) * _effective_cell_size.y + float(maxi(0, grid_rows - 1)) * GRID_V_SEPARATION)
|
||||
if page_items == 0:
|
||||
body_height = maxf(body_height, _empty_label.get_combined_minimum_size().y)
|
||||
var fixed_height: float = _title_bar.get_combined_minimum_size().y + _footer.get_combined_minimum_size().y + POPUP_VERTICAL_PADDING
|
||||
var visible_sections: int = 3
|
||||
if _tabs.visible:
|
||||
fixed_height += _tabs.get_combined_minimum_size().y
|
||||
visible_sections += 1
|
||||
fixed_height += float(visible_sections - 1) * 8.0
|
||||
var popup_height: float = minf(fixed_height + body_height, max_height)
|
||||
size = Vector2i(maxi(1, roundi(popup_width)), maxi(1, roundi(popup_height)))
|
||||
|
||||
|
||||
func _rebuild() -> void:
|
||||
@@ -233,19 +542,30 @@ func _rebuild() -> void:
|
||||
c.queue_free()
|
||||
_cell_texrects.clear()
|
||||
var filtered: Array[Dictionary] = _filtered_entries()
|
||||
_empty_label.visible = filtered.is_empty()
|
||||
_grid.visible = not filtered.is_empty()
|
||||
var in_setup: bool = not _setup_kind.is_empty()
|
||||
_setup_view.visible = in_setup
|
||||
_empty_label.visible = filtered.is_empty() and not in_setup
|
||||
_grid.visible = not filtered.is_empty() and not in_setup
|
||||
_grid_scroll.visible = not filtered.is_empty() or in_setup
|
||||
var total := _page_count()
|
||||
var single := total <= 1
|
||||
_prev_button.visible = not single
|
||||
_next_button.visible = not single
|
||||
_page_label.visible = not single
|
||||
_prev_button.disabled = _page <= 0
|
||||
_next_button.disabled = (_page + 1) * PAGE_SIZE >= filtered.size()
|
||||
_next_button.disabled = (_page + 1) * _page_capacity >= filtered.size()
|
||||
_page_label.text = "Page %d/%d" % [_page + 1, total]
|
||||
if in_setup:
|
||||
_prev_button.visible = false
|
||||
_next_button.visible = false
|
||||
_page_label.visible = false
|
||||
_browse_button.visible = false
|
||||
_refresh_button.visible = false
|
||||
_make_artwork_button.visible = false
|
||||
return
|
||||
if filtered.is_empty():
|
||||
return
|
||||
var bounds := page_bounds(filtered.size(), _page)
|
||||
var bounds := page_bounds(filtered.size(), _page, _page_capacity)
|
||||
for i: int in range(int(bounds["start"]), int(bounds["end"])):
|
||||
_grid.add_child(_build_cell(filtered[i]))
|
||||
|
||||
@@ -257,12 +577,16 @@ func _build_cell(entry: Dictionary) -> Control:
|
||||
# opening a selector never implies that one asset is pre-selected.
|
||||
var categories: Array = entry.get("tags", []) as Array
|
||||
var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", "")))
|
||||
var card_size: Vector2 = _cell_min_size
|
||||
var thumb_size: Vector2 = _thumbnail_size
|
||||
var card_size: Vector2 = _effective_cell_size
|
||||
var is_background_ground_setup: bool = String(entry.get("background_kind", "")) == "background_ground_setup"
|
||||
var thumb_height_budget: float = card_size.y - (74.0 if is_background_ground_setup else 64.0)
|
||||
var thumb_edge: float = minf(_thumbnail_size.x, minf(card_size.x - 20.0, thumb_height_budget))
|
||||
var thumb_size := Vector2.ONE * maxf(72.0, thumb_edge)
|
||||
if kind == "starters":
|
||||
card_size.y = maxf(card_size.y, 248.0)
|
||||
thumb_size = Vector2(128.0, 128.0)
|
||||
thumb_size = Vector2.ONE * minf(128.0, maxf(72.0, thumb_edge))
|
||||
var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": String(entry.get("description", "")), "thumbnail_size": thumb_size}
|
||||
if is_background_ground_setup:
|
||||
card_payload["description_min_height"] = 42.0
|
||||
if entry.has("background_kind"):
|
||||
card_payload["background_kind"] = entry["background_kind"]
|
||||
card_payload["finish_id"] = entry.get("finish_id", "")
|
||||
@@ -301,7 +625,27 @@ func _theme_color(path: String, fallback: Color) -> Color:
|
||||
return fallback
|
||||
|
||||
|
||||
func _theme_integer(path: String, fallback: int, minimum: int, maximum: int) -> int:
|
||||
return fallback if _sandbox_theme == null else _sandbox_theme.integer(path, fallback, minimum, maximum)
|
||||
|
||||
|
||||
func _theme_vector2(path: String, fallback: Vector2) -> Vector2:
|
||||
if _sandbox_theme == null:
|
||||
return fallback
|
||||
var raw: Variant = _sandbox_theme.value(path, [fallback.x, fallback.y])
|
||||
if not raw is Array or (raw as Array).size() != 2:
|
||||
return fallback
|
||||
return Vector2(float((raw as Array)[0]), float((raw as Array)[1]))
|
||||
|
||||
|
||||
func _on_cell_pressed(entry: Dictionary) -> void:
|
||||
var background_kind: String = String(entry.get("background_kind", ""))
|
||||
if background_kind == "wall_setup":
|
||||
_open_surface_setup(&"wall")
|
||||
return
|
||||
if background_kind == "background_ground_setup":
|
||||
_open_surface_setup(&"background_ground")
|
||||
return
|
||||
item_selected.emit(entry)
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,122 @@
|
||||
class_name ColorSwatchSelector
|
||||
extends HBoxContainer
|
||||
extends FlowContainer
|
||||
|
||||
signal color_selected(color: Color)
|
||||
|
||||
var _selected: Color = Color.WHITE
|
||||
var _group: ButtonGroup = ButtonGroup.new()
|
||||
var _buttons: Array[Button] = []
|
||||
var _stage_theme: SandboxTheme
|
||||
var _colors: Array[Color] = []
|
||||
|
||||
|
||||
func configure(colors: Array[Color], selected: Color, theme: Variant = null) -> void:
|
||||
for child: Node in get_children(): child.queue_free()
|
||||
_stage_theme = theme as SandboxTheme if theme is SandboxTheme else null
|
||||
_selected = selected
|
||||
for color: Color in colors:
|
||||
var button := Button.new(); button.custom_minimum_size = Vector2(44,44); button.toggle_mode = true; button.button_group = _group; button.tooltip_text = color.to_html(true)
|
||||
var box := StyleBoxFlat.new(); box.bg_color = color; box.set_corner_radius_all(22); button.add_theme_stylebox_override("normal", box)
|
||||
button.pressed.connect(func() -> void: set_selected(color, true)); add_child(button)
|
||||
button.set_pressed_no_signal(color.is_equal_approx(selected))
|
||||
_colors = colors.duplicate()
|
||||
var focus_key: String = ""
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner() if is_inside_tree() else null
|
||||
if focus_owner != null and focus_owner.get_parent() == self:
|
||||
var focused_color: Color = focus_owner.get_meta("surface_color", Color.WHITE)
|
||||
focus_key = "#" + focused_color.to_html(false).to_upper()
|
||||
_group = ButtonGroup.new()
|
||||
_group.allow_unpress = true
|
||||
_buttons.clear()
|
||||
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 seen: Dictionary = {}
|
||||
for color: Color in _colors:
|
||||
var color_key: String = color.to_html(true).to_upper()
|
||||
if seen.has(color_key):
|
||||
continue
|
||||
seen[color_key] = true
|
||||
var button := Button.new()
|
||||
var swatch_size: float = 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
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
button.tooltip_text = "Color #%s" % color.to_html(false).to_upper()
|
||||
button.set_meta("surface_color", color)
|
||||
button.pressed.connect(_on_pressed.bind(button))
|
||||
add_child(button)
|
||||
_buttons.append(button)
|
||||
_apply_button_style(button, color)
|
||||
_sync_pressed_state()
|
||||
if not focus_key.is_empty():
|
||||
call_deferred("_restore_color_focus", focus_key)
|
||||
|
||||
|
||||
func set_selected(color: Color, emit_signal: bool = false) -> void:
|
||||
_selected = color
|
||||
if emit_signal: color_selected.emit(color)
|
||||
func get_selected() -> Color: return _selected
|
||||
_sync_pressed_state()
|
||||
if emit_signal:
|
||||
color_selected.emit(color)
|
||||
|
||||
|
||||
func get_selected() -> Color:
|
||||
return _selected
|
||||
|
||||
|
||||
func _restore_color_focus(color_key: String) -> void:
|
||||
for button: Button in _buttons:
|
||||
var button_color: Color = button.get_meta("surface_color", Color.WHITE)
|
||||
if "#" + button_color.to_html(false).to_upper() == color_key:
|
||||
button.grab_focus()
|
||||
return
|
||||
|
||||
|
||||
func _on_pressed(button: Button) -> void:
|
||||
var color: Color = button.get_meta("surface_color", Color.WHITE)
|
||||
set_selected(color, true)
|
||||
|
||||
|
||||
func _sync_pressed_state() -> void:
|
||||
for button: Button in _buttons:
|
||||
var color: Color = button.get_meta("surface_color", Color.WHITE)
|
||||
button.set_pressed_no_signal(color.to_html(true) == _selected.to_html(true))
|
||||
button.text = "✓" if color.to_html(true) == _selected.to_html(true) else ""
|
||||
var label_color: Color = Color("#111821") if color.get_luminance() > 0.58 else Color("#FFFFFF")
|
||||
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)
|
||||
|
||||
|
||||
func _apply_button_style(button: Button, color: Color) -> void:
|
||||
var border: Color = _color("stage_surface_colors.swatch_border", Color("#FFFFFFB8"))
|
||||
var accent: Color = _color("mode_colors.edit", Color("#22C6FF"))
|
||||
var corner: int = _integer("stage_surface_colors.swatch_corner_radius", 8)
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = color
|
||||
normal.border_color = border
|
||||
normal.set_border_width_all(_integer("stage_surface_colors.swatch_border_width", 2))
|
||||
normal.set_corner_radius_all(corner)
|
||||
var selected := normal.duplicate() as StyleBoxFlat
|
||||
selected.border_color = accent
|
||||
selected.set_border_width_all(_integer("stage_surface_colors.selected_border_width", 4))
|
||||
var hover := normal.duplicate() as StyleBoxFlat
|
||||
hover.border_color = Color.WHITE
|
||||
hover.set_border_width_all(_integer("stage_surface_colors.focus_border_width", 3))
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
button.add_theme_stylebox_override("hover", hover)
|
||||
button.add_theme_stylebox_override("pressed", selected)
|
||||
button.add_theme_stylebox_override("hover_pressed", selected.duplicate())
|
||||
var focus := normal.duplicate() as StyleBoxFlat
|
||||
focus.draw_center = false
|
||||
focus.border_color = _color("stage_surface_colors.swatch_focus_border", Color.WHITE)
|
||||
focus.set_border_width_all(_integer("stage_surface_colors.focus_border_width", 3))
|
||||
button.add_theme_stylebox_override("focus", focus)
|
||||
if _stage_theme != null and _stage_theme.font() != null:
|
||||
button.add_theme_font_override("font", _stage_theme.font())
|
||||
button.add_theme_font_size_override("font_size", _integer("stage_surface_colors.swatch_check_size", 24))
|
||||
|
||||
|
||||
func _integer(path: String, fallback: int) -> int:
|
||||
return fallback if _stage_theme == null else _stage_theme.integer(path, fallback, 0, 64)
|
||||
|
||||
|
||||
func _color(path: String, fallback: Color) -> Color:
|
||||
return fallback if _stage_theme == null else _stage_theme.color(path, fallback)
|
||||
|
||||
@@ -13,7 +13,7 @@ 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": {}, "asset_selector": {"backdrop_alpha": 0.55, "cell_size":[190,220], "thumbnail_size":[156,156]}},
|
||||
"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}},
|
||||
"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"},
|
||||
@@ -24,6 +24,7 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"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}
|
||||
},
|
||||
"stage_surface_colors": {"section_gap": 8, "swatch_gap": 8, "swatch_size": 48, "swatch_border": "#FFFFFFB8", "swatch_focus_border": "#FFFFFF", "swatch_corner_radius": 8, "swatch_border_width": 2, "swatch_check_size": 24, "selected_border_width": 4, "focus_border_width": 3},
|
||||
"overlays": {"style_defaults": {"line_width": 2, "dash_length": 8, "dash_gap": 5}, "terrain_guide": {"color": "#22C6FF"}, "completed_path_alpha": 0.20, "completed_path_color": "#959EAD", "rule_connector_color": "#AAB2C0", "rule_action_color": "#7E57C2", "valid": {"color": "#4DE66680"}, "same_type": {"color": "#FFFFFF26"}, "invalid": {"color": "#FF4D4D80"}, "set_down": {"unknown_color": "#C5CAD4", "reach_color": "#FFB300", "ghost_alpha": 0.36}, "use_preview": {"valid_color": "#42D887", "invalid_color": "#F06464", "uncertain_color": "#FFB300", "neutral_color": "#C5CAD4", "completion_duration": 0.6}},
|
||||
"widgets": {"swatches": ["#FFFFFF", "#111111", "#22C6FF", "#FFB300", "#33DD77", "#FF4D4D"], "size_presets": [{"id":"small", "label":"Small", "value":20}, {"id":"medium", "label":"Medium", "value":28}, {"id":"huge", "label":"HUGE", "value":40}], "emoji": ["😀", "😂", "😍", "😮", "😢", "😡", "👍", "❤️"]},
|
||||
"speech": {"toolbar": {"surface":"#1C2431F7", "border":"#9EADBF", "selection_halo":"#FFD84D", "gap":10, "max_width":480, "min_hit_height":48}, "ui": {"panel_width": 440, "button_min_height": 44, "heading_size": 24}, "layout": {"max_width": 420, "stack_gap": 12, "screen_margin": 18, "bubble_padding": 18}, "animation": {"letter_interval": 0.045, "word_interval": 0.20, "pop_duration": 0.24}, "styles": {"talk": {"fill":"#FFFDF6", "outline":"#202020", "tail":"talk"}, "shout": {"fill":"#FFD642", "outline":"#D92F26", "tail":"shout"}, "think": {"fill":"#E9F5FF", "outline":"#37698B", "tail":"think"}, "whisper": {"fill":"#F2F2F2", "outline":"#6D7885", "tail":"whisper"}, "sfx": {"fill":"#FFDD39", "outline":"#D92F26", "tail":"none"}}, "text_colors": [{"name":"Ink","color":"#202020"},{"name":"Midnight","color":"#111827"},{"name":"Slate","color":"#475569"},{"name":"Snow","color":"#FFFFFF"},{"name":"Cloud","color":"#CBD5E1"},{"name":"Sky","color":"#2196F3"},{"name":"Ocean","color":"#0077B6"},{"name":"Teal","color":"#00897B"},{"name":"Leaf","color":"#2E7D32"},{"name":"Lime","color":"#8BC34A"},{"name":"Sunshine","color":"#FFC107"},{"name":"Orange","color":"#F57C00"},{"name":"Sunset red","color":"#E53935"},{"name":"Rose","color":"#D81B60"},{"name":"Violet","color":"#7E57C2"},{"name":"Brown","color":"#795548"}], "preview_ghost":"#FFFFFF99", "connector":"#FFFFFFB0"}
|
||||
|
||||
@@ -37,9 +37,37 @@ func configure(entry: Dictionary) -> void:
|
||||
func _draw() -> void:
|
||||
if _background_kind == "wall" and SURFACE_RENDERER.is_wall_finish(_preview_id):
|
||||
var wall_tint: Color = Color("#7FA36C") if _preview_id == &"finish_wall_paint" else Color.WHITE
|
||||
SURFACE_RENDERER.draw_wall(self, Rect2(Vector2.ZERO, size), _preview_id, wall_tint, Vector2.ZERO)
|
||||
# The preview rect is already small and clipped, so draw its full pattern directly.
|
||||
SURFACE_RENDERER.draw_wall(
|
||||
self,
|
||||
Rect2(Vector2.ZERO, size),
|
||||
_preview_id,
|
||||
wall_tint,
|
||||
Vector2.ZERO,
|
||||
Vector2.ONE,
|
||||
false
|
||||
)
|
||||
elif _background_kind == "wall_setup":
|
||||
var finishes: Array[StringName] = [&"finish_wall_paint", &"finish_wall_brick", &"finish_wall_wood"]
|
||||
_draw_finish_samples(finishes, true)
|
||||
elif _background_kind == "background_ground_setup":
|
||||
var finishes: Array[StringName] = [&"finish_background_grass", &"finish_background_concrete", &"finish_background_carpet"]
|
||||
_draw_finish_samples(finishes, false)
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw_finish_samples(finishes: Array[StringName], wall: bool) -> void:
|
||||
var gap: float = 2.0
|
||||
var sample_width: float = (size.x - gap * float(finishes.size() - 1)) / float(finishes.size())
|
||||
for index: int in finishes.size():
|
||||
var sample_rect := Rect2(Vector2(float(index) * (sample_width + gap), 0.0), Vector2(sample_width, size.y))
|
||||
var finish: StringName = finishes[index]
|
||||
var tint: Color = SURFACE_RENDERER.default_tint(finish)
|
||||
if wall:
|
||||
SURFACE_RENDERER.draw_wall(self, sample_rect, finish, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_background(self, sample_rect, finish, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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")
|
||||
|
||||
|
||||
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 _draw() -> void:
|
||||
SURFACE_RENDERER.draw_background(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
|
||||
@@ -0,0 +1 @@
|
||||
uid://bl3tfgsieeq5x
|
||||
@@ -0,0 +1,39 @@
|
||||
class_name StageRectSurface
|
||||
extends Node2D
|
||||
## Shared rectangular geometry and transient tint preview for inert Stage scenery.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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))
|
||||
finish_id = finish
|
||||
tint = color
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_local_bounds() -> Rect2:
|
||||
return Rect2(-panel_size * 0.5, panel_size)
|
||||
|
||||
|
||||
func set_preview_tint(color: Color) -> void:
|
||||
_preview_tint = color
|
||||
_has_preview_tint = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func clear_preview_tint() -> void:
|
||||
_has_preview_tint = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_render_tint() -> Color:
|
||||
return _preview_tint if _has_preview_tint else tint
|
||||
|
||||
|
||||
func get_surface_draw_origin_world() -> Vector2:
|
||||
return global_position
|
||||
@@ -0,0 +1 @@
|
||||
uid://sk8pkbk41ef0
|
||||
@@ -0,0 +1,28 @@
|
||||
class_name StageSurfacePreview
|
||||
extends Control
|
||||
## Small render-only preview that uses the Stage surface motif renderer.
|
||||
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
|
||||
var surface_kind: StringName = &"wall"
|
||||
var finish_id: StringName = &"finish_wall_paint"
|
||||
var tint: Color = Color("#5A9D70")
|
||||
|
||||
|
||||
func configure(kind: StringName, finish: StringName, color: Color) -> void:
|
||||
surface_kind = kind
|
||||
finish_id = finish
|
||||
tint = color
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
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:
|
||||
return
|
||||
if surface_kind == &"background_ground":
|
||||
SURFACE_RENDERER.draw_background(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
else:
|
||||
SURFACE_RENDERER.draw_wall(self, rect, finish_id, tint, Vector2.ZERO, Vector2.ONE, false)
|
||||
draw_rect(rect, Color("#F5F7FA"), false, 2.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://kyveykcipe5c
|
||||
@@ -3,17 +3,37 @@ extends RefCounted
|
||||
## World-aligned, bounded vector motifs for finite walls and Ground.
|
||||
|
||||
const WALL_FINISHES: Array[StringName] = [&"finish_wall_paint", &"finish_wall_brick", &"finish_wall_wood"]
|
||||
const BACKGROUND_FINISHES: Array[StringName] = [&"finish_background_grass", &"finish_background_concrete", &"finish_background_carpet"]
|
||||
const GROUND_FINISHES: Array[StringName] = [&"finish_ground_paint", &"finish_ground_brick", &"finish_ground_wood", &"finish_ground_grass", &"finish_ground_dirt", &"finish_ground_sidewalk", &"finish_ground_asphalt"]
|
||||
const MAX_CELLS: int = 1200
|
||||
const BRICK_WIDTH: float = 82.0
|
||||
const BRICK_HEIGHT: float = 34.0
|
||||
const MAX_VISIBLE_BRICK_CELLS: int = 12000
|
||||
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
|
||||
|
||||
|
||||
static func is_wall_finish(id: StringName) -> bool:
|
||||
return WALL_FINISHES.has(id)
|
||||
|
||||
|
||||
static func is_background_finish(id: StringName) -> bool:
|
||||
return BACKGROUND_FINISHES.has(id)
|
||||
|
||||
|
||||
static func default_tint(finish: StringName) -> Color:
|
||||
match finish:
|
||||
&"finish_wall_paint": return Color("#5A9D70")
|
||||
&"finish_wall_brick": return Color("#B95F3D")
|
||||
&"finish_wall_wood": return Color("#C87137")
|
||||
&"finish_background_grass": return Color("#72A84C")
|
||||
&"finish_background_concrete": return Color("#A6AAA3")
|
||||
&"finish_background_carpet": return Color("#8B537B")
|
||||
_: return Color("#5A9D70")
|
||||
|
||||
|
||||
static func is_ground_finish(id: StringName) -> bool:
|
||||
return GROUND_FINISHES.has(id) or id == &"finish_ground_legacy"
|
||||
|
||||
@@ -24,9 +44,10 @@ static func draw_wall(
|
||||
finish: StringName,
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE
|
||||
drawing_scale: Vector2 = Vector2.ONE,
|
||||
cull_to_viewport: bool = true
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true)
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, true, false, cull_to_viewport)
|
||||
|
||||
|
||||
static func draw_ground(
|
||||
@@ -37,7 +58,19 @@ static func draw_ground(
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false)
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, false, true)
|
||||
|
||||
|
||||
static func draw_background(
|
||||
canvas: CanvasItem,
|
||||
rect: Rect2,
|
||||
finish: StringName,
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2 = Vector2.ONE,
|
||||
cull_to_viewport: bool = true
|
||||
) -> void:
|
||||
_draw_surface(canvas, rect, finish, tint, world_origin, drawing_scale, false, true, cull_to_viewport)
|
||||
|
||||
|
||||
static func _draw_surface(
|
||||
@@ -47,7 +80,9 @@ static func _draw_surface(
|
||||
tint: Color,
|
||||
world_origin: Vector2,
|
||||
drawing_scale: Vector2,
|
||||
wall: bool
|
||||
wall: bool,
|
||||
background: bool,
|
||||
cull_to_viewport: bool
|
||||
) -> void:
|
||||
var normalized := rect.abs()
|
||||
if normalized.size.x <= 0.0 or normalized.size.y <= 0.0:
|
||||
@@ -57,11 +92,15 @@ static func _draw_surface(
|
||||
if finish == &"finish_ground_legacy":
|
||||
base = Color("#468B42")
|
||||
elif finish.ends_with("brick"):
|
||||
base = Color("#B95F3D") if wall else Color("#994D35")
|
||||
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":
|
||||
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":
|
||||
@@ -70,13 +109,39 @@ static func _draw_surface(
|
||||
base = Color("#505A62")
|
||||
canvas.draw_rect(normalized, base, true)
|
||||
if finish.ends_with("brick"):
|
||||
var view_rect: Rect2 = _visible_local_rect(canvas)
|
||||
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 = brick_detail_scale_for_view(view_rect.size if not view_rect.size.is_zero_approx() else normalized.size, motif_scale)
|
||||
_draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale)
|
||||
var detail_size: Vector2 = view_rect.size if not view_rect.size.is_zero_approx() else normalized.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)
|
||||
else:
|
||||
_draw_bricks(canvas, pattern_rect, world_origin, base, motif_scale, detail_scale)
|
||||
elif finish.ends_with("wood"):
|
||||
_draw_wood(canvas, normalized, world_origin, base, motif_scale)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
elif finish == &"finish_ground_grass":
|
||||
_draw_grass(canvas, normalized, world_origin, motif_scale)
|
||||
elif finish == &"finish_ground_dirt":
|
||||
@@ -89,20 +154,529 @@ static func _draw_surface(
|
||||
push_warning("StageSurfaceRenderer: unknown finish '%s'; drawing paint." % String(finish))
|
||||
|
||||
|
||||
static func brick_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> float:
|
||||
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))
|
||||
var safe_motif_scale: Vector2 = _normalized_scale(motif_scale)
|
||||
var brick_width: float = WALL_BRICK_WIDTH if wall else BRICK_WIDTH
|
||||
var brick_height: float = WALL_BRICK_HEIGHT if wall else BRICK_HEIGHT
|
||||
var max_cells: int = MAX_WALL_BRICK_CELLS if wall else MAX_VISIBLE_BRICK_CELLS
|
||||
for _iteration: int in 8:
|
||||
var columns: int = int(ceil(safe_size.x * safe_motif_scale.x / (brick_width * scale))) + 2
|
||||
var rows: int = int(ceil(safe_size.y * safe_motif_scale.y / (brick_height * scale))) + 2
|
||||
var cell_count: int = rows * columns
|
||||
if cell_count <= max_cells:
|
||||
return scale
|
||||
scale *= sqrt(float(cell_count) / float(max_cells)) * 1.01
|
||||
return scale
|
||||
|
||||
|
||||
static func wood_detail_scale_for_view(view_size: Vector2, motif_scale: Vector2 = Vector2.ONE) -> float:
|
||||
var scale: float = 1.0
|
||||
var safe_size: Vector2 = Vector2(maxf(1.0, view_size.x), maxf(1.0, view_size.y))
|
||||
var safe_motif_scale: Vector2 = _normalized_scale(motif_scale)
|
||||
for _iteration: int in 8:
|
||||
var columns: int = int(ceil(safe_size.x * safe_motif_scale.x / (BRICK_WIDTH * scale))) + 2
|
||||
var rows: int = int(ceil(safe_size.y * safe_motif_scale.y / (BRICK_HEIGHT * scale))) + 2
|
||||
var columns: int = int(ceil(safe_size.x * safe_motif_scale.x / (320.0 * scale))) + 2
|
||||
var rows: int = int(ceil(safe_size.y * safe_motif_scale.y / (64.0 * scale))) + 2
|
||||
var cell_count: int = rows * columns
|
||||
if cell_count <= MAX_VISIBLE_BRICK_CELLS:
|
||||
if cell_count <= MAX_CELLS:
|
||||
return scale
|
||||
scale *= sqrt(float(cell_count) / float(MAX_VISIBLE_BRICK_CELLS)) * 1.01
|
||||
scale *= sqrt(float(cell_count) / float(MAX_CELLS)) * 1.01
|
||||
return scale
|
||||
|
||||
|
||||
static func _draw_wall_wood(canvas: CanvasItem, rect: Rect2, origin: Vector2, motif_scale: Vector2, detail_scale: float, tint: Color) -> void:
|
||||
const BOARD_HEIGHT: float = 64.0
|
||||
const PLANK_WIDTH: float = 320.0
|
||||
var safe_scale: Vector2 = _normalized_scale(motif_scale)
|
||||
var board_world_height: float = BOARD_HEIGHT * detail_scale
|
||||
var plank_world_width: float = PLANK_WIDTH * detail_scale
|
||||
var world_left: float = rect.position.x * safe_scale.x + origin.x
|
||||
var world_right: float = rect.end.x * safe_scale.x + origin.x
|
||||
var world_top: float = rect.position.y * safe_scale.y + origin.y
|
||||
var world_bottom: float = rect.end.y * safe_scale.y + origin.y
|
||||
var first_row: int = floori(world_top / board_world_height)
|
||||
var last_row: int = floori((world_bottom - 0.001) / board_world_height)
|
||||
var row_count: int = mini(last_row - first_row + 1, MAX_CELLS)
|
||||
var natural: Color = default_tint(&"finish_wall_wood")
|
||||
var seam_color: Color = _material_color(Color("#784421"), tint, natural)
|
||||
var grain_color: Color = _material_color(Color("#A05A2C"), tint, natural)
|
||||
var highlight_color: Color = _material_color(Color("#E39A59"), tint, natural)
|
||||
for row_offset: int in row_count:
|
||||
var row: int = first_row + row_offset
|
||||
var board_world_top: float = float(row) * board_world_height
|
||||
var board_local_top: float = (board_world_top - origin.y) / safe_scale.y
|
||||
var stagger: float = plank_world_width * 0.5 if posmod(row, 2) == 1 else 0.0
|
||||
var first_column: int = floori((world_left - stagger) / plank_world_width)
|
||||
var last_column: int = floori((world_right - stagger - 0.001) / plank_world_width)
|
||||
var column_count: int = mini(last_column - first_column + 1, MAX_CELLS)
|
||||
for column_offset: int in column_count:
|
||||
var column: int = first_column + column_offset
|
||||
var plank_world_left: float = float(column) * plank_world_width + stagger
|
||||
var plank_rect := Rect2(
|
||||
Vector2((plank_world_left - origin.x) / safe_scale.x, board_local_top),
|
||||
Vector2(plank_world_width / safe_scale.x, board_world_height / safe_scale.y)
|
||||
)
|
||||
var visible_plank: Rect2 = rect.intersection(plank_rect)
|
||||
if visible_plank.size.x <= 0.0 or visible_plank.size.y <= 0.0:
|
||||
continue
|
||||
var tone_index: int = posmod(row + column, 3)
|
||||
var plank_color: Color = _material_color(Color("#C87137"), tint, natural)
|
||||
if tone_index == 1:
|
||||
plank_color = plank_color.darkened(0.045)
|
||||
elif tone_index == 2:
|
||||
plank_color = plank_color.lightened(0.035)
|
||||
canvas.draw_rect(visible_plank, plank_color, true)
|
||||
var plank_local_left: float = (plank_world_left - origin.x) / safe_scale.x
|
||||
_draw_clipped_line(
|
||||
canvas, rect,
|
||||
Vector2(plank_local_left, board_local_top),
|
||||
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
|
||||
)
|
||||
_draw_clipped_line(
|
||||
canvas, rect,
|
||||
Vector2(rect.position.x, board_local_top), Vector2(rect.end.x, board_local_top),
|
||||
seam_color, 2.4 / safe_scale.y
|
||||
)
|
||||
_draw_clipped_line(
|
||||
canvas, rect,
|
||||
Vector2(rect.position.x, board_local_top + 2.4 / safe_scale.y),
|
||||
Vector2(rect.end.x, board_local_top + 2.4 / safe_scale.y),
|
||||
_color_with_alpha(highlight_color, 0.62), 1.0 / safe_scale.y
|
||||
)
|
||||
|
||||
|
||||
static func _draw_wall_wood_grain(
|
||||
canvas: CanvasItem,
|
||||
clip_rect: Rect2,
|
||||
plank_world_left: float,
|
||||
board_world_top: float,
|
||||
plank_world_width: float,
|
||||
board_world_height: float,
|
||||
origin: Vector2,
|
||||
motif_scale: Vector2,
|
||||
row: int,
|
||||
column: int,
|
||||
grain_color: Color,
|
||||
highlight_color: Color,
|
||||
detail_scale: float,
|
||||
tint: Color,
|
||||
natural: Color
|
||||
) -> void:
|
||||
const TRACK_COUNT: int = 5
|
||||
const SAMPLES: int = 32
|
||||
var seed: int = row * 73856093 + column * 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
|
||||
var amplitude: float = (0.55 + _wood_hash(track_seed + 11) * 2.4) * detail_scale
|
||||
var cycles: float = 1.0 + _wood_hash(track_seed + 23) * 2.6
|
||||
var phase: float = _wood_hash(track_seed + 37) * TAU
|
||||
var points := PackedVector2Array()
|
||||
for sample: int in SAMPLES + 1:
|
||||
var t: float = float(sample) / float(SAMPLES)
|
||||
var wave: float = sin(t * TAU * cycles + phase) * amplitude
|
||||
wave += sin(t * TAU * (cycles * 0.43 + 0.4) + phase * 1.7) * amplitude * 0.32
|
||||
var world_x: float = plank_world_left + t * plank_world_width
|
||||
var world_y: float = board_world_top + lane * board_world_height + wave
|
||||
points.append(Vector2((world_x - origin.x) / motif_scale.x, (world_y - origin.y) / motif_scale.y))
|
||||
_draw_clipped_polyline(canvas, clip_rect, points, _color_with_alpha(grain_color, 0.19), 5.0 * detail_scale / motif_scale.y)
|
||||
_draw_clipped_polyline(canvas, clip_rect, points, _color_with_alpha(grain_color, 0.58), 1.15 * detail_scale / motif_scale.y)
|
||||
var highlight_points := PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
highlight_points.append(point + Vector2(0.0, -1.0 * detail_scale / motif_scale.y))
|
||||
_draw_clipped_polyline(canvas, clip_rect, highlight_points, _color_with_alpha(highlight_color, 0.32), 0.9 * detail_scale / motif_scale.y)
|
||||
|
||||
|
||||
static func _draw_wall_wood_knot(
|
||||
canvas: CanvasItem,
|
||||
clip_rect: Rect2,
|
||||
plank_world_left: float,
|
||||
board_world_top: float,
|
||||
plank_world_width: float,
|
||||
board_world_height: float,
|
||||
origin: Vector2,
|
||||
motif_scale: Vector2,
|
||||
row: int,
|
||||
column: int,
|
||||
detail_scale: float,
|
||||
tint: Color,
|
||||
natural: Color
|
||||
) -> void:
|
||||
var seed: int = row * 92821 + column * 68917 + 31
|
||||
if _wood_hash(seed) > 0.24:
|
||||
return
|
||||
var center_world := Vector2(
|
||||
plank_world_left + plank_world_width * (0.27 + _wood_hash(seed + 1) * 0.46),
|
||||
board_world_top + board_world_height * (0.34 + _wood_hash(seed + 2) * 0.34)
|
||||
)
|
||||
var radius_x: float = (12.0 + _wood_hash(seed + 3) * 12.0) * detail_scale
|
||||
var radius_y: float = (3.0 + _wood_hash(seed + 4) * 2.8) * detail_scale
|
||||
var center := Vector2((center_world.x - origin.x) / motif_scale.x, (center_world.y - origin.y) / motif_scale.y)
|
||||
for ring: int in 3:
|
||||
var ring_scale: float = 1.0 - float(ring) * 0.27
|
||||
var points := PackedVector2Array()
|
||||
for point_index: int in 33:
|
||||
var angle: float = TAU * float(point_index) / 32.0
|
||||
var wobble: float = 1.0 + 0.07 * sin(angle * 3.0 + float(seed % 17))
|
||||
var world_offset := Vector2(
|
||||
cos(angle) * radius_x * ring_scale * wobble,
|
||||
sin(angle) * radius_y * ring_scale * (1.0 + 0.08 * cos(angle * 2.0))
|
||||
)
|
||||
points.append(center + Vector2(world_offset.x / motif_scale.x, world_offset.y / motif_scale.y))
|
||||
var ring_source: Color = Color("#784421") if ring != 1 else Color("#E39A59")
|
||||
var ring_color: Color = _color_with_alpha(_material_color(ring_source, tint, natural), 0.56 if ring != 1 else 0.52)
|
||||
var ring_width: float = (1.25 if ring != 1 else 0.85) * detail_scale / motif_scale.y
|
||||
_draw_clipped_polyline(canvas, clip_rect, points, ring_color, ring_width)
|
||||
|
||||
|
||||
static func _draw_clipped_polyline(canvas: CanvasItem, rect: Rect2, points: PackedVector2Array, color: Color, width: float) -> void:
|
||||
if points.size() < 2:
|
||||
return
|
||||
var inset: float = maxf(0.0, width) * 0.5
|
||||
var safe_rect := Rect2(rect.position + Vector2.ONE * inset, rect.size - Vector2.ONE * inset * 2.0)
|
||||
var fully_inside: bool = safe_rect.size.x > 0.0 and safe_rect.size.y > 0.0
|
||||
if fully_inside:
|
||||
for point: Vector2 in points:
|
||||
if not safe_rect.has_point(point):
|
||||
fully_inside = false
|
||||
break
|
||||
if fully_inside:
|
||||
canvas.draw_polyline(points, color, width, true)
|
||||
return
|
||||
for index: int in range(points.size() - 1):
|
||||
_draw_clipped_line(canvas, rect, points[index], points[index + 1], color, width)
|
||||
|
||||
|
||||
static func _wood_hash(seed: int) -> float:
|
||||
var value: float = sin(float(seed) * 12.9898 + 78.233) * 43758.5453
|
||||
return value - floorf(value)
|
||||
|
||||
|
||||
static func _color_with_alpha(color: Color, alpha: float) -> Color:
|
||||
return Color(color.r, color.g, color.b, alpha)
|
||||
|
||||
|
||||
static func _material_color(source: Color, selected: Color, natural: Color) -> Color:
|
||||
var natural_value: float = maxf(0.08, natural.v)
|
||||
var source_value_ratio: float = source.v / natural_value
|
||||
var value: float = clampf(selected.v * source_value_ratio, 0.035, 1.0)
|
||||
var natural_saturation: float = maxf(0.08, natural.s)
|
||||
var source_saturation_ratio: float = source.s / natural_saturation
|
||||
var saturation: float = clampf(selected.s * source_saturation_ratio, 0.0, 1.0)
|
||||
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:
|
||||
const CELL_WIDTH: float = 44.0
|
||||
const CELL_HEIGHT: float = 36.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) - 1
|
||||
var last_column: int = floori(world_right / CELL_WIDTH) + 1
|
||||
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):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = row * 73856093 + column * 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)
|
||||
if not rect.grow(8.0).has_point(center):
|
||||
continue
|
||||
var tuft_color: Color = Color("#3A7837").lightened(_wood_hash(seed + 2) * 0.24)
|
||||
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:
|
||||
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
|
||||
var blade_height: float = (4.5 + _wood_hash(blade_seed + 2) * 4.0) / motif_scale.y
|
||||
var start := center + Vector2(x_offset, y_offset)
|
||||
var tip := start + Vector2((_wood_hash(blade_seed + 3) - 0.5) * 4.0 / motif_scale.x, -blade_height)
|
||||
_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:
|
||||
const SLAB_WIDTH: float = 280.0
|
||||
const SLAB_HEIGHT: float = 190.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 / SLAB_WIDTH)
|
||||
var last_column: int = floori(world_right / SLAB_WIDTH)
|
||||
var first_row: int = floori(world_top / SLAB_HEIGHT)
|
||||
var last_row: int = floori(world_bottom / SLAB_HEIGHT)
|
||||
var column_gap: float = 1.4 / motif_scale.x
|
||||
var row_gap: float = 1.4 / motif_scale.y
|
||||
var remaining: int = MAX_BACKGROUND_CELLS
|
||||
for column: int in range(first_column, mini(last_column + 1, first_column + MAX_BACKGROUND_CELLS)):
|
||||
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)
|
||||
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 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):
|
||||
var speckle: Color = Color("#E2E1D8") if _wood_hash(seed + 2) > 0.48 else Color("#677173")
|
||||
canvas.draw_circle(point, (0.65 + _wood_hash(seed + 3) * 0.8) / motif_scale.y, _color_with_alpha(speckle, 0.34))
|
||||
for row: int in range(first_row, mini(last_row + 1, first_row + MAX_BACKGROUND_CELLS)):
|
||||
var y: float = float(row) * SLAB_HEIGHT
|
||||
var local_y: float = (y - origin.y) / motif_scale.y
|
||||
_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:
|
||||
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 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):
|
||||
if remaining <= 0:
|
||||
return
|
||||
remaining -= 1
|
||||
var seed: int = row * 92821 + column * 68917 + 71
|
||||
var cell_left: float = float(column) * CELL_WIDTH
|
||||
var cell_top: float = float(row) * CELL_HEIGHT
|
||||
for fiber: int in 2:
|
||||
var fiber_seed: int = seed + fiber * 53
|
||||
var start_world := Vector2(cell_left + 3.0 + _wood_hash(fiber_seed) * (CELL_WIDTH - 6.0), cell_top + 3.0 + _wood_hash(fiber_seed + 1) * (CELL_HEIGHT - 6.0))
|
||||
var end_world := start_world + Vector2(2.0 + _wood_hash(fiber_seed + 2) * 4.0, (_wood_hash(fiber_seed + 3) - 0.5) * 2.2)
|
||||
var start := Vector2((start_world.x - origin.x) / motif_scale.x, (start_world.y - origin.y) / motif_scale.y)
|
||||
var finish := Vector2((end_world.x - origin.x) / motif_scale.x, (end_world.y - origin.y) / motif_scale.y)
|
||||
var source: Color = Color("#E7B8D2") if _wood_hash(fiber_seed + 4) > 0.52 else Color("#3E3044")
|
||||
var fiber_color: Color = _material_color(source, tint, natural)
|
||||
_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:
|
||||
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
|
||||
var brick_h: float = brick_world_h / motif_scale.y
|
||||
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_row: int = floori(world_top / brick_world_h)
|
||||
var last_row: int = floori((world_bottom - 0.001) / brick_world_h)
|
||||
var mortar_gap := Vector2(3.5 * detail_scale / motif_scale.x, 3.5 * detail_scale / motif_scale.y)
|
||||
var source_palette: Array[Color] = [
|
||||
Color("#B65E3E"), Color("#C16B47"), Color("#A95337"),
|
||||
Color("#BD704B"), Color("#AD583A"), Color("#C47A53"),
|
||||
]
|
||||
var palette: Array[Color] = []
|
||||
for source_color: Color in source_palette:
|
||||
palette.append(_material_color(source_color, tint, default_tint(&"finish_wall_brick")))
|
||||
var row_count: int = mini(last_row - first_row + 1, MAX_WALL_BRICK_CELLS)
|
||||
for row_offset: int in row_count:
|
||||
var row: int = first_row + row_offset
|
||||
var row_world_top: float = float(row) * brick_world_h
|
||||
var row_local_top: float = (row_world_top - origin.y) / motif_scale.y
|
||||
var stagger_world: float = brick_world_w * 0.5 if posmod(row, 2) == 1 else 0.0
|
||||
var first_column: int = floori((world_left - stagger_world) / brick_world_w) - 1
|
||||
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 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
|
||||
var height_variation: float = 0.92 + _wood_hash(seed + 2) * 0.12
|
||||
var face_size := Vector2(brick_w * width_variation, brick_h * height_variation) - mortar_gap * 2.0
|
||||
var center_jitter := Vector2(
|
||||
(_wood_hash(seed + 3) - 0.5) * 2.0 * mortar_gap.x * 0.3,
|
||||
(_wood_hash(seed + 4) - 0.5) * 2.0 * mortar_gap.y * 0.3
|
||||
)
|
||||
var face_rect := Rect2(
|
||||
Vector2(brick_local_left, row_local_top) + mortar_gap + center_jitter,
|
||||
face_size
|
||||
)
|
||||
if face_rect.end.x < rect.position.x or face_rect.position.x > rect.end.x:
|
||||
continue
|
||||
if face_rect.end.y < rect.position.y or face_rect.position.y > rect.end.y:
|
||||
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)
|
||||
var shadow_points: PackedVector2Array = _clip_polygon_to_rect(_offset_polygon(outline, shadow_offset), rect)
|
||||
if shadow_points.size() >= 3:
|
||||
canvas.draw_colored_polygon(shadow_points, _color_with_alpha(_material_color(Color("#72503E"), tint, default_tint(&"finish_wall_brick")), 0.40))
|
||||
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])
|
||||
_draw_clipped_polyline(
|
||||
canvas, rect, closed_outline, _color_with_alpha(_material_color(Color("#784631"), tint, default_tint(&"finish_wall_brick")), 0.30), 1.2 * detail_scale / motif_scale.y
|
||||
)
|
||||
var upper_edge := PackedVector2Array()
|
||||
for point_index: int in mini(5, outline.size()):
|
||||
upper_edge.append(outline[point_index])
|
||||
_draw_clipped_polyline(
|
||||
canvas, rect, upper_edge, _color_with_alpha(_material_color(Color("#F0B27C"), tint, default_tint(&"finish_wall_brick")), 0.58), 1.15 * detail_scale / motif_scale.y
|
||||
)
|
||||
var facet := PackedVector2Array([
|
||||
Vector2(face_rect.position.x + face_rect.size.x * 0.06, face_rect.position.y + face_rect.size.y * 0.20),
|
||||
Vector2(face_rect.position.x + face_rect.size.x * 0.40, face_rect.position.y + face_rect.size.y * 0.12),
|
||||
Vector2(face_rect.position.x + face_rect.size.x * 0.68, face_rect.position.y + face_rect.size.y * 0.22),
|
||||
Vector2(face_rect.position.x + face_rect.size.x * 0.34, face_rect.position.y + face_rect.size.y * 0.40),
|
||||
])
|
||||
var facet_points: PackedVector2Array = _clip_polygon_to_rect(facet, rect)
|
||||
if facet_points.size() >= 3:
|
||||
canvas.draw_colored_polygon(facet_points, _color_with_alpha(_material_color(Color("#E49A68"), tint, default_tint(&"finish_wall_brick")), 0.20))
|
||||
_draw_wall_brick_texture(canvas, rect, face_rect, seed, detail_scale, motif_scale, tint)
|
||||
|
||||
|
||||
static func _rough_brick_outline(rect: Rect2, seed: int, roughness: Vector2) -> PackedVector2Array:
|
||||
var perimeter: Array[Vector2] = [
|
||||
Vector2(0.02, 0.05), Vector2(0.22, 0.00), Vector2(0.45, 0.04), Vector2(0.68, 0.00),
|
||||
Vector2(0.96, 0.03), Vector2(1.00, 0.22), Vector2(0.97, 0.48), Vector2(1.00, 0.73),
|
||||
Vector2(0.97, 0.95), Vector2(0.72, 1.00), Vector2(0.48, 0.96), Vector2(0.22, 1.00),
|
||||
Vector2(0.03, 0.96), Vector2(0.00, 0.73), Vector2(0.04, 0.48), Vector2(0.00, 0.22),
|
||||
]
|
||||
var points := PackedVector2Array()
|
||||
for point_index: int in perimeter.size():
|
||||
var normalized_point: Vector2 = perimeter[point_index]
|
||||
var jitter := Vector2(
|
||||
(_wood_hash(seed + point_index * 2 + 11) - 0.5) * 2.0 * roughness.x,
|
||||
(_wood_hash(seed + point_index * 2 + 12) - 0.5) * 2.0 * roughness.y
|
||||
)
|
||||
points.append(rect.position + normalized_point * rect.size + jitter)
|
||||
return points
|
||||
|
||||
|
||||
static func _draw_wall_brick_texture(
|
||||
canvas: CanvasItem,
|
||||
clip_rect: Rect2,
|
||||
face_rect: Rect2,
|
||||
seed: int,
|
||||
detail_scale: float,
|
||||
motif_scale: Vector2,
|
||||
tint: Color
|
||||
) -> void:
|
||||
var mark_count: int = 4 + int(_wood_hash(seed + 201) * 4.0)
|
||||
var natural: Color = default_tint(&"finish_wall_brick")
|
||||
var dark_ochre: Color = _material_color(Color("#74412D"), tint, natural)
|
||||
var warm_clay: Color = _material_color(Color("#E09A68"), tint, natural)
|
||||
for mark_index: int in mark_count:
|
||||
var mark_seed: int = seed + mark_index * 37 + 211
|
||||
var start_x: float = face_rect.position.x + face_rect.size.x * (0.09 + _wood_hash(mark_seed) * 0.72)
|
||||
var center_y: float = face_rect.position.y + face_rect.size.y * (0.20 + _wood_hash(mark_seed + 1) * 0.63)
|
||||
var length: float = face_rect.size.x * (0.08 + _wood_hash(mark_seed + 2) * 0.22)
|
||||
var wave: float = (0.7 + _wood_hash(mark_seed + 3) * 1.8) * detail_scale / motif_scale.y
|
||||
var points := PackedVector2Array([
|
||||
Vector2(start_x, center_y),
|
||||
Vector2(start_x + length * 0.48, center_y + wave),
|
||||
Vector2(start_x + length, center_y - wave * 0.35),
|
||||
])
|
||||
var light_mark: bool = _wood_hash(mark_seed + 4) > 0.72
|
||||
var mark_color: Color = warm_clay if light_mark else dark_ochre
|
||||
var alpha: float = 0.20 + _wood_hash(mark_seed + 5) * 0.25
|
||||
var width: float = (0.8 + _wood_hash(mark_seed + 6) * 1.6) * detail_scale / motif_scale.y
|
||||
_draw_clipped_polyline(canvas, clip_rect, points, _color_with_alpha(mark_color, alpha), width)
|
||||
var pore_count: int = 3 + int(_wood_hash(seed + 401) * 4.0)
|
||||
for pore_index: int in pore_count:
|
||||
var pore_seed: int = seed + pore_index * 53 + 419
|
||||
var pore := Vector2(
|
||||
face_rect.position.x + face_rect.size.x * (0.10 + _wood_hash(pore_seed) * 0.80),
|
||||
face_rect.position.y + face_rect.size.y * (0.15 + _wood_hash(pore_seed + 1) * 0.70)
|
||||
)
|
||||
var radius: float = (0.55 + _wood_hash(pore_seed + 2) * 0.8) * detail_scale / motif_scale.y
|
||||
var pore_color: Color = warm_clay if _wood_hash(pore_seed + 3) > 0.62 else dark_ochre
|
||||
canvas.draw_circle(pore, radius, _color_with_alpha(pore_color, 0.38))
|
||||
|
||||
|
||||
static func _offset_polygon(points: PackedVector2Array, offset: Vector2) -> PackedVector2Array:
|
||||
var result := PackedVector2Array()
|
||||
for point: Vector2 in points:
|
||||
result.append(point + offset)
|
||||
return result
|
||||
|
||||
|
||||
static func _clip_polygon_to_rect(points: PackedVector2Array, rect: Rect2) -> PackedVector2Array:
|
||||
var clipped := points
|
||||
for edge: int in 4:
|
||||
clipped = _clip_polygon_to_edge(clipped, rect, edge)
|
||||
if clipped.is_empty():
|
||||
return clipped
|
||||
return clipped
|
||||
|
||||
|
||||
static func _clip_polygon_to_edge(points: PackedVector2Array, rect: Rect2, edge: int) -> PackedVector2Array:
|
||||
var result := PackedVector2Array()
|
||||
if points.is_empty():
|
||||
return result
|
||||
var previous: Vector2 = points[points.size() - 1]
|
||||
var previous_inside: bool = _point_inside_rect_edge(previous, rect, edge)
|
||||
for current: Vector2 in points:
|
||||
var current_inside: bool = _point_inside_rect_edge(current, rect, edge)
|
||||
if current_inside:
|
||||
if not previous_inside:
|
||||
result.append(_rect_edge_intersection(previous, current, rect, edge))
|
||||
result.append(current)
|
||||
elif previous_inside:
|
||||
result.append(_rect_edge_intersection(previous, current, rect, edge))
|
||||
previous = current
|
||||
previous_inside = current_inside
|
||||
return result
|
||||
|
||||
|
||||
static func _point_inside_rect_edge(point: Vector2, rect: Rect2, edge: int) -> bool:
|
||||
match edge:
|
||||
0:
|
||||
return point.x >= rect.position.x
|
||||
1:
|
||||
return point.x <= rect.end.x
|
||||
2:
|
||||
return point.y >= rect.position.y
|
||||
_:
|
||||
return point.y <= rect.end.y
|
||||
|
||||
|
||||
static func _rect_edge_intersection(from: Vector2, to: Vector2, rect: Rect2, edge: int) -> Vector2:
|
||||
if edge <= 1:
|
||||
var boundary_x: float = rect.position.x if edge == 0 else rect.end.x
|
||||
var ratio_x: float = (boundary_x - from.x) / (to.x - from.x)
|
||||
return Vector2(boundary_x, lerpf(from.y, to.y, ratio_x))
|
||||
var boundary_y: float = rect.position.y if edge == 2 else rect.end.y
|
||||
var ratio_y: float = (boundary_y - from.y) / (to.y - from.y)
|
||||
return Vector2(lerpf(from.x, to.x, ratio_y), boundary_y)
|
||||
|
||||
|
||||
static func _draw_bricks(canvas: CanvasItem, rect: Rect2, origin: Vector2, base: Color, motif_scale: Vector2, detail_scale: float = 1.0) -> void:
|
||||
var brick_world_w: float = BRICK_WIDTH * detail_scale
|
||||
var brick_world_h: float = BRICK_HEIGHT * detail_scale
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
class_name StageWallPanel
|
||||
extends Node2D
|
||||
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")
|
||||
var finish_id: StringName = &"finish_wall_paint"
|
||||
var tint: Color = Color("#5A9D70")
|
||||
var panel_size: Vector2 = Vector2(320.0, 400.0)
|
||||
## 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.
|
||||
var motif_origin_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
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))
|
||||
finish_id = finish if SURFACE_RENDERER.is_wall_finish(finish) else &"finish_wall_paint"
|
||||
tint = color
|
||||
super.configure(new_size, finish if SURFACE_RENDERER.is_wall_finish(finish) else &"finish_wall_paint", color)
|
||||
z_index = DRAW_ORDER.WALL
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_local_bounds() -> Rect2:
|
||||
return Rect2(-panel_size * 0.5, panel_size)
|
||||
|
||||
|
||||
func get_effective_motif_origin_world() -> Vector2:
|
||||
@@ -33,4 +23,4 @@ func get_surface_draw_origin_world() -> Vector2:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, tint, get_surface_draw_origin_world())
|
||||
SURFACE_RENDERER.draw_wall(self, get_local_bounds(), finish_id, get_render_tint(), get_surface_draw_origin_world())
|
||||
|
||||
@@ -3,6 +3,7 @@ extends RefCounted
|
||||
## Semantic draw bands shared by scenery, terrain, Props, and characters.
|
||||
|
||||
const WALL: int = -40
|
||||
const BACKGROUND_GROUND: int = -50
|
||||
const FAR: int = -30
|
||||
const NEAR: int = -20
|
||||
const TERRAIN: int = -10
|
||||
|
||||
@@ -12,6 +12,7 @@ extends Node2D
|
||||
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||
const StageWallPanelScript := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const StageBackgroundGroundScript := preload("res://scripts/stage/scenery/stage_background_ground.gd")
|
||||
const StageSceneryArtworkScript := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -300,12 +301,12 @@ func _corners(rect: Rect2) -> Array[Vector2]:
|
||||
|
||||
|
||||
func _is_scenery_target(node: Node2D) -> bool:
|
||||
return node != null and (node.get_script() == StageWallPanelScript or node.get_script() == StageSceneryArtworkScript)
|
||||
return node != null and (node.get_script() == StageWallPanelScript or node.get_script() == StageBackgroundGroundScript or node.get_script() == StageSceneryArtworkScript)
|
||||
|
||||
|
||||
func _target_bounds(node: Node2D) -> Rect2:
|
||||
if node != null and node.get_script() == StageWallPanelScript:
|
||||
return node.global_transform * (node as StageWallPanel).get_local_bounds()
|
||||
if node is StageRectSurface:
|
||||
return node.global_transform * (node as StageRectSurface).get_local_bounds()
|
||||
if node != null and node.get_script() == StageSceneryArtworkScript:
|
||||
return (node as StageSceneryArtwork).get_visual_world_bounds()
|
||||
return STAGE_SELECTION.get_world_aabb(node)
|
||||
|
||||
@@ -3,7 +3,8 @@ extends RefCounted
|
||||
## JSON boundary for Sandbox Stage documents. This class deliberately has no
|
||||
## scene-tree dependencies, which makes rejected documents non-destructive.
|
||||
|
||||
const FILE_VERSION: String = "1.5"
|
||||
const FILE_VERSION: String = "1.6"
|
||||
const SURFACE_RENDERER := preload("res://scripts/stage/scenery/stage_surface_renderer.gd")
|
||||
const CONTENT_PATHS := preload("res://scripts/shared/content_paths.gd")
|
||||
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
||||
const TRIGGER_REGISTRY := preload("res://scripts/stage/trigger_registry.gd")
|
||||
@@ -11,6 +12,7 @@ const SPEECH_PAYLOAD := preload("res://scripts/speech/speech_payload.gd")
|
||||
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
||||
const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd")
|
||||
const WALL_FINISHES: Array[String] = ["finish_wall_paint", "finish_wall_brick", "finish_wall_wood"]
|
||||
const BACKGROUND_FINISHES: Array[String] = ["finish_background_grass", "finish_background_concrete", "finish_background_carpet"]
|
||||
const GROUND_FINISHES: Array[String] = ["finish_ground_paint", "finish_ground_brick", "finish_ground_wood", "finish_ground_grass", "finish_ground_dirt", "finish_ground_sidewalk", "finish_ground_asphalt", "finish_ground_legacy"]
|
||||
const SKY_IDS: Array[String] = ["sky_clear_day", "sky_cloudy_day", "sky_sunset", "sky_starry_night", "sky_neutral"]
|
||||
const SCENERY_DEPTHS: Array[String] = ["far", "near", "floor"]
|
||||
@@ -183,10 +185,11 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
prop_entry["wobble_on_impact"] = false
|
||||
_assign_legacy_depths(current.get("objects", []))
|
||||
return current
|
||||
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4"]:
|
||||
if version not in ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5"]:
|
||||
return {}
|
||||
var migrated: Dictionary = data.duplicate(true)
|
||||
migrated["sky_id"] = "sky_neutral"
|
||||
if version != "1.5":
|
||||
migrated["sky_id"] = "sky_neutral"
|
||||
for raw: Variant in migrated.get("objects", []):
|
||||
if not raw is Dictionary:
|
||||
continue
|
||||
@@ -200,13 +203,32 @@ static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
object["next_use_spot_number"] = object.get("next_use_spot_number", 1)
|
||||
if not object.has("wobble_on_impact"):
|
||||
object["wobble_on_impact"] = false
|
||||
if String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
|
||||
if version != "1.5" and String(object.get("type", "")) == "terrain" and String(object.get("asset_id", "")) == "ground":
|
||||
object["finish_id"] = "finish_ground_legacy"
|
||||
if String(object.get("type", "")) == "wall_panel":
|
||||
_migrate_legacy_wall_tint(object)
|
||||
migrated["version"] = FILE_VERSION
|
||||
_assign_legacy_depths(migrated.get("objects", []))
|
||||
return migrated
|
||||
|
||||
|
||||
static func _migrate_legacy_wall_tint(entry: Dictionary) -> void:
|
||||
var finish: StringName = StringName(String(entry.get("finish_id", "finish_wall_paint")))
|
||||
if finish not in [&"finish_wall_brick", &"finish_wall_wood"]:
|
||||
if not entry.has("color"):
|
||||
entry["color"] = "#5A9D70"
|
||||
return
|
||||
if not entry.has("color") or _is_legacy_green_tint(entry.get("color")):
|
||||
entry["color"] = "#" + SURFACE_RENDERER.default_tint(finish).to_html(true)
|
||||
|
||||
|
||||
static func _is_legacy_green_tint(value: Variant) -> bool:
|
||||
if not value is String:
|
||||
return false
|
||||
var raw: String = String(value).strip_edges().trim_prefix("#").to_upper()
|
||||
return raw == "5A9D70" or raw == "5A9D70FF"
|
||||
|
||||
|
||||
static func _assign_legacy_depths(raw_objects: Variant) -> void:
|
||||
if not raw_objects is Array:
|
||||
return
|
||||
@@ -277,6 +299,14 @@ static func _validate_root(data: Dictionary) -> Dictionary:
|
||||
return {"ok": false, "error": "Wall panel color is invalid."}
|
||||
if entry.has("motif_origin_offset") and not is_vector_data(entry.get("motif_origin_offset")):
|
||||
return {"ok": false, "error": "Wall panel motif origin is invalid."}
|
||||
elif String(entry.get("type", "")) == "background_ground":
|
||||
if not is_vector_data(entry.get("size")):
|
||||
return {"ok": false, "error": "Background ground size is invalid."}
|
||||
var background_size: Vector2 = data_to_vector(entry.get("size"), Vector2.ZERO)
|
||||
if background_size.x <= 0.0 or background_size.y <= 0.0 or not BACKGROUND_FINISHES.has(String(entry.get("finish_id", ""))):
|
||||
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."}
|
||||
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():
|
||||
|
||||
@@ -20,6 +20,7 @@ signal selection_changed(nodes: Array[Node2D])
|
||||
|
||||
const RAGDOLL_CONTAINER_NAME := "RagdollBodyContainer"
|
||||
const STAGE_WALL_PANEL := preload("res://scripts/stage/scenery/stage_wall_panel.gd")
|
||||
const STAGE_BACKGROUND_GROUND := preload("res://scripts/stage/scenery/stage_background_ground.gd")
|
||||
const STAGE_SCENERY_ARTWORK := preload("res://scripts/stage/scenery/stage_scenery_artwork.gd")
|
||||
|
||||
enum SelectionScope { ACTION, SCENERY }
|
||||
@@ -172,6 +173,8 @@ func box_select(rect: Rect2, additive: bool) -> void:
|
||||
static func get_world_aabb(node: Node2D) -> Rect2:
|
||||
if node == null or not is_instance_valid(node):
|
||||
return Rect2()
|
||||
if node is StageRectSurface:
|
||||
return node.global_transform * (node as StageRectSurface).get_local_bounds()
|
||||
# Duck-typed TriggerArea: expose its centered local rect through the transform.
|
||||
if node.has_method("get_area_rect"):
|
||||
return node.global_transform * (node.call("get_area_rect") as Rect2)
|
||||
@@ -230,7 +233,7 @@ func _is_selectable(node: Node) -> bool:
|
||||
return false
|
||||
if node.is_in_group("stickman_ragdoll_container") or bool(node.get_meta("stickman_ragdoll_container", false)):
|
||||
return false
|
||||
var is_scenery: bool = node.get_script() == STAGE_WALL_PANEL or node.get_script() == STAGE_SCENERY_ARTWORK
|
||||
var is_scenery: bool = node.get_script() == STAGE_WALL_PANEL or node.get_script() == STAGE_BACKGROUND_GROUND or node.get_script() == STAGE_SCENERY_ARTWORK
|
||||
if _scope == SelectionScope.ACTION and is_scenery:
|
||||
return false
|
||||
if _scope == SelectionScope.SCENERY and not is_scenery:
|
||||
|
||||
Reference in New Issue
Block a user