Add visual harmonization tests for editor UI components and ground finish chooser
- Implemented `test_editor_visual_harmonization.gd` to validate the Stickman editor and prop editor UI layouts, ensuring proper functionality and visual consistency. - Created `test_editor_visual_theme.gd` to verify the loading and application of visual themes in the editor, including semantic overrides and default values. - Introduced `test_phase19_ground_finish_chooser.gd` to test the ground finish chooser's functionality, including layout responsiveness, selection behavior, and interaction with the stage. - Added new scripts for ground finish preview and chooser UI components to enhance the editor's visual representation of ground finishes.
This commit is contained in:
@@ -121,6 +121,7 @@ var _pan_offset: Vector2 = Vector2.ZERO
|
||||
var _grid_size: int = 15
|
||||
var _snap_enabled: bool = false
|
||||
var _has_clipboard: bool = false
|
||||
var _create_point_armed: bool = false
|
||||
var _context_menu: PopupMenu
|
||||
var _edit_mode: EditMode = EditMode.TRANSFORM
|
||||
var _anchor_overlay: RefCounted
|
||||
@@ -214,6 +215,8 @@ func set_edit_mode(mode: int) -> void:
|
||||
if _stage_size_preview_active:
|
||||
cancel_stage_size_preview()
|
||||
_edit_mode = m
|
||||
if _edit_mode != EditMode.POINTS:
|
||||
_create_point_armed = false
|
||||
if _edit_mode != EditMode.TRANSFORM:
|
||||
set_whole_prop_resize_enabled(false)
|
||||
_drag_mode = DragMode.NONE
|
||||
@@ -225,6 +228,21 @@ func set_edit_mode(mode: int) -> void:
|
||||
func get_edit_mode() -> int:
|
||||
return _edit_mode
|
||||
|
||||
|
||||
func arm_create_point() -> void:
|
||||
set_edit_mode(EditMode.POINTS)
|
||||
_create_point_armed = true
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func cancel_create_point() -> void:
|
||||
_create_point_armed = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func is_create_point_armed() -> bool:
|
||||
return _create_point_armed
|
||||
|
||||
func set_anchor_data(data: Variant) -> void:
|
||||
if _anchor_overlay == null: _anchor_overlay = PropAnchorOverlayScript.new()
|
||||
_anchor_overlay.set_anchor_data(data)
|
||||
@@ -1254,7 +1272,10 @@ func _on_canvas_gui_input(event: InputEvent) -> void:
|
||||
return
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
if mb.pressed:
|
||||
_handle_left_press(world_pos)
|
||||
if _create_point_armed and _edit_mode == EditMode.POINTS:
|
||||
_create_point_from_toolbar(world_pos)
|
||||
else:
|
||||
_handle_left_press(world_pos)
|
||||
else:
|
||||
_handle_left_release()
|
||||
|
||||
@@ -1464,6 +1485,21 @@ func _handle_left_press(world_pos: Vector2) -> void:
|
||||
shape_selected.emit(-1)
|
||||
|
||||
|
||||
func _create_point_from_toolbar(world_pos: Vector2) -> void:
|
||||
var shape_idx: int = hit_test_segment(world_pos)
|
||||
if shape_idx < 0:
|
||||
return
|
||||
_selected_shape_idx = shape_idx
|
||||
shape_selected.emit(shape_idx)
|
||||
var new_vertex_index: int = create_point_on_edge(world_pos)
|
||||
if new_vertex_index < 0:
|
||||
return
|
||||
_create_point_armed = false
|
||||
_drag_mode = DragMode.NONE
|
||||
_emit_changed()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _handle_drag_motion(mm: InputEventMouseMotion, world_pos: Vector2, ctrl_pressed: bool) -> void:
|
||||
match _drag_mode:
|
||||
DragMode.VERTEX:
|
||||
|
||||
@@ -12,6 +12,7 @@ extends Control
|
||||
|
||||
const PropFileScript := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const PropThemeScript := preload("res://scripts/editors/prop/prop_theme.gd")
|
||||
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||
const PropCanvasScript := preload("res://scripts/editors/prop/prop_canvas.gd")
|
||||
const PropThumbnailScript := preload("res://scripts/shared/thumbnails/prop_thumbnail.gd")
|
||||
@@ -92,10 +93,10 @@ static func should_warn_off_origin(has_geometry: bool, bounds: Rect2) -> bool:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@onready var _menu_bar: MenuBar = %MenuBar
|
||||
@onready var _top_right: HBoxContainer = %TopRightControls
|
||||
@onready var _top_right: FlowContainer = %TopRightControls
|
||||
@onready var _prop_name_edit: LineEdit = %PropNameEdit
|
||||
@onready var _canvas: PropCanvas = %PropCanvas
|
||||
@onready var _mode_bar: HBoxContainer = %ModeBar
|
||||
@onready var _mode_bar: FlowContainer = %ModeBar
|
||||
@onready var _size_tools: FlowContainer = %SizeTools
|
||||
@onready var _attach_tools: FlowContainer = %AttachTools
|
||||
@onready var _canvas_workspace: HSplitContainer = %CanvasWorkspace
|
||||
@@ -114,6 +115,7 @@ static func should_warn_off_origin(has_geometry: bool, bounds: Rect2) -> bool:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var _theme: PropTheme
|
||||
var _editor_visual_theme: EditorVisualTheme
|
||||
var _material_preset: int = PropBlockScript.MaterialPreset.NONE
|
||||
var _current_path: String = ""
|
||||
var _dirty: bool = false
|
||||
@@ -139,6 +141,8 @@ var _radius_row: Control
|
||||
var _reshaped_note: Label
|
||||
var _vertex_label: Label
|
||||
var _delete_button: Button
|
||||
var _create_point_button: Button
|
||||
var _create_point_hint: Label
|
||||
var _shape_section: VBoxContainer
|
||||
var _primitive_option: OptionButton
|
||||
var _params_box: VBoxContainer
|
||||
@@ -212,6 +216,8 @@ var _recent_props: Array[String] = []
|
||||
func _ready() -> void:
|
||||
if not _stage_authoring:
|
||||
get_tree().auto_accept_quit = false
|
||||
_editor_visual_theme = EditorVisualThemeScript.new()
|
||||
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||
_theme = PropThemeScript.new()
|
||||
_theme.load_from(PropThemeScript.DEFAULT_PATH)
|
||||
_thumb_cache = ThumbnailCacheScript.new()
|
||||
@@ -260,7 +266,7 @@ func _ready() -> void:
|
||||
_cursor_coords.add_theme_font_size_override("font_size", _theme.font_status())
|
||||
_save_status.add_theme_font_size_override("font_size", _theme.font_status())
|
||||
|
||||
_mode_bar.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
_mode_bar.alignment = FlowContainer.ALIGNMENT_CENTER
|
||||
|
||||
_refresh_inspector_from_selection()
|
||||
_sync_mode_buttons(_canvas.get_edit_mode())
|
||||
@@ -311,7 +317,11 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
return
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner() if get_viewport() != null else null
|
||||
if not (focus_owner is LineEdit or focus_owner is TextEdit):
|
||||
if _canvas.is_whole_prop_resize_dragging():
|
||||
if _canvas.is_create_point_armed():
|
||||
_canvas.cancel_create_point()
|
||||
_update_create_point_feedback()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _canvas.is_whole_prop_resize_dragging():
|
||||
_canvas.cancel_whole_prop_resize()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _canvas.is_whole_prop_resize_enabled():
|
||||
@@ -337,8 +347,11 @@ func _update_save_status() -> void:
|
||||
var name: String = _prop_name_edit.text if _prop_name_edit != null else ""
|
||||
if name.is_empty():
|
||||
name = "Unnamed Prop"
|
||||
var suffix: String = " • Modified" if _dirty else ""
|
||||
_save_status.text = "%s — %s%s" % [name, PRESET_LABELS[_material_preset], suffix]
|
||||
var has_saved_path: bool = not _current_path.is_empty()
|
||||
var state: String = "Unsaved changes" if _dirty else ("Saved" if has_saved_path else "Not saved yet")
|
||||
_save_status.text = "%s — %s — %s" % [name, PRESET_LABELS[_material_preset], state]
|
||||
var status_color: Color = _editor_visual_theme.chrome_color(&"dirty", &"prop") if _dirty else (_editor_visual_theme.chrome_color(&"saved", &"prop") if has_saved_path else _editor_visual_theme.chrome_color(&"text_muted", &"prop"))
|
||||
_save_status.add_theme_color_override("font_color", status_color)
|
||||
_update_window_title()
|
||||
|
||||
|
||||
@@ -372,10 +385,7 @@ func _make_mode_button(mode: int) -> Button:
|
||||
button.button_group = _mode_group
|
||||
button.custom_minimum_size.y = float(_theme.mode_button_min_height())
|
||||
button.add_theme_font_size_override("font_size", _theme.font_mode())
|
||||
# No keyboard focus: the focus stylebox otherwise draws over the pressed
|
||||
# stylebox, hiding the selected (accent-bordered) state until the cursor
|
||||
# moves away (Round 4 fix).
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
button.focus_mode = Control.FOCUS_ALL
|
||||
_mode_bar.add_child(button)
|
||||
return button
|
||||
|
||||
@@ -399,6 +409,8 @@ func _on_canvas_mode_changed(mode: int) -> void:
|
||||
return
|
||||
if mode != PropCanvasScript.EditMode.TRANSFORM:
|
||||
_set_whole_prop_resize_enabled(false)
|
||||
if mode != PropCanvasScript.EditMode.POINTS and _canvas.is_create_point_armed():
|
||||
_canvas.cancel_create_point()
|
||||
var entering_attach: bool = mode == PropCanvasScript.EditMode.ATTACH and _last_canvas_mode != PropCanvasScript.EditMode.ATTACH
|
||||
if entering_attach and _show_preview_check != null:
|
||||
_show_preview_check.set_pressed_no_signal(true)
|
||||
@@ -430,6 +442,11 @@ func _sync_mode_controls(mode: int) -> void:
|
||||
_add_section.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||
if _delete_button != null:
|
||||
_delete_button.visible = mode == PropCanvasScript.EditMode.TRANSFORM
|
||||
if _create_point_button != null:
|
||||
_create_point_button.visible = mode == PropCanvasScript.EditMode.POINTS and _canvas.get_selected_index() >= 0
|
||||
if _create_point_hint != null:
|
||||
_create_point_hint.visible = mode == PropCanvasScript.EditMode.POINTS and _canvas.get_selected_index() >= 0
|
||||
_update_create_point_feedback()
|
||||
if _shape_section != null:
|
||||
_shape_section.visible = mode != PropCanvasScript.EditMode.ATTACH and not _canvas.is_whole_prop_resize_enabled() and _canvas.get_selected_index() >= 0
|
||||
if _material_section != null:
|
||||
@@ -461,73 +478,15 @@ func _build_attach_tools() -> void:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _build_control_theme() -> void:
|
||||
_ui_theme = Theme.new()
|
||||
var bg: Color = _theme.canvas_background_color()
|
||||
var panel := StyleBoxFlat.new()
|
||||
panel.bg_color = Color(0.12, 0.12, 0.12, 1.0)
|
||||
panel.set_corner_radius_all(6)
|
||||
panel.content_margin_left = 8
|
||||
panel.content_margin_right = 8
|
||||
panel.content_margin_top = 6
|
||||
panel.content_margin_bottom = 6
|
||||
panel.border_color = Color(1.0, 1.0, 1.0, 0.12)
|
||||
panel.set_border_width_all(1)
|
||||
var panel_dark := StyleBoxFlat.new()
|
||||
panel_dark.bg_color = bg.darkened(0.25)
|
||||
panel_dark.set_corner_radius_all(6)
|
||||
panel_dark.content_margin_left = 8
|
||||
panel_dark.content_margin_right = 8
|
||||
panel_dark.content_margin_top = 6
|
||||
panel_dark.content_margin_bottom = 6
|
||||
panel_dark.border_color = Color(1.0, 1.0, 1.0, 0.12)
|
||||
panel_dark.set_border_width_all(1)
|
||||
var panel_pressed := StyleBoxFlat.new()
|
||||
panel_pressed.bg_color = bg.darkened(0.25)
|
||||
panel_pressed.set_corner_radius_all(6)
|
||||
panel_pressed.content_margin_left = 8
|
||||
panel_pressed.content_margin_right = 8
|
||||
panel_pressed.content_margin_top = 6
|
||||
panel_pressed.content_margin_bottom = 6
|
||||
panel_pressed.border_color = _theme.accent_color()
|
||||
panel_pressed.set_border_width_all(2)
|
||||
_ui_theme.set_stylebox("normal", "Button", panel)
|
||||
_ui_theme.set_stylebox("hover", "Button", panel_dark)
|
||||
_ui_theme.set_stylebox("pressed", "Button", panel_pressed)
|
||||
_ui_theme.set_stylebox("focus", "Button", panel)
|
||||
var inspector_surface := StyleBoxFlat.new()
|
||||
inspector_surface.bg_color = _theme.inspector_background_color()
|
||||
inspector_surface.border_color = _theme.border_color()
|
||||
inspector_surface.set_border_width_all(1)
|
||||
inspector_surface.content_margin_left = 8
|
||||
inspector_surface.content_margin_right = 8
|
||||
_ui_theme = _editor_visual_theme.control_theme(&"prop")
|
||||
theme = _ui_theme
|
||||
var inspector_surface: StyleBoxFlat = _ui_theme.get_stylebox("panel", "PanelContainer").duplicate() as StyleBoxFlat
|
||||
_inspector_column.add_theme_stylebox_override("panel", inspector_surface)
|
||||
var splitter := StyleBoxFlat.new()
|
||||
splitter.bg_color = _theme.border_color().lightened(0.35)
|
||||
splitter.bg_color = _editor_visual_theme.chrome_color(&"border", &"prop").lightened(0.35)
|
||||
splitter.content_margin_left = 3
|
||||
splitter.content_margin_right = 3
|
||||
_canvas_workspace.add_theme_stylebox_override("grabber", splitter)
|
||||
_ui_theme.set_stylebox("normal", "OptionButton", panel)
|
||||
_ui_theme.set_stylebox("hover", "OptionButton", panel_dark)
|
||||
_ui_theme.set_stylebox("pressed", "OptionButton", panel_dark)
|
||||
_ui_theme.set_stylebox("normal", "SpinBox", panel_dark)
|
||||
_ui_theme.set_stylebox("normal", "LineEdit", panel_dark)
|
||||
_ui_theme.set_stylebox("normal", "ColorPickerButton", panel)
|
||||
_ui_theme.set_stylebox("hover", "ColorPickerButton", panel_dark)
|
||||
_ui_theme.set_stylebox("normal", "PopupMenu", panel_dark)
|
||||
_ui_theme.set_stylebox("panel", "MenuBar", panel_dark)
|
||||
_ui_theme.set_stylebox("normal", "CheckBox", panel)
|
||||
_ui_theme.set_stylebox("panel", "PanelContainer", panel_dark)
|
||||
_ui_theme.set_color("font_color", "Label", _theme.text_color())
|
||||
_ui_theme.set_color("font_color", "Button", _theme.text_color())
|
||||
_ui_theme.set_color("font_hover_color", "Button", Color.WHITE)
|
||||
_ui_theme.set_color("font_pressed_color", "Button", _theme.accent_color())
|
||||
_ui_theme.set_font_size("font_size", "Label", _theme.font_body())
|
||||
_ui_theme.set_font_size("font_size", "Button", _theme.font_body())
|
||||
_ui_theme.set_font_size("font_size", "OptionButton", _theme.font_body())
|
||||
_ui_theme.set_font_size("font_size", "SpinBox", _theme.font_body())
|
||||
_ui_theme.set_font_size("font_size", "LineEdit", _theme.font_body())
|
||||
_ui_theme.set_font_size("font_size", "PopupMenu", _theme.font_menu())
|
||||
_ui_theme.set_font_size("font_size", "MenuBar", _theme.font_menu_bar())
|
||||
_menu_bar.theme = _ui_theme
|
||||
_top_right.theme = _ui_theme
|
||||
_prop_name_edit.theme = _ui_theme
|
||||
@@ -654,11 +613,18 @@ func _setup_dialogs() -> void:
|
||||
_load_dialog.current_dir = content_dir
|
||||
_save_dialog.file_selected.connect(_on_save_file_selected)
|
||||
_load_dialog.file_selected.connect(_on_load_file_selected)
|
||||
_editor_visual_theme.style_button_role(_save_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_save_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_error_dialog.get_ok_button(), &"secondary", &"prop")
|
||||
_build_exit_dialog()
|
||||
_rename_anchor_dialog = ConfirmationDialog.new()
|
||||
_rename_anchor_dialog.title = "Rename Anchor"
|
||||
_rename_anchor_dialog.ok_button_text = "Rename"
|
||||
_rename_anchor_dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_rename_anchor_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_rename_anchor_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
_rename_anchor_edit = LineEdit.new()
|
||||
_rename_anchor_edit.placeholder_text = "Anchor name"
|
||||
_rename_anchor_dialog.add_child(_rename_anchor_edit)
|
||||
@@ -708,8 +674,8 @@ func _build_top_right_controls() -> void:
|
||||
_top_right.add_child(_snap_check)
|
||||
func _build_size_tools() -> void:
|
||||
_whole_prop_resize_button = Button.new()
|
||||
_whole_prop_resize_button.text = "✥ Transform Whole Prop"
|
||||
_whole_prop_resize_button.tooltip_text = "Move, rotate, or resize the whole prop including grip markers."
|
||||
_whole_prop_resize_button.text = "Move, Turn, or Resize Prop"
|
||||
_whole_prop_resize_button.tooltip_text = "Move, turn, or resize the whole prop, including grip markers."
|
||||
_whole_prop_resize_button.toggle_mode = true
|
||||
_whole_prop_resize_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_whole_prop_resize_button.toggled.connect(_set_whole_prop_resize_enabled)
|
||||
@@ -923,9 +889,12 @@ func _build_exit_dialog() -> void:
|
||||
_exit_dialog.title = "⚠️ Unsaved Changes"
|
||||
_exit_dialog.dialog_text = "You have unsaved changes. Save them before quitting?"
|
||||
_exit_dialog.ok_button_text = "💾 Save"
|
||||
_exit_dialog.add_button("🗑️ Discard", false, "discard")
|
||||
var discard_button: Button = _exit_dialog.add_button("🗑️ Discard", false, "discard")
|
||||
_exit_dialog.get_cancel_button().text = "❌ Cancel"
|
||||
_exit_dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_exit_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"prop")
|
||||
_editor_visual_theme.style_button_role(_exit_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
add_child(_exit_dialog)
|
||||
_exit_dialog.confirmed.connect(_on_exit_save)
|
||||
_exit_dialog.custom_action.connect(_on_exit_custom_action)
|
||||
@@ -934,9 +903,12 @@ func _build_exit_dialog() -> void:
|
||||
_off_origin_save_dialog.title = "🎯 Center Prop?"
|
||||
_off_origin_save_dialog.dialog_text = "This prop is far from (0,0). Centering it keeps its shapes and anchors together.\n\nWhat would you like to do?"
|
||||
_off_origin_save_dialog.ok_button_text = "🎯 Center & Save"
|
||||
_off_origin_save_dialog.add_button("💾 Save Anyway", false, "save_anyway")
|
||||
var save_anyway_button: Button = _off_origin_save_dialog.add_button("💾 Save Anyway", false, "save_anyway")
|
||||
_off_origin_save_dialog.get_cancel_button().text = "❌ Cancel"
|
||||
_off_origin_save_dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_off_origin_save_dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(save_anyway_button, &"secondary", &"prop")
|
||||
_editor_visual_theme.style_button_role(_off_origin_save_dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
add_child(_off_origin_save_dialog)
|
||||
_off_origin_save_dialog.confirmed.connect(func() -> void: await _execute_pending_save(true))
|
||||
_off_origin_save_dialog.custom_action.connect(func(action: StringName) -> void:
|
||||
@@ -1178,9 +1150,22 @@ func _build_inspector() -> void:
|
||||
_vertex_label = Label.new()
|
||||
_shape_section.add_child(_vertex_label)
|
||||
|
||||
_create_point_button = Button.new()
|
||||
_create_point_button.text = "📍 Create Point"
|
||||
_create_point_button.tooltip_text = "Add a point to the selected shape's edge."
|
||||
_create_point_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_editor_visual_theme.style_button_role(_create_point_button, &"primary", &"prop")
|
||||
_create_point_button.pressed.connect(_on_create_point_pressed)
|
||||
_shape_section.add_child(_create_point_button)
|
||||
_create_point_hint = Label.new()
|
||||
_create_point_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_create_point_hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||
_shape_section.add_child(_create_point_hint)
|
||||
|
||||
_delete_button = Button.new()
|
||||
_delete_button.text = "Erase Shape"
|
||||
_delete_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_editor_visual_theme.style_button_role(_delete_button, &"destructive", &"prop")
|
||||
_delete_button.pressed.connect(_on_delete_shape_pressed)
|
||||
_shape_section.add_child(_delete_button)
|
||||
|
||||
@@ -1208,6 +1193,7 @@ func _build_inspector() -> void:
|
||||
_add_button = Button.new()
|
||||
_add_button.text = "Add a Shape"
|
||||
_add_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_editor_visual_theme.style_button_role(_add_button, &"primary", &"prop")
|
||||
_add_button.pressed.connect(_on_add_pressed)
|
||||
_add_section.add_child(_add_button)
|
||||
|
||||
@@ -1219,7 +1205,7 @@ func _build_inspector() -> void:
|
||||
|
||||
|
||||
func _build_stage_authoring_controls() -> void:
|
||||
var top_bar: HBoxContainer = get_node_or_null(NodePath("TopBar")) as HBoxContainer
|
||||
var top_bar: HBoxContainer = get_node_or_null(NodePath("ChromeRoot/TopBar")) as HBoxContainer
|
||||
if top_bar == null:
|
||||
return
|
||||
var name_label: Label = top_bar.get_node_or_null(NodePath("NameLabel")) as Label
|
||||
@@ -1232,6 +1218,7 @@ func _build_stage_authoring_controls() -> void:
|
||||
_stage_place_button.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
_stage_place_button.size_flags_horizontal = Control.SIZE_SHRINK_END
|
||||
_stage_place_button.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(_stage_place_button, &"primary", &"prop")
|
||||
_stage_place_button.pressed.connect(_request_stage_return)
|
||||
top_bar.add_child(_stage_place_button)
|
||||
|
||||
@@ -1312,6 +1299,8 @@ func _build_anchor_inspector() -> void:
|
||||
var actions := HBoxContainer.new(); _anchor_section.add_child(actions)
|
||||
for label: String in ["Add Anchor", "Rename", "Delete"]:
|
||||
var button := Button.new(); button.text = label; button.custom_minimum_size.y = float(_theme.button_min_height()); actions.add_child(button)
|
||||
var role: StringName = &"primary" if label == "Add Anchor" else (&"destructive" if label == "Delete" else &"secondary")
|
||||
_editor_visual_theme.style_button_role(button, role, &"prop")
|
||||
if label == "Add Anchor": button.pressed.connect(_on_add_anchor_pressed)
|
||||
elif label == "Rename":
|
||||
_rename_anchor_button = button
|
||||
@@ -1324,11 +1313,17 @@ func _build_anchor_inspector() -> void:
|
||||
_anchor_x = _anchor_spin("X", -100000.0, 100000.0, _on_anchor_x_changed)
|
||||
_anchor_y = _anchor_spin("Y", -100000.0, 100000.0, _on_anchor_y_changed)
|
||||
_anchor_rotation = _anchor_spin("Rotation", -180.0, 179.0, _on_anchor_rotation_changed)
|
||||
_anchor_z = _anchor_spin("Z Offset", -5.0, 5.0, _on_anchor_z_changed)
|
||||
_anchor_z = _anchor_spin("Held prop layer", -5.0, 5.0, _on_anchor_z_changed)
|
||||
_anchor_z.step = 1.0
|
||||
var anchor_layer_hint := Label.new()
|
||||
anchor_layer_hint.text = "Negative: behind · 0: normal order · Positive: farther in front."
|
||||
anchor_layer_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
anchor_layer_hint.add_theme_font_size_override("font_size", _theme.font_status())
|
||||
anchor_layer_hint.add_theme_color_override("font_color", _theme.text_muted_color())
|
||||
_anchor_section.add_child(anchor_layer_hint)
|
||||
var presets := FlowContainer.new(); _anchor_section.add_child(presets)
|
||||
for entry: Dictionary in [{"text":"One-Handed","id":"one_handed"}, {"text":"Two-Handed","id":"two_handed"}, {"text":"Tool / Nozzle","id":"tool_nozzle"}, {"text":"Melee Edge","id":"melee_edge"}]:
|
||||
var button := Button.new(); button.text = str(entry["text"]); button.pressed.connect(func() -> void: _apply_anchor_preset(str(entry["id"]))); presets.add_child(button)
|
||||
var button := Button.new(); button.text = str(entry["text"]); button.custom_minimum_size.y = float(_theme.button_min_height()); button.pressed.connect(func() -> void: _apply_anchor_preset(str(entry["id"]))); presets.add_child(button)
|
||||
_refresh_anchor_inspector()
|
||||
|
||||
func _anchor_option(label_text: String, entries: PackedStringArray, callback: Callable) -> OptionButton:
|
||||
@@ -1341,6 +1336,7 @@ func _anchor_option(label_text: String, entries: PackedStringArray, callback: Ca
|
||||
row.add_child(label)
|
||||
var option := OptionButton.new()
|
||||
option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
option.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
for entry: String in entries:
|
||||
option.add_item(entry)
|
||||
option.item_selected.connect(callback)
|
||||
@@ -1360,6 +1356,7 @@ func _anchor_spin(label_text: String, minimum: float, maximum: float, callback:
|
||||
spin.max_value = maximum
|
||||
spin.step = 1.0
|
||||
spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
spin.custom_minimum_size.y = float(_theme.button_min_height())
|
||||
spin.value_changed.connect(callback)
|
||||
row.add_child(spin)
|
||||
return spin
|
||||
@@ -1467,14 +1464,15 @@ func _apply_control_theme(node: Node) -> void:
|
||||
func _update_physics_labels() -> void:
|
||||
var mat: PhysicsMaterial = PropBlockScript.physics_material(_material_preset)
|
||||
_mass_label.text = "Mass: %.1f" % PropBlockScript.mass_for(_material_preset)
|
||||
_friction_label.text = "Friction: %.2f" % mat.friction
|
||||
_friction_label.text = "Surface grip: %.2f" % mat.friction
|
||||
_friction_label.tooltip_text = "Higher values make sliding harder."
|
||||
_bounce_label.text = "Bounce: %.2f" % mat.bounce
|
||||
|
||||
|
||||
func _on_preset_selected(index: int) -> void:
|
||||
_material_preset = clampi(index, 0, PRESET_LABELS.size() - 1)
|
||||
_update_physics_labels()
|
||||
_update_save_status()
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
func _rebuild_primitive_params() -> void:
|
||||
@@ -1604,6 +1602,7 @@ func _refresh_inspector_from_selection() -> void:
|
||||
func _on_canvas_shape_changed(_shapes: Array[Dictionary]) -> void:
|
||||
_mark_dirty()
|
||||
_refresh_inspector_from_selection()
|
||||
_sync_mode_controls(_canvas.get_edit_mode())
|
||||
_refresh_anchor_inspector()
|
||||
_refresh_center_prop_button()
|
||||
_sync_attach_preview()
|
||||
@@ -1700,6 +1699,25 @@ func _center_prop_on_origin(focus_view: bool = true) -> bool:
|
||||
|
||||
func _on_canvas_shape_selected(_idx: int) -> void:
|
||||
_refresh_inspector_from_selection()
|
||||
_sync_mode_controls(_canvas.get_edit_mode())
|
||||
|
||||
|
||||
func _on_create_point_pressed() -> void:
|
||||
if _canvas.get_selected_index() < 0:
|
||||
return
|
||||
_canvas.arm_create_point()
|
||||
_sync_mode_buttons(_canvas.get_edit_mode())
|
||||
_sync_mode_controls(_canvas.get_edit_mode())
|
||||
_update_create_point_feedback()
|
||||
|
||||
|
||||
func _update_create_point_feedback() -> void:
|
||||
if _create_point_button == null or _create_point_hint == null or _canvas == null:
|
||||
return
|
||||
var armed: bool = _canvas.is_create_point_armed()
|
||||
_create_point_button.text = "Click an Edge to Place Point" if armed else "📍 Create Point"
|
||||
_create_point_button.tooltip_text = "Press Esc to cancel." if armed else "Add a point to the selected shape's edge."
|
||||
_create_point_hint.text = "Click near an edge to place the point. Press Esc to cancel." if armed else "Adds a point where you click on the selected edge."
|
||||
|
||||
|
||||
func _on_canvas_copy_requested(shape: Dictionary) -> void:
|
||||
@@ -1770,8 +1788,11 @@ func _check_autosave_recovery() -> void:
|
||||
dialog.title = "Restore Prop?"
|
||||
dialog.dialog_text = "An unsaved prop was recovered. Restore it?"
|
||||
dialog.ok_button_text = "🔄 Restore"
|
||||
dialog.add_button("🗑️ Discard", false, "discard")
|
||||
var discard_button: Button = dialog.add_button("🗑️ Discard", false, "discard")
|
||||
dialog.theme = _ui_theme
|
||||
_editor_visual_theme.style_button_role(dialog.get_ok_button(), &"primary", &"prop")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"prop")
|
||||
_editor_visual_theme.style_button_role(dialog.get_cancel_button(), &"secondary", &"prop")
|
||||
add_child(dialog)
|
||||
dialog.confirmed.connect(func() -> void:
|
||||
_apply_json_data(raw, true)
|
||||
|
||||
@@ -10,6 +10,7 @@ extends RefCounted
|
||||
## truth.
|
||||
|
||||
const PropBlockScript := preload("res://scripts/props/prop_block.gd")
|
||||
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
|
||||
const SCHEMA_VERSION: String = "1.0"
|
||||
const DEFAULT_PATH: String = "res://prop_theme.json"
|
||||
@@ -39,19 +40,16 @@ const DEFAULT_CONFIG: Dictionary = {
|
||||
"background_color": "#1F1F1F",
|
||||
"grid_color": "#FFFFFF26",
|
||||
},
|
||||
"ui": {
|
||||
"fonts": {"body": 18, "header": 24, "menu": 22, "menu_bar": 22, "mode": 26, "status": 18, "prop_name": 20, "context_menu": 22},
|
||||
"hit_targets": {"button_min_height": 36, "row_min_height": 36, "mode_button_min_height": 56},
|
||||
"colors": {"text": "#F2F2F2", "text_muted": "#9A9A9A", "border": "#FFFFFF33", "accent": "#4FA3FF", "inspector_background": "#262C39"},
|
||||
},
|
||||
}
|
||||
|
||||
var _config: Dictionary = DEFAULT_CONFIG.duplicate(true)
|
||||
var _warned: Dictionary = {}
|
||||
var _editor_visual_theme: EditorVisualTheme = EditorVisualThemeScript.new()
|
||||
|
||||
|
||||
func load_from(path: String = DEFAULT_PATH) -> bool:
|
||||
invalidate_caches()
|
||||
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||
_config = DEFAULT_CONFIG.duplicate(true)
|
||||
if not FileAccess.file_exists(path):
|
||||
_warn_once(path, "PropTheme: '%s' was not found; using defaults." % path)
|
||||
@@ -155,71 +153,71 @@ func canvas_grid_color() -> Color:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kid-friendly UI tokens (Round 2)
|
||||
# Compatibility accessors: editor chrome is owned by EditorVisualTheme.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func font_body() -> int:
|
||||
return integer("ui.fonts.body", 18, 8, 72)
|
||||
return _editor_visual_theme.chrome_px(&"body", &"prop")
|
||||
|
||||
|
||||
func font_header() -> int:
|
||||
return integer("ui.fonts.header", 24, 8, 96)
|
||||
return _editor_visual_theme.chrome_px(&"section", &"prop")
|
||||
|
||||
|
||||
func font_menu() -> int:
|
||||
return integer("ui.fonts.menu", 22, 8, 72)
|
||||
return _editor_visual_theme.chrome_px(&"menu", &"prop")
|
||||
|
||||
|
||||
func font_status() -> int:
|
||||
return integer("ui.fonts.status", 18, 8, 72)
|
||||
return _editor_visual_theme.chrome_px(&"status", &"prop")
|
||||
|
||||
|
||||
func font_mode() -> int:
|
||||
return integer("ui.fonts.mode", 26, 8, 96)
|
||||
return _editor_visual_theme.chrome_px(&"mode", &"prop")
|
||||
|
||||
|
||||
func font_menu_bar() -> int:
|
||||
return integer("ui.fonts.menu_bar", 22, 8, 72)
|
||||
return _editor_visual_theme.chrome_px(&"menu", &"prop")
|
||||
|
||||
|
||||
func font_prop_name() -> int:
|
||||
return integer("ui.fonts.prop_name", 20, 8, 96)
|
||||
return _editor_visual_theme.chrome_px(&"prop_name", &"prop")
|
||||
|
||||
|
||||
func font_context_menu() -> int:
|
||||
return integer("ui.fonts.context_menu", 22, 8, 96)
|
||||
return _editor_visual_theme.chrome_px(&"context_menu", &"prop")
|
||||
|
||||
|
||||
func button_min_height() -> int:
|
||||
return integer("ui.hit_targets.button_min_height", 36, 16, 200)
|
||||
return _editor_visual_theme.chrome_px(&"button_min_height", &"prop")
|
||||
|
||||
|
||||
func row_min_height() -> int:
|
||||
return integer("ui.hit_targets.row_min_height", 36, 16, 200)
|
||||
return _editor_visual_theme.chrome_px(&"row_min_height", &"prop")
|
||||
|
||||
|
||||
func mode_button_min_height() -> int:
|
||||
return integer("ui.hit_targets.mode_button_min_height", 56, 16, 200)
|
||||
return _editor_visual_theme.chrome_px(&"mode_button_min_height", &"prop")
|
||||
|
||||
|
||||
func text_color() -> Color:
|
||||
return color("ui.colors.text", Color("#F2F2F2"))
|
||||
return _editor_visual_theme.chrome_color(&"text", &"prop")
|
||||
|
||||
|
||||
func text_muted_color() -> Color:
|
||||
return color("ui.colors.text_muted", Color("#9A9A9A"))
|
||||
return _editor_visual_theme.chrome_color(&"text_muted", &"prop")
|
||||
|
||||
|
||||
func border_color() -> Color:
|
||||
return color("ui.colors.border", Color(1.0, 1.0, 1.0, 0.2))
|
||||
return _editor_visual_theme.chrome_color(&"border", &"prop")
|
||||
|
||||
|
||||
func accent_color() -> Color:
|
||||
return color("ui.colors.accent", Color("#4FA3FF"))
|
||||
return _editor_visual_theme.chrome_color(&"edit", &"prop")
|
||||
|
||||
|
||||
func inspector_background_color() -> Color:
|
||||
return color("ui.colors.inspector_background", Color("#262C39"))
|
||||
return _editor_visual_theme.chrome_color(&"surface", &"prop")
|
||||
|
||||
|
||||
func invalidate_caches() -> void:
|
||||
|
||||
@@ -63,6 +63,7 @@ const STAGE_PANEL_CHOICE_GROUP_SCRIPT := preload("res://scripts/shared/stage_ui/
|
||||
const STAGE_SURFACE_COLOR_CONTROLS_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_controls.gd")
|
||||
const STAGE_SURFACE_COLOR_DIALOG_SCRIPT := preload("res://scripts/shared/stage_ui/stage_surface_color_dialog.gd")
|
||||
const STAGE_SURFACE_PREVIEW_SCRIPT := preload("res://scripts/stage/scenery/stage_surface_preview.gd")
|
||||
const STAGE_GROUND_FINISH_CHOOSER_SCRIPT := preload("res://scripts/shared/stage_ui/stage_ground_finish_chooser.gd")
|
||||
const STAGE_DEPTH_CHOICE_PREVIEW_SCRIPT := preload("res://scripts/shared/stage_ui/stage_depth_choice_preview.gd")
|
||||
const PROP_FILE_SCRIPT := preload("res://scripts/editors/prop/prop_file.gd")
|
||||
const PROP_EDITOR_SCENE := preload("res://scenes/editors/prop/prop_editor.tscn")
|
||||
@@ -277,7 +278,7 @@ var _plain_color_draft: Color = Color("#F5EFE0")
|
||||
var _new_ground_finish: StringName = &"finish_ground_grass"
|
||||
var _hosted_prop_editor: PropEditor
|
||||
var _hosted_artwork_path: String = ""
|
||||
var _ground_finish_popup: PopupMenu
|
||||
var _ground_finish_chooser: StageGroundFinishChooser
|
||||
var _scenery_edit_mode: bool = false
|
||||
var _scenery_multi_select: bool = false
|
||||
var _scenery_single_dragging: bool = false
|
||||
@@ -1729,9 +1730,13 @@ func _handle_world_click(mb: InputEventMouseButton) -> void:
|
||||
return
|
||||
if _scenery_controller != null and _scenery_controller.is_active():
|
||||
var scenery_world_position: Vector2 = _viewport_to_world(mb.position)
|
||||
var return_to_scenery_selector: bool = mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed and _scenery_controller.is_placement_tool()
|
||||
if _scenery_controller.handle_mouse_button(mb, scenery_world_position, _is_mouse_over_ui()):
|
||||
get_viewport().set_input_as_handled()
|
||||
_refresh_scenery_capture_bar()
|
||||
if return_to_scenery_selector:
|
||||
_return_to_scenery_selector()
|
||||
else:
|
||||
_refresh_scenery_capture_bar()
|
||||
return
|
||||
if _scenery_edit_mode:
|
||||
var edit_world_position: Vector2 = _viewport_to_world(mb.position)
|
||||
@@ -3281,7 +3286,7 @@ func _build_ui() -> void:
|
||||
_asset_browser.surface_color_confirmed.connect(_record_recent_surface_color)
|
||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
|
||||
_build_ground_finish_popup(ui)
|
||||
_build_ground_finish_chooser(ui)
|
||||
|
||||
_modal_views.build_file_dialogs(ui)
|
||||
_scene_open_dialog = _modal_views.scene_open_dialog
|
||||
@@ -3895,43 +3900,41 @@ func _update_scenery_depth_cursor_badge() -> void:
|
||||
_scenery_depth_cursor_badge.visible = true
|
||||
|
||||
|
||||
func _build_ground_finish_popup(ui: CanvasLayer) -> void:
|
||||
_ground_finish_popup = PopupMenu.new()
|
||||
_ground_finish_popup.name = "NewGroundLook"
|
||||
_ground_finish_popup.theme = _sandbox_theme.control_theme() if _sandbox_theme != null else null
|
||||
for index: int in GROUND_FINISH_LABELS.size():
|
||||
_ground_finish_popup.add_item(GROUND_FINISH_LABELS[index], index)
|
||||
_ground_finish_popup.set_item_as_checkable(index, true)
|
||||
_ground_finish_popup.id_pressed.connect(_on_ground_finish_selected)
|
||||
ui.add_child(_ground_finish_popup)
|
||||
func _build_ground_finish_chooser(ui: CanvasLayer) -> void:
|
||||
_ground_finish_chooser = STAGE_GROUND_FINISH_CHOOSER_SCRIPT.new() as StageGroundFinishChooser
|
||||
_ground_finish_chooser.name = "NewGroundLook"
|
||||
_ground_finish_chooser.configure(_sandbox_theme as SandboxTheme, _ui_font, GROUND_FINISH_IDS, GROUND_FINISH_LABELS)
|
||||
_ground_finish_chooser.confirmed.connect(_on_ground_finish_draw_requested)
|
||||
_ground_finish_chooser.canceled.connect(_on_ground_finish_chooser_closed)
|
||||
ui.add_child(_ground_finish_chooser)
|
||||
|
||||
|
||||
func _open_ground_finish_popup() -> void:
|
||||
if _ground_finish_popup == null:
|
||||
func _open_ground_finish_chooser() -> void:
|
||||
if _ground_finish_chooser == null:
|
||||
return
|
||||
for index: int in GROUND_FINISH_IDS.size():
|
||||
_ground_finish_popup.set_item_checked(index, GROUND_FINISH_IDS[index] == _new_ground_finish)
|
||||
var origin: Vector2 = get_viewport().get_mouse_position()
|
||||
var ground_button: Button = _palette_buttons.get("ground") as Button
|
||||
if ground_button != null:
|
||||
var button_rect: Rect2 = ground_button.get_global_rect()
|
||||
origin = Vector2(button_rect.position.x, button_rect.end.y)
|
||||
var viewport_size: Vector2 = get_viewport_rect().size
|
||||
origin.x = clampf(origin.x, 8.0, maxf(8.0, viewport_size.x - 288.0))
|
||||
origin.y = clampf(origin.y, 8.0, maxf(8.0, viewport_size.y - 320.0))
|
||||
_ground_finish_popup.popup(Rect2i(Vector2i(origin), Vector2i(280, 312)))
|
||||
_ground_finish_chooser.open_for_finish(_new_ground_finish)
|
||||
|
||||
|
||||
func _on_ground_finish_selected(index: int) -> void:
|
||||
if index < 0 or index >= GROUND_FINISH_IDS.size():
|
||||
func _on_ground_finish_draw_requested() -> void:
|
||||
if _ground_finish_chooser == null:
|
||||
return
|
||||
_new_ground_finish = GROUND_FINISH_IDS[index]
|
||||
var finish_id: StringName = _ground_finish_chooser.selected_finish()
|
||||
var index: int = GROUND_FINISH_IDS.find(finish_id)
|
||||
if index < 0 or index >= GROUND_FINISH_LABELS.size():
|
||||
return
|
||||
_new_ground_finish = finish_id
|
||||
if _terrain_painter != null:
|
||||
_terrain_painter.set_ground_finish(_new_ground_finish)
|
||||
set_placement_mode("ground")
|
||||
_show_toast("New Ground look: %s" % GROUND_FINISH_LABELS[index])
|
||||
|
||||
|
||||
func _on_ground_finish_chooser_closed() -> void:
|
||||
for placement_id: String in _palette_buttons:
|
||||
var palette_button: Button = _palette_buttons[placement_id] as Button
|
||||
palette_button.set_pressed_no_signal(placement_id == _placement_id)
|
||||
|
||||
|
||||
func _build_use_spot_overlay(ui: CanvasLayer) -> void:
|
||||
_use_spot_overlay = STAGE_USE_SPOT_OVERLAY_SCRIPT.new() as StageUseSpotOverlay
|
||||
_use_spot_overlay.name = "UseSpotOverlay"
|
||||
@@ -4938,7 +4941,7 @@ func _on_palette_toggled(pressed: bool, id: String) -> void:
|
||||
_open_selector(id)
|
||||
return
|
||||
if id == "ground":
|
||||
_open_ground_finish_popup()
|
||||
_open_ground_finish_chooser()
|
||||
return
|
||||
set_placement_mode(id)
|
||||
else:
|
||||
@@ -4954,7 +4957,7 @@ func _on_palette_card_activated(payload: Dictionary) -> void:
|
||||
if bool(payload.get("opens_selector", false)):
|
||||
_open_selector(id)
|
||||
elif id == "ground":
|
||||
_open_ground_finish_popup()
|
||||
_open_ground_finish_chooser()
|
||||
else:
|
||||
set_placement_mode(id)
|
||||
|
||||
@@ -5001,12 +5004,13 @@ func _is_stage_drop_position(viewport_pos: Vector2) -> bool:
|
||||
return hovered == null or hovered.mouse_filter == Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func _open_selector(kind: String) -> void:
|
||||
func _open_selector(kind: String, preserve_scenery_browse_state: bool = false) -> void:
|
||||
if kind == "scenery":
|
||||
if _scenery_edit_mode:
|
||||
_exit_scenery_edit_mode()
|
||||
if _scenery_controller != null and _scenery_controller.is_active():
|
||||
_scenery_controller.exit_tool()
|
||||
_hide_scenery_capture_bar()
|
||||
_scenery_placed_count = 0
|
||||
elif _scenery_edit_mode:
|
||||
_exit_scenery_edit_mode()
|
||||
@@ -5017,12 +5021,14 @@ func _open_selector(kind: String) -> void:
|
||||
(_palette_buttons[pid] as Button).set_pressed_no_signal(pid == kind)
|
||||
if kind == "scenery":
|
||||
_asset_browser.set_surface_color_history(_recent_surface_colors)
|
||||
_asset_browser.set_background_state(_background_kind, _sky_id, _plain_color)
|
||||
var selector_state: Dictionary = _asset_browser.open_selector(
|
||||
kind,
|
||||
_spawner.get_selected_stickman_path(),
|
||||
_sky_id,
|
||||
_background_kind,
|
||||
_plain_color
|
||||
_plain_color,
|
||||
preserve_scenery_browse_state
|
||||
)
|
||||
if kind == "stickman":
|
||||
var selected_stickman_path: String = String(selector_state.get("selected_stickman_path", _spawner.get_selected_stickman_path()))
|
||||
@@ -5099,6 +5105,11 @@ func _on_asset_selected(entry: Dictionary) -> void:
|
||||
set_placement_mode(kind)
|
||||
|
||||
|
||||
func _return_to_scenery_selector() -> void:
|
||||
_hide_scenery_capture_bar()
|
||||
_open_selector("scenery", true)
|
||||
|
||||
|
||||
func _load_starter_scene(entry: Dictionary) -> void:
|
||||
var path: String = CONTENT_PATHS.canonicalize(String(entry.get("path", "")))
|
||||
var read_result: Dictionary = _document.read(path) if _document != null else {}
|
||||
@@ -5658,7 +5669,7 @@ func _refresh_scenery_capture_bar() -> void:
|
||||
actions.append(_make_scenery_depth_capture_action(&"depth_floor", &"floor", "Front", selected_depth))
|
||||
var artwork_name: String = String(_scenery_controller.get_active_artwork_name())
|
||||
var prompt: String = "Click to place %s" % artwork_name if _scenery_placed_count == 0 else "Place another · Cancel undoes"
|
||||
actions.append({"id": "done_tool", "label": "Done", "primary": true})
|
||||
actions.append({"id": "done_tool", "label": "Back", "primary": true})
|
||||
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
||||
_use_target_capture_bar.set_scenery_capture_actions(prompt, actions)
|
||||
elif _scenery_controller.get_active_tool() in [&"wall_eraser", &"background_ground_eraser", &"scenery_eraser"]:
|
||||
@@ -5672,8 +5683,7 @@ func _refresh_scenery_capture_bar() -> void:
|
||||
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
||||
_use_target_capture_bar.set_scenery_capture_actions(erase_prompt, actions)
|
||||
else:
|
||||
actions.append({"id": "choose_scenery", "label": "Choose scenery"})
|
||||
actions.append({"id": "done_tool", "label": "Done", "primary": true})
|
||||
actions.append({"id": "done_tool", "label": "Back", "primary": true})
|
||||
actions.append({"id": "cancel_tool", "label": "Cancel"})
|
||||
if _scenery_controller.get_active_tool() == &"background_ground":
|
||||
var ground_prompt: String = "Draw background ground · No walking"
|
||||
@@ -5737,8 +5747,6 @@ func _hide_scenery_capture_bar() -> void:
|
||||
|
||||
func _on_capture_action_requested(action_id: StringName) -> void:
|
||||
match action_id:
|
||||
&"choose_scenery":
|
||||
_open_selector("scenery")
|
||||
&"depth_far":
|
||||
_scenery_controller.set_depth(&"far")
|
||||
_refresh_scenery_capture_bar()
|
||||
@@ -5749,8 +5757,12 @@ func _on_capture_action_requested(action_id: StringName) -> void:
|
||||
_scenery_controller.set_depth(&"floor")
|
||||
_refresh_scenery_capture_bar()
|
||||
&"done_tool":
|
||||
_scenery_controller.exit_tool()
|
||||
_hide_scenery_capture_bar()
|
||||
if _scenery_controller != null and _scenery_controller.is_placement_tool():
|
||||
_return_to_scenery_selector()
|
||||
else:
|
||||
if _scenery_controller != null:
|
||||
_scenery_controller.exit_tool()
|
||||
_hide_scenery_capture_bar()
|
||||
&"cancel_tool":
|
||||
_scenery_controller.cancel_active_tool()
|
||||
_hide_scenery_capture_bar()
|
||||
|
||||
@@ -160,8 +160,12 @@ func open_selector(
|
||||
current_stickman_path: String,
|
||||
current_sky_id: StringName = &"",
|
||||
background_kind: StringName = &"sky",
|
||||
plain_color: Color = Color("#F5EFE0")
|
||||
plain_color: Color = Color("#F5EFE0"),
|
||||
preserve_scenery_browse_state: bool = false
|
||||
) -> Dictionary:
|
||||
var scenery_browse_state: Dictionary = {}
|
||||
if preserve_scenery_browse_state and kind == "scenery" and selector != null:
|
||||
scenery_browse_state = selector.capture_browse_state()
|
||||
_current_background_kind = &"plain" if background_kind == &"plain" else &"sky"
|
||||
_current_sky_id = current_sky_id if not current_sky_id.is_empty() else &"sky_neutral"
|
||||
_current_plain_color = Color(plain_color.r, plain_color.g, plain_color.b, 1.0)
|
||||
@@ -177,7 +181,7 @@ func open_selector(
|
||||
selected_stickman_path = String(entries[0].get("path", ""))
|
||||
var auto_selection: Dictionary = entries[0] if kind == "stickman" and entries.size() == 1 else {}
|
||||
if auto_selection.is_empty():
|
||||
open(kind, entries, _current_sky_id)
|
||||
open(kind, entries, _current_sky_id, scenery_browse_state)
|
||||
return {
|
||||
"entries": entries,
|
||||
"selected_stickman_path": selected_stickman_path,
|
||||
@@ -201,14 +205,19 @@ func set_selector_kind(kind: String) -> void:
|
||||
selector_kind = kind
|
||||
|
||||
|
||||
func open(kind: String, entries: Array[Dictionary], current_sky_id: StringName = &"") -> void:
|
||||
func open(
|
||||
kind: String,
|
||||
entries: Array[Dictionary],
|
||||
current_sky_id: StringName = &"",
|
||||
scenery_browse_state: Dictionary = {}
|
||||
) -> void:
|
||||
selector_kind = kind
|
||||
selector_open = true
|
||||
if selector_dim != null:
|
||||
selector_dim.visible = true
|
||||
selector_dim.color = Color(0.0, 0.0, 0.0, SELECTOR_DIM_ALPHA)
|
||||
if selector != null:
|
||||
selector.open(kind, entries, String(current_sky_id), String(_current_background_kind), _current_plain_color)
|
||||
selector.open(kind, entries, String(current_sky_id), String(_current_background_kind), _current_plain_color, scenery_browse_state)
|
||||
load_or_enqueue_thumbnails(kind, entries)
|
||||
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ func is_active() -> bool:
|
||||
return not _active_tool.is_empty()
|
||||
|
||||
|
||||
func is_placement_tool() -> bool:
|
||||
return _is_placement_tool()
|
||||
|
||||
|
||||
func is_group_preview_pending() -> bool:
|
||||
return _group_pending
|
||||
|
||||
|
||||
@@ -893,7 +893,7 @@ func _apply_compact_action_labels(compact: bool) -> void:
|
||||
"resize_group": "Size",
|
||||
"keep_group": "Keep",
|
||||
"cancel_group": "Cancel",
|
||||
"done_tool": "Done",
|
||||
"done_tool": "Back",
|
||||
"cancel_tool": "Cancel",
|
||||
"depth_far": "Back",
|
||||
"depth_near": "Middle",
|
||||
|
||||
@@ -20,6 +20,7 @@ signal shape_selected()
|
||||
signal shape_copy_requested(shape_dict: Dictionary)
|
||||
signal shape_paste_requested(world_pos: Vector2)
|
||||
signal color_selected(color: Color)
|
||||
signal presentation_safe
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exported / public properties
|
||||
@@ -66,6 +67,7 @@ const ZOOM_STEP: float = 1.10
|
||||
@onready var context_menu: PopupMenu = %ContextMenu
|
||||
@onready var color_picker_popup: PopupPanel = %ColorPickerPopup
|
||||
@onready var color_picker: ColorPicker = %ColorPicker
|
||||
@onready var color_picker_button_row: HBoxContainer = %ButtonRow
|
||||
@onready var cancel_button: Button = %CancelButton
|
||||
@onready var ok_button: Button = %OKButton
|
||||
|
||||
@@ -89,6 +91,12 @@ var _snap_enabled: bool = false
|
||||
var _is_panning: bool = false
|
||||
var _pan_start: Vector2 = Vector2.ZERO
|
||||
var _restore_color: Color = Color.BLACK
|
||||
var _grid_color: Color = GRID_COLOR
|
||||
var _selection_core_color: Color = Color.WHITE
|
||||
var _selection_halo_color: Color = Color(0.0, 0.0, 0.0, 0.85)
|
||||
var _hint_color: Color = Color(0.67, 0.70, 0.75, 0.8)
|
||||
var _button_target_height: float = 44.0
|
||||
var _editor_visual_theme: EditorVisualTheme
|
||||
|
||||
# Phase 4: tracks which shape was right-clicked for context menu ops.
|
||||
var _context_shape_idx: int = -1
|
||||
@@ -117,6 +125,18 @@ func _ready() -> void:
|
||||
ok_button.pressed.connect(_on_color_picker_ok)
|
||||
if not cancel_button.pressed.is_connected(_on_color_picker_cancel):
|
||||
cancel_button.pressed.connect(_on_color_picker_cancel)
|
||||
if not context_menu.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||
context_menu.popup_hide.connect(_on_presentation_popup_hidden)
|
||||
if not color_picker_popup.popup_hide.is_connected(_on_presentation_popup_hidden):
|
||||
color_picker_popup.popup_hide.connect(_on_presentation_popup_hidden)
|
||||
cancel_button.text = "Cancel"
|
||||
cancel_button.custom_minimum_size.y = _button_target_height
|
||||
ok_button.text = "Use Color"
|
||||
ok_button.custom_minimum_size.y = _button_target_height
|
||||
color_picker_button_row.add_theme_constant_override("separation", 12)
|
||||
if _editor_visual_theme != null:
|
||||
_editor_visual_theme.style_button_role(ok_button, &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(cancel_button, &"secondary", &"stickman")
|
||||
|
||||
_drawing_area_initialized = true
|
||||
|
||||
@@ -262,6 +282,75 @@ func reset_view() -> void:
|
||||
_pan_offset = Vector2.ZERO
|
||||
drawing_area.queue_redraw()
|
||||
|
||||
|
||||
func can_switch_presentation() -> bool:
|
||||
return not _is_panning \
|
||||
and _dragging_vertex < 0 \
|
||||
and not _is_dragging_shape \
|
||||
and not context_menu.visible \
|
||||
and not color_picker_popup.visible
|
||||
|
||||
|
||||
func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo: Color, hint_color: Color = Color(0.67, 0.70, 0.75, 0.8), button_target_height: float = 44.0, editor_visual_theme: EditorVisualTheme = null) -> void:
|
||||
_grid_color = grid_color
|
||||
_selection_core_color = selection_core
|
||||
_selection_halo_color = selection_halo
|
||||
_hint_color = hint_color
|
||||
_button_target_height = button_target_height
|
||||
_editor_visual_theme = editor_visual_theme
|
||||
if drawing_area != null:
|
||||
drawing_area.queue_redraw()
|
||||
|
||||
|
||||
func set_active_presentation(active: bool, surface: Color, border: Color, accent: Color, section_font_size: int = 22) -> void:
|
||||
var style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
style.bg_color = surface
|
||||
style.border_color = accent if active else border
|
||||
style.set_border_width_all(2 if active else 1)
|
||||
style.set_corner_radius_all(8)
|
||||
style.content_margin_left = 6
|
||||
style.content_margin_right = 6
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
add_theme_stylebox_override("panel", style)
|
||||
title_label.add_theme_color_override("font_color", accent if active else _selection_core_color)
|
||||
title_label.add_theme_font_size_override("font_size", section_font_size)
|
||||
|
||||
|
||||
func add_shape(kind: StringName, at_world: Vector2) -> bool:
|
||||
match kind:
|
||||
&"line": _create_line(at_world)
|
||||
&"rectangle": _create_rectangle(at_world)
|
||||
&"circle": _create_circle(at_world)
|
||||
_:
|
||||
return false
|
||||
drawing_area.queue_redraw()
|
||||
_emit_changed()
|
||||
return true
|
||||
|
||||
|
||||
func selected_shape_index() -> int:
|
||||
return _selected_shape_idx
|
||||
|
||||
|
||||
func has_selected_shape() -> bool:
|
||||
return _selected_shape_idx >= 0 and _selected_shape_idx < shapes.size()
|
||||
|
||||
|
||||
func open_selected_color_picker() -> void:
|
||||
if not has_selected_shape():
|
||||
return
|
||||
_context_shape_idx = _selected_shape_idx
|
||||
_show_color_picker()
|
||||
|
||||
|
||||
func get_view_context() -> Dictionary:
|
||||
return {"zoom": _zoom, "pan_offset": _pan_offset}
|
||||
|
||||
|
||||
func drawing_center_world() -> Vector2:
|
||||
return _screen_to_world(drawing_area.size * 0.5)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: Clipboard
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -358,11 +447,11 @@ func _on_drawing_area_draw() -> void:
|
||||
drawing_area.draw_string(
|
||||
font,
|
||||
hint_pos,
|
||||
"Right-click to add shape",
|
||||
"Use Add Shape above",
|
||||
HORIZONTAL_ALIGNMENT_CENTER,
|
||||
-1,
|
||||
14,
|
||||
Color(0.5, 0.5, 0.5, 0.6)
|
||||
_hint_color
|
||||
)
|
||||
return
|
||||
|
||||
@@ -386,7 +475,8 @@ func _on_drawing_area_draw() -> void:
|
||||
|
||||
# 3. Selection highlight (only on selected shape)
|
||||
if i == _selected_shape_idx:
|
||||
_draw_polyline(pts, Color.WHITE, closed, 2.5)
|
||||
_draw_polyline(pts, _selection_halo_color, closed, 5.0)
|
||||
_draw_polyline(pts, _selection_core_color, closed, 2.5)
|
||||
|
||||
# 4. Vertex handles (only on selected shape)
|
||||
for j: int in range(count):
|
||||
@@ -396,6 +486,7 @@ func _on_drawing_area_draw() -> void:
|
||||
if sd_vf.size() > j:
|
||||
flag = sd_vf[j]
|
||||
|
||||
drawing_area.draw_circle(pt, (HANDLE_RADIUS + 2.0) / _zoom, _selection_halo_color)
|
||||
var handle_color := color
|
||||
if j == _dragging_vertex:
|
||||
handle_color = Color.YELLOW
|
||||
@@ -423,12 +514,12 @@ func _draw_grid() -> void:
|
||||
|
||||
var x: float = start_x
|
||||
while x <= world_end.x:
|
||||
drawing_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), GRID_COLOR, line_width)
|
||||
drawing_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), _grid_color, line_width)
|
||||
x += gs
|
||||
|
||||
var y: float = start_y
|
||||
while y <= world_end.y:
|
||||
drawing_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), GRID_COLOR, line_width)
|
||||
drawing_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), _grid_color, line_width)
|
||||
y += gs
|
||||
|
||||
|
||||
@@ -470,6 +561,7 @@ func _on_drawing_area_gui_input(event: InputEvent) -> void:
|
||||
_dragging_vertex = -1
|
||||
else:
|
||||
_is_panning = false
|
||||
_emit_presentation_safe()
|
||||
return
|
||||
|
||||
# 2. Mouse wheel -> zoom
|
||||
@@ -660,6 +752,7 @@ func _handle_left_release() -> void:
|
||||
_shape_drag_original_points = PackedVector2Array()
|
||||
drawing_area.queue_redraw()
|
||||
_emit_changed()
|
||||
_emit_presentation_safe()
|
||||
return
|
||||
|
||||
if _dragging_vertex >= 0:
|
||||
@@ -667,6 +760,22 @@ func _handle_left_release() -> void:
|
||||
_drag_offset = Vector2.ZERO
|
||||
drawing_area.queue_redraw()
|
||||
_emit_changed()
|
||||
_emit_presentation_safe()
|
||||
return
|
||||
_emit_presentation_safe()
|
||||
|
||||
|
||||
func _on_presentation_popup_hidden() -> void:
|
||||
_emit_presentation_safe()
|
||||
|
||||
|
||||
func _emit_presentation_safe() -> void:
|
||||
call_deferred("_emit_presentation_safe_if_idle")
|
||||
|
||||
|
||||
func _emit_presentation_safe_if_idle() -> void:
|
||||
if can_switch_presentation():
|
||||
presentation_safe.emit()
|
||||
|
||||
|
||||
func _on_context_menu_id_pressed(id: int) -> void:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
class_name StickmanEditor
|
||||
extends Control
|
||||
## StickmanEditor - Main controller for the Stickman Studio editor.
|
||||
##
|
||||
@@ -15,6 +16,10 @@ extends Control
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const ContentPathsScript := preload("res://scripts/shared/content_paths.gd")
|
||||
const EditorVisualThemeScript := preload("res://scripts/shared/editor_ui/editor_visual_theme.gd")
|
||||
|
||||
enum WorkspaceView { OVERVIEW, FOCUS }
|
||||
enum PendingDocumentAction { NONE, LOAD, CLEAR, QUIT }
|
||||
|
||||
const BODY_PART_NAMES: PackedStringArray = [
|
||||
"head", "torso",
|
||||
@@ -89,6 +94,10 @@ const SETTINGS_VERSION := "1.0"
|
||||
const DEFAULT_GRID_SIZE := 15
|
||||
const MIN_GRID_SIZE := 1
|
||||
const MAX_GRID_SIZE := 100
|
||||
const OVERVIEW_PANEL_MIN_WIDTH: float = 230.0
|
||||
const OVERVIEW_PREVIEW_MIN_WIDTH: float = 320.0
|
||||
const FOCUS_PANEL_MIN_WIDTH: float = 350.0
|
||||
const WORKSPACE_GAP: float = 8.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# On-ready node references
|
||||
@@ -97,6 +106,19 @@ const MAX_GRID_SIZE := 100
|
||||
@onready var _stickman_name_edit: LineEdit = %StickmanNameEdit
|
||||
@onready var _menu_bar: MenuBar = %MenuBar
|
||||
@onready var _body_part_panels: Dictionary = {} ## { String : BodyPartPanel }
|
||||
@onready var _body_part_workspace: Control = %BodyPartWorkspace
|
||||
@onready var _overview_scroll: ScrollContainer = %OverviewScroll
|
||||
@onready var _focus_host: VBoxContainer = %FocusHost
|
||||
@onready var _focus_panel_host: Control = %FocusPanelHost
|
||||
@onready var _focus_part_label: Label = %FocusPartLabel
|
||||
@onready var _workspace_row: HSplitContainer = %WorkspaceRow
|
||||
@onready var _overview_button: Button = %OverviewButton
|
||||
@onready var _focus_button: Button = %FocusButton
|
||||
@onready var _part_picker: OptionButton = %PartPicker
|
||||
@onready var _add_shape_button: Button = %AddShapeButton
|
||||
@onready var _color_button: Button = %ColorButton
|
||||
@onready var _whole_stickman_button: Button = %WholeStickmanButton
|
||||
@onready var _back_to_part_button: Button = %BackToPartButton
|
||||
@onready var _whole_preview: WholeStickmanPreview = %WholeStickmanPreview
|
||||
@onready var _save_dialog: FileDialog = %SaveDialog
|
||||
@onready var _load_dialog: FileDialog = %LoadDialog
|
||||
@@ -106,6 +128,24 @@ const MAX_GRID_SIZE := 100
|
||||
@onready var _grid_size_spin_box: SpinBox = %GridSizeSpinBox
|
||||
@onready var _status_cursor_coords: Label = %CursorCoords
|
||||
@onready var _status_snap_status: Label = %SnapStatus
|
||||
@onready var _status_save_status: Label = %SaveStatus
|
||||
@onready var _unsaved_dialog: ConfirmationDialog = %UnsavedChangesDialog
|
||||
|
||||
var _editor_visual_theme: EditorVisualTheme
|
||||
var _body_part_origins: Dictionary = {}
|
||||
var _workspace_view: WorkspaceView = WorkspaceView.OVERVIEW
|
||||
var _view_choice_was_manual: bool = false
|
||||
var _active_part_name: String = "head"
|
||||
var _pending_view: int = -1
|
||||
var _pending_part_name: String = ""
|
||||
var _workspace_controls_initialized: bool = false
|
||||
var _view_button_group: ButtonGroup
|
||||
var _add_shape_menu: PopupMenu
|
||||
var _preview_takes_workspace: bool = false
|
||||
var _current_path: String = ""
|
||||
var _dirty: bool = false
|
||||
var _applying_document: bool = false
|
||||
var _pending_document_action: PendingDocumentAction = PendingDocumentAction.NONE
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Grid / snap state
|
||||
@@ -131,19 +171,42 @@ var _view_menu: PopupMenu
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_editor_visual_theme = EditorVisualThemeScript.new()
|
||||
_editor_visual_theme.load_from(EditorVisualTheme.DEFAULT_PATH)
|
||||
theme = _editor_visual_theme.control_theme(&"stickman")
|
||||
_setup_menu_bar()
|
||||
_setup_dialogs()
|
||||
_populate_body_part_panels()
|
||||
_setup_workspace_controls()
|
||||
_whole_preview.part_moved.connect(_on_preview_part_moved)
|
||||
_whole_preview.part_transform_committed.connect(_on_preview_part_transform_committed)
|
||||
_whole_preview.part_order_changed.connect(_on_preview_part_order_changed)
|
||||
_whole_preview.set_body_parts(_collect_all_shape_data())
|
||||
_whole_preview.set_canvas_tokens(
|
||||
_editor_visual_theme.stickman_canvas_color(&"grid_color"),
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_core"),
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||
_editor_visual_theme.chrome_color(&"preview", &"stickman")
|
||||
)
|
||||
_stickman_name_edit.text_changed.connect(_on_stickman_name_changed)
|
||||
|
||||
_load_settings()
|
||||
_update_snap_status_label()
|
||||
_broadcast_settings()
|
||||
_broadcast_recent_colors()
|
||||
_update_active_part_presentation()
|
||||
_update_document_status()
|
||||
|
||||
if not _grid_config_dialog.confirmed.is_connected(_on_grid_config_confirmed):
|
||||
_grid_config_dialog.confirmed.connect(_on_grid_config_confirmed)
|
||||
call_deferred("_initialize_workspace_presentation")
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||
_request_document_action(PendingDocumentAction.QUIT)
|
||||
elif what == NOTIFICATION_RESIZED:
|
||||
call_deferred("_refresh_responsive_workspace")
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
@@ -151,7 +214,7 @@ func _process(_delta: float) -> void:
|
||||
|
||||
for part_name: String in BODY_PART_NAMES:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
if panel and panel.is_cursor_over_drawing(mouse_pos):
|
||||
if panel and panel.is_visible_in_tree() and panel.is_cursor_over_drawing(mouse_pos):
|
||||
var world_pos := panel.global_to_world(mouse_pos)
|
||||
_status_cursor_coords.text = "X: %d Y: %d" % [int(world_pos.x), int(world_pos.y)]
|
||||
return
|
||||
@@ -173,7 +236,7 @@ func _setup_menu_bar() -> void:
|
||||
file_menu.add_item("Save", 0)
|
||||
file_menu.add_item("Load", 1)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Clear", 2)
|
||||
file_menu.add_item("Start Over", 2)
|
||||
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
||||
_menu_bar.add_child(file_menu)
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "File")
|
||||
@@ -201,12 +264,319 @@ func _setup_menu_bar() -> void:
|
||||
|
||||
|
||||
func _setup_dialogs() -> void:
|
||||
_save_dialog.theme = theme
|
||||
_load_dialog.theme = theme
|
||||
_clear_confirm.theme = theme
|
||||
_unsaved_dialog.theme = theme
|
||||
_error_dialog.theme = theme
|
||||
_grid_config_dialog.theme = theme
|
||||
var content_dir: String = ProjectSettings.globalize_path(ContentPathsScript.STICKMEN_DIR)
|
||||
_save_dialog.current_dir = content_dir
|
||||
_load_dialog.current_dir = content_dir
|
||||
_save_dialog.file_selected.connect(_on_save_file_selected)
|
||||
_load_dialog.file_selected.connect(_on_load_file_selected)
|
||||
_editor_visual_theme.style_button_role(_save_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_save_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_load_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_clear_confirm.confirmed.connect(_on_clear_confirmed)
|
||||
_clear_confirm.get_cancel_button().text = "Cancel"
|
||||
_editor_visual_theme.style_button_role(_clear_confirm.get_ok_button(), &"destructive", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_clear_confirm.get_cancel_button(), &"secondary", &"stickman")
|
||||
var discard_button: Button = _unsaved_dialog.add_button("Discard Changes", false, "discard")
|
||||
_unsaved_dialog.get_cancel_button().text = "Cancel"
|
||||
_editor_visual_theme.style_button_role(_unsaved_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(discard_button, &"destructive", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_unsaved_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_error_dialog.get_ok_button(), &"secondary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_grid_config_dialog.get_ok_button(), &"primary", &"stickman")
|
||||
_editor_visual_theme.style_button_role(_grid_config_dialog.get_cancel_button(), &"secondary", &"stickman")
|
||||
_unsaved_dialog.confirmed.connect(_on_unsaved_save_pressed)
|
||||
_unsaved_dialog.custom_action.connect(_on_unsaved_custom_action)
|
||||
_unsaved_dialog.canceled.connect(_cancel_pending_document_action)
|
||||
get_tree().auto_accept_quit = false
|
||||
|
||||
|
||||
func _setup_workspace_controls() -> void:
|
||||
_view_button_group = ButtonGroup.new()
|
||||
_overview_button.button_group = _view_button_group
|
||||
_focus_button.button_group = _view_button_group
|
||||
_overview_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_focus_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"mode_button_min_height", &"stickman"))
|
||||
_part_picker.custom_minimum_size = Vector2(160.0, float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")))
|
||||
_add_shape_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_editor_visual_theme.style_button_role(_add_shape_button, &"primary", &"stickman")
|
||||
_color_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_editor_visual_theme.style_button_role(_color_button, &"secondary", &"stickman")
|
||||
_whole_stickman_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_back_to_part_button.custom_minimum_size.y = float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman"))
|
||||
_focus_part_label.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"section", &"stickman"))
|
||||
_status_cursor_coords.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
_status_save_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
_status_snap_status.add_theme_font_size_override("font_size", _editor_visual_theme.chrome_px(&"status", &"stickman"))
|
||||
for part_name: String in BODY_PART_NAMES:
|
||||
_part_picker.add_item(BODY_PART_DISPLAY[part_name])
|
||||
_overview_button.pressed.connect(func() -> void: request_workspace_view(WorkspaceView.OVERVIEW))
|
||||
_focus_button.pressed.connect(func() -> void: request_workspace_view(WorkspaceView.FOCUS))
|
||||
_part_picker.item_selected.connect(_on_part_picker_selected)
|
||||
_add_shape_button.pressed.connect(_on_add_shape_button_pressed)
|
||||
_color_button.pressed.connect(_on_color_button_pressed)
|
||||
_whole_stickman_button.pressed.connect(_on_whole_stickman_button_pressed)
|
||||
_back_to_part_button.pressed.connect(_on_back_to_part_button_pressed)
|
||||
_add_shape_menu = PopupMenu.new()
|
||||
_add_shape_menu.add_item("Line", 0)
|
||||
_add_shape_menu.add_item("Rectangle", 1)
|
||||
_add_shape_menu.add_item("Circle", 2)
|
||||
_add_shape_menu.id_pressed.connect(_on_add_shape_menu_id_pressed)
|
||||
_add_shape_menu.theme = theme
|
||||
add_child(_add_shape_menu)
|
||||
_workspace_controls_initialized = true
|
||||
_refresh_workspace_controls()
|
||||
|
||||
|
||||
func _initialize_workspace_presentation() -> void:
|
||||
if _workspace_row.size.x <= 0.0:
|
||||
await get_tree().process_frame
|
||||
if not _view_choice_was_manual:
|
||||
var initial_view: WorkspaceView = WorkspaceView.OVERVIEW if _overview_width_fits() else WorkspaceView.FOCUS
|
||||
request_workspace_view(initial_view, false)
|
||||
_refresh_responsive_workspace()
|
||||
|
||||
|
||||
func request_workspace_view(view: WorkspaceView, user_chosen: bool = true) -> void:
|
||||
if user_chosen:
|
||||
_view_choice_was_manual = true
|
||||
var panel: BodyPartPanel = _active_panel()
|
||||
if panel != null and not panel.can_switch_presentation():
|
||||
_pending_view = int(view)
|
||||
return
|
||||
_pending_view = -1
|
||||
_apply_workspace_view(view)
|
||||
|
||||
|
||||
func request_active_part(part_name: String) -> void:
|
||||
if not BODY_PART_NAMES.has(part_name):
|
||||
return
|
||||
var panel: BodyPartPanel = _active_panel()
|
||||
if panel != null and not panel.can_switch_presentation():
|
||||
_pending_part_name = part_name
|
||||
return
|
||||
_pending_part_name = ""
|
||||
_set_active_part(part_name)
|
||||
|
||||
|
||||
func get_workspace_view() -> WorkspaceView:
|
||||
return _workspace_view
|
||||
|
||||
|
||||
func get_active_part_name() -> String:
|
||||
return _active_part_name
|
||||
|
||||
|
||||
func has_unsaved_changes() -> bool:
|
||||
return _dirty
|
||||
|
||||
|
||||
func current_path() -> String:
|
||||
return _current_path
|
||||
|
||||
|
||||
func _apply_pending_presentation() -> void:
|
||||
var panel: BodyPartPanel = _active_panel()
|
||||
if panel != null and not panel.can_switch_presentation():
|
||||
return
|
||||
if not _pending_part_name.is_empty():
|
||||
var requested_part: String = _pending_part_name
|
||||
_pending_part_name = ""
|
||||
_set_active_part(requested_part)
|
||||
if _pending_view >= 0:
|
||||
var requested_view: WorkspaceView = _pending_view
|
||||
_pending_view = -1
|
||||
_apply_workspace_view(requested_view)
|
||||
|
||||
|
||||
func _apply_workspace_view(view: WorkspaceView) -> void:
|
||||
if view == _workspace_view:
|
||||
_refresh_workspace_controls()
|
||||
_refresh_responsive_workspace()
|
||||
return
|
||||
if _workspace_view == WorkspaceView.FOCUS:
|
||||
_restore_panel_to_overview(_active_part_name)
|
||||
_workspace_view = view
|
||||
_preview_takes_workspace = false
|
||||
if _workspace_view == WorkspaceView.FOCUS:
|
||||
_move_panel_to_focus(_active_part_name)
|
||||
_overview_scroll.visible = _workspace_view == WorkspaceView.OVERVIEW
|
||||
_focus_host.visible = _workspace_view == WorkspaceView.FOCUS
|
||||
_refresh_workspace_controls()
|
||||
_update_active_part_presentation()
|
||||
_refresh_responsive_workspace()
|
||||
|
||||
|
||||
func _set_active_part(part_name: String) -> void:
|
||||
if not BODY_PART_NAMES.has(part_name) or part_name == _active_part_name:
|
||||
_refresh_workspace_controls()
|
||||
return
|
||||
if _workspace_view == WorkspaceView.FOCUS:
|
||||
_restore_panel_to_overview(_active_part_name)
|
||||
_active_part_name = part_name
|
||||
if _workspace_view == WorkspaceView.FOCUS:
|
||||
_move_panel_to_focus(_active_part_name)
|
||||
_refresh_workspace_controls()
|
||||
_update_active_part_presentation()
|
||||
|
||||
|
||||
func _active_panel() -> BodyPartPanel:
|
||||
return _body_part_panels.get(_active_part_name) as BodyPartPanel
|
||||
|
||||
|
||||
func _move_panel_to_focus(part_name: String) -> void:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
if panel == null or panel.get_parent() == _focus_panel_host:
|
||||
return
|
||||
var current_focus_panel: Control = _focus_panel_host.get_child(0) as Control if _focus_panel_host.get_child_count() > 0 else null
|
||||
if current_focus_panel != null:
|
||||
var current_part: String = String(current_focus_panel.get_meta("body_part_name", ""))
|
||||
if not current_part.is_empty():
|
||||
_restore_panel_to_overview(current_part)
|
||||
var old_parent: Node = panel.get_parent()
|
||||
if old_parent == null:
|
||||
return
|
||||
old_parent.remove_child(panel)
|
||||
_focus_panel_host.add_child(panel)
|
||||
panel.set_meta("body_part_name", part_name)
|
||||
panel.layout_mode = 1
|
||||
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
panel.custom_minimum_size = Vector2(FOCUS_PANEL_MIN_WIDTH, 300.0)
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
|
||||
func _restore_panel_to_overview(part_name: String) -> void:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
var origin: Dictionary = _body_part_origins.get(part_name, {})
|
||||
if panel == null or origin.is_empty():
|
||||
return
|
||||
var origin_parent: Node = origin.get("parent") as Node
|
||||
if origin_parent == null or panel.get_parent() == origin_parent:
|
||||
return
|
||||
var old_parent: Node = panel.get_parent()
|
||||
if old_parent != null:
|
||||
old_parent.remove_child(panel)
|
||||
panel.layout_mode = 2
|
||||
panel.custom_minimum_size = origin.get("minimum_size", Vector2(230.0, 160.0))
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
origin_parent.add_child(panel)
|
||||
origin_parent.move_child(panel, int(origin.get("index", origin_parent.get_child_count() - 1)))
|
||||
panel.remove_meta("body_part_name")
|
||||
|
||||
|
||||
func _overview_width_fits() -> bool:
|
||||
var required_width: float = OVERVIEW_PANEL_MIN_WIDTH * 2.0 + OVERVIEW_PREVIEW_MIN_WIDTH + WORKSPACE_GAP * 2.0
|
||||
return _available_workspace_width() >= required_width
|
||||
|
||||
|
||||
func _available_workspace_width() -> float:
|
||||
var viewport_width: float = get_viewport_rect().size.x
|
||||
return minf(size.x, viewport_width) if viewport_width > 0.0 else _workspace_row.size.x
|
||||
|
||||
|
||||
func _refresh_responsive_workspace() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
if not _view_choice_was_manual and _workspace_controls_initialized:
|
||||
var responsive_view: WorkspaceView = WorkspaceView.OVERVIEW if _overview_width_fits() else WorkspaceView.FOCUS
|
||||
if responsive_view != _workspace_view:
|
||||
request_workspace_view(responsive_view, false)
|
||||
var narrow_preview: bool = _available_workspace_width() < FOCUS_PANEL_MIN_WIDTH + OVERVIEW_PREVIEW_MIN_WIDTH + WORKSPACE_GAP
|
||||
if _workspace_view == WorkspaceView.OVERVIEW:
|
||||
_preview_takes_workspace = false
|
||||
_body_part_workspace.visible = true
|
||||
_whole_preview.visible = not narrow_preview
|
||||
_whole_stickman_button.visible = false
|
||||
_back_to_part_button.visible = false
|
||||
return
|
||||
if narrow_preview and _preview_takes_workspace:
|
||||
_body_part_workspace.visible = false
|
||||
_whole_preview.visible = true
|
||||
_whole_stickman_button.visible = false
|
||||
_back_to_part_button.visible = true
|
||||
elif narrow_preview:
|
||||
_body_part_workspace.visible = true
|
||||
_whole_preview.visible = false
|
||||
_whole_stickman_button.visible = true
|
||||
_back_to_part_button.visible = false
|
||||
else:
|
||||
_preview_takes_workspace = false
|
||||
_body_part_workspace.visible = true
|
||||
_whole_preview.visible = true
|
||||
_whole_stickman_button.visible = false
|
||||
_back_to_part_button.visible = false
|
||||
|
||||
|
||||
func _update_active_part_presentation() -> void:
|
||||
if _editor_visual_theme == null:
|
||||
return
|
||||
var surface: Color = _editor_visual_theme.chrome_color(&"surface", &"stickman")
|
||||
var border: Color = _editor_visual_theme.chrome_color(&"border", &"stickman")
|
||||
var accent: Color = _editor_visual_theme.chrome_color(&"edit", &"stickman")
|
||||
var section_size: int = _editor_visual_theme.chrome_px(&"section", &"stickman")
|
||||
for part_name: String in BODY_PART_NAMES:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(part_name) as BodyPartPanel
|
||||
if panel != null:
|
||||
panel.set_active_presentation(part_name == _active_part_name, surface, border, accent, section_size)
|
||||
if is_instance_valid(_focus_part_label):
|
||||
_focus_part_label.text = BODY_PART_DISPLAY.get(_active_part_name, _active_part_name)
|
||||
|
||||
|
||||
func _refresh_workspace_controls() -> void:
|
||||
if not _workspace_controls_initialized:
|
||||
return
|
||||
_overview_button.set_pressed_no_signal(_workspace_view == WorkspaceView.OVERVIEW)
|
||||
_focus_button.set_pressed_no_signal(_workspace_view == WorkspaceView.FOCUS)
|
||||
var part_index: int = BODY_PART_NAMES.find(_active_part_name)
|
||||
if part_index >= 0:
|
||||
_part_picker.select(part_index)
|
||||
var active: BodyPartPanel = _active_panel()
|
||||
var selected: bool = active != null and active.has_selected_shape()
|
||||
_color_button.disabled = not selected
|
||||
_color_button.tooltip_text = "Select a shape to choose its color." if not selected else "Choose this shape's color."
|
||||
_update_active_part_presentation()
|
||||
|
||||
|
||||
func _on_part_picker_selected(index: int) -> void:
|
||||
if index >= 0 and index < BODY_PART_NAMES.size():
|
||||
request_active_part(BODY_PART_NAMES[index])
|
||||
|
||||
|
||||
func _on_add_shape_button_pressed() -> void:
|
||||
var rect: Rect2 = Rect2(_add_shape_button.global_position + Vector2(0, _add_shape_button.size.y), Vector2(_add_shape_button.size.x, 1.0))
|
||||
_add_shape_menu.popup(rect)
|
||||
|
||||
|
||||
func _on_add_shape_menu_id_pressed(id: int) -> void:
|
||||
var kind: StringName = [&"line", &"rectangle", &"circle"][clampi(id, 0, 2)]
|
||||
var panel: BodyPartPanel = _active_panel()
|
||||
if panel != null:
|
||||
panel.add_shape(kind, panel.drawing_center_world())
|
||||
_refresh_workspace_controls()
|
||||
|
||||
|
||||
func _on_color_button_pressed() -> void:
|
||||
var panel: BodyPartPanel = _active_panel()
|
||||
if panel != null:
|
||||
panel.open_selected_color_picker()
|
||||
|
||||
|
||||
func _on_whole_stickman_button_pressed() -> void:
|
||||
_preview_takes_workspace = true
|
||||
_refresh_responsive_workspace()
|
||||
|
||||
|
||||
func _on_back_to_part_button_pressed() -> void:
|
||||
_preview_takes_workspace = false
|
||||
_refresh_responsive_workspace()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Body-part panel population
|
||||
@@ -230,7 +600,17 @@ func _populate_body_part_panels() -> void:
|
||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
||||
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
||||
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
||||
panel.presentation_safe.connect(_apply_pending_presentation)
|
||||
panel.set_canvas_tokens(
|
||||
_editor_visual_theme.stickman_canvas_color(&"grid_color"),
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_core"),
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||
_editor_visual_theme.chrome_color(&"text_muted", &"stickman"),
|
||||
float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")),
|
||||
_editor_visual_theme
|
||||
)
|
||||
_body_part_panels[part_name] = panel
|
||||
_body_part_origins[part_name] = {"parent": left_col, "index": left_col.get_child_count(), "minimum_size": panel.custom_minimum_size}
|
||||
left_col.add_child(panel)
|
||||
|
||||
for part_name: String in center_names:
|
||||
@@ -242,7 +622,17 @@ func _populate_body_part_panels() -> void:
|
||||
panel.shape_copy_requested.connect(_on_shape_copy_requested.bind(part_name))
|
||||
panel.shape_paste_requested.connect(_on_shape_paste_requested.bind(part_name))
|
||||
panel.color_selected.connect(_on_color_selected.bind(part_name))
|
||||
panel.presentation_safe.connect(_apply_pending_presentation)
|
||||
panel.set_canvas_tokens(
|
||||
_editor_visual_theme.stickman_canvas_color(&"grid_color"),
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_core"),
|
||||
_editor_visual_theme.stickman_canvas_color(&"selection_halo"),
|
||||
_editor_visual_theme.chrome_color(&"text_muted", &"stickman"),
|
||||
float(_editor_visual_theme.chrome_px(&"button_min_height", &"stickman")),
|
||||
_editor_visual_theme
|
||||
)
|
||||
_body_part_panels[part_name] = panel
|
||||
_body_part_origins[part_name] = {"parent": center_col, "index": center_col.get_child_count(), "minimum_size": panel.custom_minimum_size}
|
||||
center_col.add_child(panel)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -252,11 +642,121 @@ func _populate_body_part_panels() -> void:
|
||||
func _on_file_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: # Save
|
||||
_save_dialog.popup_centered_ratio(0.6)
|
||||
_save_current_document()
|
||||
1: # Load
|
||||
_request_document_action(PendingDocumentAction.LOAD)
|
||||
2: # Start Over
|
||||
_request_document_action(PendingDocumentAction.CLEAR)
|
||||
|
||||
|
||||
func _save_current_document() -> void:
|
||||
if _current_path.is_empty():
|
||||
_save_dialog.popup_centered_ratio(0.6)
|
||||
return
|
||||
save_to_path(_current_path)
|
||||
|
||||
|
||||
func save_to_path(path: String) -> bool:
|
||||
var save_path: String = ContentPathsScript.canonicalize(path)
|
||||
if save_path.get_extension().to_lower() != "stk":
|
||||
save_path = save_path.get_basename() + ".stk"
|
||||
var file: FileAccess = FileAccess.open(save_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
_show_error("Could not save the stickman. Check that the file location is available.")
|
||||
return false
|
||||
file.store_string(JSON.stringify(_build_json_data(), "\t", false))
|
||||
var write_error: Error = file.get_error()
|
||||
file.close()
|
||||
if write_error != OK:
|
||||
_show_error("Could not finish saving the stickman. Check the file location and try again.")
|
||||
return false
|
||||
_current_path = save_path
|
||||
_dirty = false
|
||||
_update_document_status()
|
||||
return true
|
||||
|
||||
|
||||
func _request_document_action(action: PendingDocumentAction) -> void:
|
||||
if _dirty:
|
||||
_pending_document_action = action
|
||||
match action:
|
||||
PendingDocumentAction.LOAD:
|
||||
_unsaved_dialog.dialog_text = "Save changes before loading another stickman?"
|
||||
PendingDocumentAction.CLEAR:
|
||||
_unsaved_dialog.dialog_text = "Save changes before starting over with a blank stickman?"
|
||||
PendingDocumentAction.QUIT:
|
||||
_unsaved_dialog.dialog_text = "Save changes before closing Stickman Studio?"
|
||||
_unsaved_dialog.popup_centered()
|
||||
return
|
||||
_continue_document_action(action)
|
||||
|
||||
|
||||
func _continue_document_action(action: PendingDocumentAction) -> void:
|
||||
match action:
|
||||
PendingDocumentAction.LOAD:
|
||||
_load_dialog.popup_centered_ratio(0.6)
|
||||
2: # Clear
|
||||
PendingDocumentAction.CLEAR:
|
||||
_clear_confirm.popup_centered()
|
||||
PendingDocumentAction.QUIT:
|
||||
get_tree().quit()
|
||||
_pending_document_action = PendingDocumentAction.NONE
|
||||
|
||||
|
||||
func _on_unsaved_save_pressed() -> void:
|
||||
if _pending_document_action == PendingDocumentAction.NONE:
|
||||
return
|
||||
if _current_path.is_empty():
|
||||
_save_dialog.popup_centered_ratio(0.6)
|
||||
return
|
||||
if save_to_path(_current_path):
|
||||
_resume_pending_document_action()
|
||||
else:
|
||||
_cancel_pending_document_action()
|
||||
|
||||
|
||||
func _on_unsaved_custom_action(action: StringName) -> void:
|
||||
if action != &"discard":
|
||||
return
|
||||
_resume_pending_document_action()
|
||||
|
||||
|
||||
func _resume_pending_document_action() -> void:
|
||||
var action: PendingDocumentAction = _pending_document_action
|
||||
_pending_document_action = PendingDocumentAction.NONE
|
||||
if action == PendingDocumentAction.CLEAR:
|
||||
_clear_all()
|
||||
return
|
||||
_continue_document_action(action)
|
||||
|
||||
|
||||
func _cancel_pending_document_action() -> void:
|
||||
_pending_document_action = PendingDocumentAction.NONE
|
||||
|
||||
|
||||
func _on_stickman_name_changed(_value: String) -> void:
|
||||
if not _applying_document:
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
func _mark_dirty() -> void:
|
||||
if _applying_document:
|
||||
return
|
||||
_dirty = true
|
||||
_update_document_status()
|
||||
|
||||
|
||||
func _update_document_status() -> void:
|
||||
if _status_save_status == null:
|
||||
return
|
||||
if _dirty:
|
||||
_status_save_status.text = "Unsaved changes"
|
||||
_status_save_status.add_theme_color_override("font_color", _editor_visual_theme.chrome_color(&"dirty", &"stickman"))
|
||||
elif _current_path.is_empty():
|
||||
_status_save_status.text = "Not saved yet"
|
||||
_status_save_status.add_theme_color_override("font_color", _editor_visual_theme.chrome_color(&"text_muted", &"stickman"))
|
||||
else:
|
||||
_status_save_status.text = "Saved"
|
||||
_status_save_status.add_theme_color_override("font_color", _editor_visual_theme.chrome_color(&"saved", &"stickman"))
|
||||
|
||||
|
||||
func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
@@ -320,19 +820,12 @@ func _on_grid_config_confirmed() -> void:
|
||||
|
||||
func _on_save_file_selected(path: String) -> void:
|
||||
path = ContentPathsScript.canonicalize(path)
|
||||
if not path.ends_with(".stk"):
|
||||
path += ".stk"
|
||||
|
||||
var data := _build_json_data()
|
||||
var json_text := JSON.stringify(data, "\t", false)
|
||||
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
_show_error("Failed to save file: " + str(FileAccess.get_open_error()))
|
||||
if path.get_extension().to_lower() != "stk":
|
||||
path = path.get_basename() + ".stk"
|
||||
if not save_to_path(path):
|
||||
_cancel_pending_document_action()
|
||||
return
|
||||
|
||||
file.store_string(json_text)
|
||||
file.close()
|
||||
_resume_pending_document_action()
|
||||
|
||||
|
||||
func _on_load_file_selected(path: String) -> void:
|
||||
@@ -358,6 +851,9 @@ func _on_load_file_selected(path: String) -> void:
|
||||
if not err.is_empty():
|
||||
_show_error(err)
|
||||
return
|
||||
_current_path = path
|
||||
_dirty = false
|
||||
_update_document_status()
|
||||
|
||||
|
||||
func _on_clear_confirmed() -> void:
|
||||
@@ -379,19 +875,31 @@ func _on_body_part_shape_changed(_new_data: Array, part_name: String) -> void:
|
||||
_whole_preview.set_part_rotation(part_name, 0.0)
|
||||
_whole_preview.set_part_scale(part_name, Vector2(1.0, 1.0))
|
||||
_broadcast_clipboard_state()
|
||||
_mark_dirty()
|
||||
_refresh_workspace_controls()
|
||||
|
||||
|
||||
func _on_body_part_shape_selected(part_name: String) -> void:
|
||||
request_active_part(part_name)
|
||||
for other_name: String in BODY_PART_NAMES:
|
||||
if other_name != part_name:
|
||||
var panel: BodyPartPanel = _body_part_panels.get(other_name) as BodyPartPanel
|
||||
if panel:
|
||||
panel.deselect()
|
||||
_refresh_workspace_controls()
|
||||
|
||||
|
||||
func _on_preview_part_moved(_part_name: String, _new_position: Vector2) -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _on_preview_part_transform_committed(_part_name: String, _position: Vector2, _rotation_deg: float, _scale: Vector2) -> void:
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
func _on_preview_part_order_changed(_part_name: String, _new_order: Array[String]) -> void:
|
||||
_mark_dirty()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data collection / JSON helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -478,13 +986,13 @@ func _apply_json_data(json: Variant) -> String:
|
||||
if version not in SUPPORTED_VERSIONS:
|
||||
return "Unsupported file version: '%s' (expected '1.0', '1.1', '1.2', '1.3', '1.4', or '1.5')" % version
|
||||
|
||||
_stickman_name_edit.text = dict.get("stickman_name", "")
|
||||
|
||||
var body_parts: Variant = dict.get("body_parts")
|
||||
if not body_parts is Dictionary:
|
||||
return "Invalid file format: 'body_parts' must be a dictionary."
|
||||
|
||||
var body_dict: Dictionary = body_parts as Dictionary
|
||||
_applying_document = true
|
||||
_stickman_name_edit.text = str(dict.get("stickman_name", ""))
|
||||
|
||||
_clear_all_panels()
|
||||
|
||||
@@ -546,6 +1054,9 @@ func _apply_json_data(json: Variant) -> String:
|
||||
_whole_preview.reset_view()
|
||||
_whole_preview.set_all_part_positions(positions)
|
||||
_whole_preview.set_body_parts(_collect_all_shape_data())
|
||||
_applying_document = false
|
||||
_dirty = false
|
||||
_update_document_status()
|
||||
|
||||
return ""
|
||||
|
||||
@@ -561,12 +1072,18 @@ func _clear_all_panels() -> void:
|
||||
|
||||
|
||||
func _clear_all() -> void:
|
||||
_applying_document = true
|
||||
_clear_all_panels()
|
||||
_whole_preview.clear_all()
|
||||
_whole_preview.set_all_part_positions(DEFAULT_POSITIONS.duplicate())
|
||||
_stickman_name_edit.text = ""
|
||||
_shape_clipboard.clear()
|
||||
_broadcast_clipboard_state()
|
||||
_applying_document = false
|
||||
_current_path = ""
|
||||
_dirty = false
|
||||
_update_document_status()
|
||||
_refresh_workspace_controls()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: Shape clipboard
|
||||
@@ -611,6 +1128,7 @@ func _on_color_selected(color: Color, _part_name: String) -> void:
|
||||
_recent_colors.pop_back()
|
||||
_save_settings()
|
||||
_broadcast_recent_colors()
|
||||
_mark_dirty()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 3: Settings persistence
|
||||
|
||||
@@ -18,6 +18,8 @@ class_name WholeStickmanPreview
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
signal part_moved(part_name: String, new_position: Vector2)
|
||||
signal part_transform_committed(part_name: String, position: Vector2, rotation_deg: float, scale: Vector2)
|
||||
signal part_order_changed(part_name: String, new_order: Array[String])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
@@ -147,6 +149,14 @@ var _selected_gizmo_bounds: Rect2 = Rect2()
|
||||
|
||||
# Phase 7: pose silhouette guide (default ON per user decision)
|
||||
var _show_guide: bool = true
|
||||
var _grid_color: Color = GRID_COLOR
|
||||
var _selection_core_color: Color = Color.WHITE
|
||||
var _selection_halo_color: Color = Color(0.0, 0.0, 0.0, 0.85)
|
||||
var _preview_color: Color = Color(0.13, 0.78, 1.0, 0.4)
|
||||
var _interaction_start_part: String = ""
|
||||
var _interaction_start_position: Vector2 = Vector2.ZERO
|
||||
var _interaction_start_rotation: float = 0.0
|
||||
var _interaction_start_scale: Vector2 = Vector2.ONE
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
@@ -271,6 +281,14 @@ func set_show_guide(enabled: bool) -> void:
|
||||
preview_area.queue_redraw()
|
||||
|
||||
|
||||
func set_canvas_tokens(grid_color: Color, selection_core: Color, selection_halo: Color, preview_color: Color = Color(0.13, 0.78, 1.0, 0.4)) -> void:
|
||||
_grid_color = grid_color
|
||||
_selection_core_color = selection_core
|
||||
_selection_halo_color = selection_halo
|
||||
_preview_color = preview_color
|
||||
preview_area.queue_redraw()
|
||||
|
||||
|
||||
## Returns the preview-space position of a guide joint (a master-space
|
||||
## GUIDE_JOINTS entry mapped through _guide_to_preview). Guarded against
|
||||
## unknown joint names.
|
||||
@@ -399,25 +417,35 @@ func _on_preview_draw() -> void:
|
||||
if not _dragging_part.is_empty() and _interaction == Interaction.TRANSLATE:
|
||||
var bounds: Variant = _part_bounds.get(_dragging_part)
|
||||
if bounds is Rect2:
|
||||
preview_area.draw_rect(bounds as Rect2, Color(1.0, 0.8, 0.0, 0.35), false, 1.5)
|
||||
preview_area.draw_rect(bounds as Rect2, _preview_color, false, 1.5 / _zoom)
|
||||
|
||||
# Draw selection gizmos on top of all parts
|
||||
if _selected_gizmo_bounds.has_area():
|
||||
var b: Rect2 = _selected_gizmo_bounds
|
||||
preview_area.draw_rect(b, Color.WHITE, false, 1.5 / _zoom)
|
||||
preview_area.draw_rect(b, _selection_halo_color, false, 4.0 / _zoom)
|
||||
preview_area.draw_rect(b, _selection_core_color, false, 1.5 / _zoom)
|
||||
var rot_center := Vector2(b.position.x + b.size.x * 0.5, b.end.y + ROTATION_CIRCLE_OFFSET)
|
||||
preview_area.draw_circle(rot_center, ROTATION_CIRCLE_RADIUS / _zoom, Color.WHITE)
|
||||
preview_area.draw_circle(rot_center, (ROTATION_CIRCLE_RADIUS + 2.0) / _zoom, _selection_halo_color)
|
||||
preview_area.draw_circle(rot_center, ROTATION_CIRCLE_RADIUS / _zoom, _selection_core_color)
|
||||
var cross_half := SCALE_CROSS_SIZE / _zoom
|
||||
for i: int in range(4):
|
||||
var corner := _get_corner_position(b, i)
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(-cross_half, 0),
|
||||
corner + Vector2(cross_half, 0),
|
||||
Color.WHITE, 1.5 / _zoom)
|
||||
_selection_halo_color, 4.0 / _zoom)
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(0, -cross_half),
|
||||
corner + Vector2(0, cross_half),
|
||||
Color.WHITE, 1.5 / _zoom)
|
||||
_selection_halo_color, 4.0 / _zoom)
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(-cross_half, 0),
|
||||
corner + Vector2(cross_half, 0),
|
||||
_selection_core_color, 1.5 / _zoom)
|
||||
preview_area.draw_line(
|
||||
corner + Vector2(0, -cross_half),
|
||||
corner + Vector2(0, cross_half),
|
||||
_selection_core_color, 1.5 / _zoom)
|
||||
|
||||
|
||||
func _draw_grid() -> void:
|
||||
@@ -436,12 +464,12 @@ func _draw_grid() -> void:
|
||||
|
||||
var x: float = start_x
|
||||
while x <= world_end.x:
|
||||
preview_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), GRID_COLOR, line_width)
|
||||
preview_area.draw_line(Vector2(x, world_origin.y), Vector2(x, world_end.y), _grid_color, line_width)
|
||||
x += gs
|
||||
|
||||
var y: float = start_y
|
||||
while y <= world_end.y:
|
||||
preview_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), GRID_COLOR, line_width)
|
||||
preview_area.draw_line(Vector2(world_origin.x, y), Vector2(world_end.x, y), _grid_color, line_width)
|
||||
y += gs
|
||||
|
||||
|
||||
@@ -688,6 +716,7 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
||||
var rot_center := Vector2(bounds.position.x + bounds.size.x * 0.5, bounds.end.y + ROTATION_CIRCLE_OFFSET)
|
||||
if world_pos.distance_to(rot_center) <= ROTATION_CIRCLE_HIT_RADIUS / _zoom:
|
||||
_interaction = Interaction.ROTATE
|
||||
_capture_transform_start(_selected_part)
|
||||
_rotate_start_rotation = _part_rotations[_selected_part]
|
||||
_rotate_total_delta = 0.0
|
||||
var bounds_center := bounds.position + bounds.size * 0.5
|
||||
@@ -700,6 +729,7 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
||||
var corner := _get_corner_position(bounds, i)
|
||||
if world_pos.distance_to(corner) <= SCALE_CROSS_HIT_RADIUS / _zoom:
|
||||
_interaction = Interaction.SCALE
|
||||
_capture_transform_start(_selected_part)
|
||||
_scale_corner = i
|
||||
_scale_anchor = _get_opposite_corner(bounds, i)
|
||||
_scale_original_size = _compute_original_bounds_size(_selected_part)
|
||||
@@ -717,6 +747,7 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
||||
if rect.grow(8.0).has_point(world_pos):
|
||||
_selected_part = part_name
|
||||
_interaction = Interaction.TRANSLATE
|
||||
_capture_transform_start(_selected_part)
|
||||
_dragging_part = part_name
|
||||
var pos: Vector2 = _part_positions.get(part_name, Vector2.ZERO)
|
||||
_drag_offset = world_pos - pos
|
||||
@@ -731,13 +762,40 @@ func _try_start_interaction(world_pos: Vector2) -> void:
|
||||
|
||||
func _end_interaction() -> void:
|
||||
if _interaction != Interaction.NONE:
|
||||
var changed_part: String = _interaction_start_part
|
||||
var changed: bool = changed_part != "" and (
|
||||
_part_positions.get(changed_part, Vector2.ZERO) != _interaction_start_position
|
||||
or not is_equal_approx(float(_part_rotations.get(changed_part, 0.0)), _interaction_start_rotation)
|
||||
or Vector2(_part_scales.get(changed_part, Vector2.ONE)) != _interaction_start_scale
|
||||
)
|
||||
if _interaction == Interaction.TRANSLATE and not _dragging_part.is_empty():
|
||||
_dragging_part = ""
|
||||
_drag_offset = Vector2.ZERO
|
||||
_interaction = Interaction.NONE
|
||||
if changed:
|
||||
_emit_transform_committed(changed_part)
|
||||
_interaction_start_part = ""
|
||||
preview_area.queue_redraw()
|
||||
|
||||
|
||||
func _capture_transform_start(part_name: String) -> void:
|
||||
_interaction_start_part = part_name
|
||||
_interaction_start_position = _part_positions.get(part_name, Vector2.ZERO)
|
||||
_interaction_start_rotation = float(_part_rotations.get(part_name, 0.0))
|
||||
_interaction_start_scale = _part_scales.get(part_name, Vector2.ONE)
|
||||
|
||||
|
||||
func _emit_transform_committed(part_name: String) -> void:
|
||||
if part_name.is_empty():
|
||||
return
|
||||
part_transform_committed.emit(
|
||||
part_name,
|
||||
_part_positions.get(part_name, Vector2.ZERO),
|
||||
float(_part_rotations.get(part_name, 0.0)),
|
||||
_part_scales.get(part_name, Vector2.ONE)
|
||||
)
|
||||
|
||||
|
||||
func _handle_translate_drag(mm: InputEventMouseMotion) -> void:
|
||||
if _dragging_part.is_empty():
|
||||
return
|
||||
@@ -996,6 +1054,7 @@ func _send_back_part() -> void:
|
||||
_part_order.remove_at(idx)
|
||||
_part_order.insert(idx - 1, _selected_part)
|
||||
preview_area.queue_redraw()
|
||||
part_order_changed.emit(_selected_part, _part_order.duplicate())
|
||||
|
||||
|
||||
func _bring_forward_part() -> void:
|
||||
@@ -1007,6 +1066,7 @@ func _bring_forward_part() -> void:
|
||||
_part_order.remove_at(idx)
|
||||
_part_order.insert(idx + 1, _selected_part)
|
||||
preview_area.queue_redraw()
|
||||
part_order_changed.emit(_selected_part, _part_order.duplicate())
|
||||
|
||||
|
||||
func _mirror_part_x() -> void:
|
||||
@@ -1016,6 +1076,7 @@ func _mirror_part_x() -> void:
|
||||
_part_scales[_selected_part] = Vector2(-scl.x, scl.y)
|
||||
_rebuild_bounds()
|
||||
preview_area.queue_redraw()
|
||||
_emit_transform_committed(_selected_part)
|
||||
|
||||
|
||||
func _mirror_part_y() -> void:
|
||||
@@ -1025,3 +1086,4 @@ func _mirror_part_y() -> void:
|
||||
_part_scales[_selected_part] = Vector2(scl.x, -scl.y)
|
||||
_rebuild_bounds()
|
||||
preview_area.queue_redraw()
|
||||
_emit_transform_committed(_selected_part)
|
||||
|
||||
Reference in New Issue
Block a user