2026-08-02 00:45:07 -04:00
|
|
|
|
class_name SurfaceDrawingToolbar
|
2026-08-02 09:20:44 -04:00
|
|
|
|
extends Control
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
2026-08-15 20:38:58 -04:00
|
|
|
|
const ControllerVirtualCursorType = preload(
|
|
|
|
|
|
"res://ui/controller_virtual_cursor.gd"
|
|
|
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
const LockedContentPresentationType = preload(
|
|
|
|
|
|
"res://ui/components/locked_content_presentation.gd"
|
|
|
|
|
|
)
|
2026-08-13 15:24:23 -04:00
|
|
|
|
const MARKER_MODE_ICON: Texture2D = preload(
|
|
|
|
|
|
"res://items/icons/art/art_kit_marker.png"
|
|
|
|
|
|
)
|
2026-08-13 16:25:49 -04:00
|
|
|
|
const MARKER_MODE_SHADER: Shader = preload(
|
|
|
|
|
|
"res://ui/art_kit_marker_icon.gdshader"
|
|
|
|
|
|
)
|
2026-08-13 15:24:23 -04:00
|
|
|
|
const GRID_MODE_ICON: Texture2D = preload(
|
|
|
|
|
|
"res://items/icons/art/art_kit_grid_light.png"
|
|
|
|
|
|
)
|
2026-08-15 14:28:13 -04:00
|
|
|
|
const BRUSH_SIZE_ICONS: Dictionary[int, Texture2D] = {
|
|
|
|
|
|
1: preload("res://items/icons/art/art_kit_marker_tip_fine.png"),
|
|
|
|
|
|
2: preload("res://items/icons/art/art_kit_marker_tip_thin.png"),
|
|
|
|
|
|
3: preload("res://items/icons/art/art_kit_marker_tip_mid.png"),
|
|
|
|
|
|
4: preload("res://items/icons/art/art_kit_marker_tip_thick.png"),
|
|
|
|
|
|
}
|
|
|
|
|
|
const GRID_SIZE_ICONS: Dictionary[int, Texture2D] = {
|
|
|
|
|
|
16: preload("res://items/icons/art/art_kit_grid_small_light.png"),
|
|
|
|
|
|
32: preload("res://items/icons/art/art_kit_grid_medium_light.png"),
|
|
|
|
|
|
64: preload("res://items/icons/art/art_kit_grid_large_light.png"),
|
|
|
|
|
|
128: preload("res://items/icons/art/art_kit_grid_xl_light.png"),
|
|
|
|
|
|
}
|
2026-08-21 21:01:21 -04:00
|
|
|
|
const TOOLBAR_WIDTH: float = 634.0
|
2026-08-02 09:20:44 -04:00
|
|
|
|
const TOOLBAR_HEIGHT: float = 373.0
|
|
|
|
|
|
const COLOR_RAIL_WIDTH: float = 46.0
|
2026-08-19 10:02:28 -04:00
|
|
|
|
const POPUP_LOCK_ICON_SIZE: int = 24
|
|
|
|
|
|
const POPUP_LOCK_CANVAS_SIZE: int = 40
|
|
|
|
|
|
const POPUP_LOCK_ICON_ALPHA: float = 0.72
|
|
|
|
|
|
const COLOR_LOCK_ICON_ALPHA: float = 0.34
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
@onready var _mode_button: Button = %ModeButton
|
|
|
|
|
|
@onready var _brush_option: OptionButton = %BrushOption
|
|
|
|
|
|
@onready var _grid_option: OptionButton = %GridOption
|
|
|
|
|
|
@onready var _color_list: VBoxContainer = %ColorList
|
2026-08-02 09:20:44 -04:00
|
|
|
|
@onready var _top_panel: PanelContainer = %TopPanel
|
|
|
|
|
|
@onready var _color_panel: PanelContainer = %ColorPanel
|
|
|
|
|
|
@onready var _eraser_button: Button = %EraserButton
|
|
|
|
|
|
@onready var _undo_button: Button = %UndoButton
|
|
|
|
|
|
@onready var _hide_guide_button: Button = %HideGuideButton
|
|
|
|
|
|
@onready var _restore_guide_button: Button = %RestoreGuideButton
|
|
|
|
|
|
@onready var _finalize_guide_button: Button = %FinalizeGuideButton
|
2026-08-21 21:01:21 -04:00
|
|
|
|
@onready var _export_button: Button = %ExportButton
|
|
|
|
|
|
@onready var _stamp_button: Button = %StampButton
|
|
|
|
|
|
@onready var _stamp_library_panel: PanelContainer = %StampLibraryPanel
|
|
|
|
|
|
@onready var _stamp_library_close: Button = %StampLibraryClose
|
|
|
|
|
|
@onready var _empty_stamp_library: Label = %EmptyStampLibrary
|
|
|
|
|
|
@onready var _stamp_scroll: ScrollContainer = %StampScroll
|
|
|
|
|
|
@onready var _stamp_grid: GridContainer = %StampGrid
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
var _service: NetworkSurfaceDrawingService
|
|
|
|
|
|
var _unlocks: PlayerArtUnlocks
|
|
|
|
|
|
var _color_buttons: Dictionary[StringName, Button] = {}
|
2026-08-02 09:20:44 -04:00
|
|
|
|
var _dock_right: bool = true
|
2026-08-13 16:25:49 -04:00
|
|
|
|
var _marker_mode_material: ShaderMaterial
|
2026-08-15 20:38:58 -04:00
|
|
|
|
var _brush_popup_cursor: ControllerVirtualCursorType
|
|
|
|
|
|
var _grid_popup_cursor: ControllerVirtualCursorType
|
|
|
|
|
|
var _applied_marker_icon_color: Color = Color(-1.0, -1.0, -1.0, -1.0)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
var _brush_option_icons: Dictionary[int, Texture2D] = {}
|
|
|
|
|
|
var _popup_lock_icon: Texture2D
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
|
UtilityPageStyleType.apply_page(self)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
_apply_toolbar_panel(_top_panel)
|
|
|
|
|
|
_apply_toolbar_panel(_color_panel)
|
2026-08-21 21:01:21 -04:00
|
|
|
|
_apply_toolbar_panel(_stamp_library_panel)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
UtilityPageStyleType.apply_ocean_button(_mode_button)
|
|
|
|
|
|
UtilityPageStyleType.apply_ocean_button(_brush_option)
|
|
|
|
|
|
UtilityPageStyleType.apply_ocean_button(_grid_option)
|
2026-08-15 14:28:13 -04:00
|
|
|
|
for size_option: OptionButton in [_brush_option, _grid_option]:
|
|
|
|
|
|
size_option.expand_icon = true
|
|
|
|
|
|
size_option.add_theme_constant_override("icon_max_width", 40)
|
|
|
|
|
|
size_option.get_popup().add_theme_constant_override(
|
|
|
|
|
|
"icon_max_width", 40
|
|
|
|
|
|
)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
for button: Button in _action_buttons():
|
|
|
|
|
|
UtilityPageStyleType.apply_ocean_button(button)
|
|
|
|
|
|
button.custom_minimum_size = Vector2(48, 44)
|
|
|
|
|
|
_make_button_round(button, 22)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_mode_button.custom_minimum_size = Vector2(48, 48)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
_make_button_round(_mode_button, 24)
|
2026-08-13 15:24:23 -04:00
|
|
|
|
_enlarge_action_icon(_mode_button)
|
2026-08-13 16:25:49 -04:00
|
|
|
|
_marker_mode_material = ShaderMaterial.new()
|
|
|
|
|
|
_marker_mode_material.shader = MARKER_MODE_SHADER
|
|
|
|
|
|
_marker_mode_material.set_shader_parameter(
|
|
|
|
|
|
"marker_color",
|
|
|
|
|
|
SurfaceDrawingPalette.get_color(SurfaceDrawingPalette.DEFAULT_COLOR_ID),
|
|
|
|
|
|
)
|
|
|
|
|
|
_mode_button.material = _marker_mode_material
|
2026-08-13 15:24:23 -04:00
|
|
|
|
for button: Button in _action_buttons():
|
|
|
|
|
|
_enlarge_action_icon(button)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_mode_button.pressed.connect(_toggle_mode)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
_eraser_button.pressed.connect(_toggle_eraser)
|
|
|
|
|
|
_undo_button.pressed.connect(_undo_last_stroke)
|
|
|
|
|
|
_hide_guide_button.pressed.connect(
|
|
|
|
|
|
_arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.HIDE)
|
|
|
|
|
|
)
|
|
|
|
|
|
_restore_guide_button.pressed.connect(
|
|
|
|
|
|
_arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.RESTORE)
|
|
|
|
|
|
)
|
|
|
|
|
|
_finalize_guide_button.pressed.connect(
|
|
|
|
|
|
_arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.FINALIZE)
|
|
|
|
|
|
)
|
2026-08-21 21:01:21 -04:00
|
|
|
|
_export_button.pressed.connect(_export_aimed_artwork)
|
|
|
|
|
|
_stamp_button.pressed.connect(_toggle_stamp_library)
|
|
|
|
|
|
_stamp_library_close.pressed.connect(_close_stamp_library)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_brush_option.item_selected.connect(_select_brush)
|
|
|
|
|
|
_grid_option.item_selected.connect(_select_grid)
|
2026-08-15 20:38:58 -04:00
|
|
|
|
_configure_pointer_only_controls()
|
|
|
|
|
|
_brush_popup_cursor = _add_popup_cursor(_brush_option.get_popup())
|
|
|
|
|
|
_grid_popup_cursor = _add_popup_cursor(_grid_option.get_popup())
|
2026-08-19 10:02:28 -04:00
|
|
|
|
_popup_lock_icon = LockedContentPresentationType.make_icon_texture(
|
|
|
|
|
|
POPUP_LOCK_ICON_SIZE,
|
|
|
|
|
|
POPUP_LOCK_CANVAS_SIZE,
|
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_build_options()
|
2026-08-15 20:38:58 -04:00
|
|
|
|
_apply_marker_color_to_brush_icons(
|
|
|
|
|
|
SurfaceDrawingPalette.get_color(SurfaceDrawingPalette.DEFAULT_COLOR_ID)
|
|
|
|
|
|
)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
_apply_dock_side()
|
2026-08-02 00:45:07 -04:00
|
|
|
|
hide()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 09:20:44 -04:00
|
|
|
|
func _apply_toolbar_panel(panel: PanelContainer) -> void:
|
|
|
|
|
|
var style: StyleBoxFlat = UtilityPageStyleType.panel_style()
|
|
|
|
|
|
style.content_margin_left = 6.0
|
|
|
|
|
|
style.content_margin_top = 6.0
|
|
|
|
|
|
style.content_margin_right = 6.0
|
|
|
|
|
|
style.content_margin_bottom = 6.0
|
|
|
|
|
|
style.set_corner_radius_all(10)
|
|
|
|
|
|
panel.add_theme_stylebox_override("panel", style)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _action_buttons() -> Array[Button]:
|
|
|
|
|
|
return [
|
|
|
|
|
|
_eraser_button,
|
|
|
|
|
|
_undo_button,
|
|
|
|
|
|
_hide_guide_button,
|
|
|
|
|
|
_restore_guide_button,
|
|
|
|
|
|
_finalize_guide_button,
|
2026-08-21 21:01:21 -04:00
|
|
|
|
_export_button,
|
|
|
|
|
|
_stamp_button,
|
2026-08-02 09:20:44 -04:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _make_button_round(button: Button, radius: int) -> void:
|
2026-08-02 00:45:07 -04:00
|
|
|
|
for style_name: StringName in [
|
|
|
|
|
|
&"normal", &"hover", &"pressed", &"focus", &"disabled",
|
|
|
|
|
|
]:
|
2026-08-02 09:20:44 -04:00
|
|
|
|
var existing: StyleBox = button.get_theme_stylebox(style_name)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
var style := existing.duplicate() as StyleBoxFlat
|
|
|
|
|
|
if style == null:
|
|
|
|
|
|
continue
|
2026-08-02 09:20:44 -04:00
|
|
|
|
style.set_corner_radius_all(radius)
|
|
|
|
|
|
button.add_theme_stylebox_override(style_name, style)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 14:53:39 -04:00
|
|
|
|
func _enlarge_action_icon(button: Button) -> void:
|
|
|
|
|
|
# The shared ocean button uses generous text padding, leaving only a
|
|
|
|
|
|
# 20-pixel icon area in these compact bubbles. Keep the 48x44 control and
|
|
|
|
|
|
# hitbox intact while giving icon-only actions a full 40x40 presentation.
|
|
|
|
|
|
button.add_theme_constant_override("icon_max_width", 40)
|
|
|
|
|
|
for style_name: StringName in [
|
|
|
|
|
|
&"normal", &"hover", &"pressed", &"focus", &"disabled",
|
|
|
|
|
|
]:
|
|
|
|
|
|
var existing: StyleBox = button.get_theme_stylebox(style_name)
|
|
|
|
|
|
var style := existing.duplicate() as StyleBoxFlat
|
|
|
|
|
|
if style == null:
|
|
|
|
|
|
continue
|
|
|
|
|
|
style.content_margin_left = 4.0
|
|
|
|
|
|
style.content_margin_right = 4.0
|
|
|
|
|
|
style.content_margin_top = 2.0
|
|
|
|
|
|
style.content_margin_bottom = 2.0
|
|
|
|
|
|
button.add_theme_stylebox_override(style_name, style)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func setup(
|
|
|
|
|
|
service: NetworkSurfaceDrawingService,
|
|
|
|
|
|
unlocks: PlayerArtUnlocks,
|
|
|
|
|
|
) -> void:
|
|
|
|
|
|
_service = service
|
|
|
|
|
|
_unlocks = unlocks
|
|
|
|
|
|
if _service != null and not _service.hud_state_changed.is_connected(
|
|
|
|
|
|
_on_service_state_changed
|
|
|
|
|
|
):
|
|
|
|
|
|
_service.hud_state_changed.connect(_on_service_state_changed)
|
|
|
|
|
|
if _unlocks != null and not _unlocks.unlocks_changed.is_connected(
|
|
|
|
|
|
_on_unlocks_changed
|
|
|
|
|
|
):
|
|
|
|
|
|
_unlocks.unlocks_changed.connect(_on_unlocks_changed)
|
|
|
|
|
|
_refresh_unlocks()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 09:20:44 -04:00
|
|
|
|
func set_dock_right(should_dock_right: bool) -> void:
|
|
|
|
|
|
_dock_right = should_dock_right
|
|
|
|
|
|
if is_node_ready():
|
|
|
|
|
|
_apply_dock_side()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func is_docked_right() -> bool:
|
|
|
|
|
|
return _dock_right
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _apply_dock_side() -> void:
|
|
|
|
|
|
anchor_left = 1.0 if _dock_right else 0.0
|
|
|
|
|
|
anchor_right = anchor_left
|
|
|
|
|
|
offset_left = -TOOLBAR_WIDTH if _dock_right else 0.0
|
|
|
|
|
|
offset_right = 0.0 if _dock_right else TOOLBAR_WIDTH
|
|
|
|
|
|
offset_top = 0.0
|
|
|
|
|
|
offset_bottom = TOOLBAR_HEIGHT
|
2026-08-13 15:24:23 -04:00
|
|
|
|
_top_panel.offset_left = 0.0
|
|
|
|
|
|
_top_panel.offset_right = TOOLBAR_WIDTH
|
2026-08-02 09:20:44 -04:00
|
|
|
|
_color_panel.offset_left = (
|
|
|
|
|
|
TOOLBAR_WIDTH - COLOR_RAIL_WIDTH if _dock_right else 0.0
|
|
|
|
|
|
)
|
|
|
|
|
|
_color_panel.offset_right = (
|
|
|
|
|
|
TOOLBAR_WIDTH if _dock_right else COLOR_RAIL_WIDTH
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func owns_pointer_event(event: InputEvent) -> bool:
|
|
|
|
|
|
if not visible:
|
|
|
|
|
|
return false
|
|
|
|
|
|
if _brush_option.get_popup().visible or _grid_option.get_popup().visible:
|
|
|
|
|
|
return true
|
|
|
|
|
|
if event is InputEventMouse:
|
2026-08-02 09:20:44 -04:00
|
|
|
|
var pointer_position: Vector2 = (event as InputEventMouse).position
|
|
|
|
|
|
return (
|
|
|
|
|
|
_top_panel.get_global_rect().has_point(pointer_position)
|
|
|
|
|
|
or _color_panel.get_global_rect().has_point(pointer_position)
|
2026-08-21 21:01:21 -04:00
|
|
|
|
or (
|
|
|
|
|
|
_stamp_library_panel.visible
|
|
|
|
|
|
and _stamp_library_panel.get_global_rect().has_point(
|
|
|
|
|
|
pointer_position
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 20:38:58 -04:00
|
|
|
|
func update_virtual_pointer_overlay(is_pointer_visible: bool) -> bool:
|
|
|
|
|
|
var active_popup: PopupMenu
|
|
|
|
|
|
var active_cursor: ControllerVirtualCursorType
|
|
|
|
|
|
if _brush_option.get_popup().visible:
|
|
|
|
|
|
active_popup = _brush_option.get_popup()
|
|
|
|
|
|
active_cursor = _brush_popup_cursor
|
|
|
|
|
|
elif _grid_option.get_popup().visible:
|
|
|
|
|
|
active_popup = _grid_option.get_popup()
|
|
|
|
|
|
active_cursor = _grid_popup_cursor
|
|
|
|
|
|
for popup_cursor: ControllerVirtualCursorType in [
|
|
|
|
|
|
_brush_popup_cursor, _grid_popup_cursor,
|
|
|
|
|
|
]:
|
|
|
|
|
|
if popup_cursor != null:
|
|
|
|
|
|
popup_cursor.visible = (
|
|
|
|
|
|
is_pointer_visible and popup_cursor == active_cursor
|
|
|
|
|
|
)
|
|
|
|
|
|
if not is_pointer_visible or active_popup == null or active_cursor == null:
|
|
|
|
|
|
return false
|
|
|
|
|
|
active_cursor.set_pointer_position(active_popup.get_mouse_position())
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func hide_virtual_pointer_overlay() -> void:
|
|
|
|
|
|
update_virtual_pointer_overlay(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _configure_pointer_only_controls() -> void:
|
|
|
|
|
|
for control: Control in [
|
|
|
|
|
|
_mode_button,
|
|
|
|
|
|
_brush_option,
|
|
|
|
|
|
_grid_option,
|
|
|
|
|
|
_eraser_button,
|
|
|
|
|
|
_undo_button,
|
|
|
|
|
|
_hide_guide_button,
|
|
|
|
|
|
_restore_guide_button,
|
|
|
|
|
|
_finalize_guide_button,
|
2026-08-21 21:01:21 -04:00
|
|
|
|
_export_button,
|
|
|
|
|
|
_stamp_button,
|
|
|
|
|
|
_stamp_library_close,
|
2026-08-15 20:38:58 -04:00
|
|
|
|
]:
|
|
|
|
|
|
control.focus_mode = Control.FOCUS_NONE
|
|
|
|
|
|
control.focus_neighbor_left = NodePath()
|
|
|
|
|
|
control.focus_neighbor_top = NodePath()
|
|
|
|
|
|
control.focus_neighbor_right = NodePath()
|
|
|
|
|
|
control.focus_neighbor_bottom = NodePath()
|
|
|
|
|
|
control.focus_next = NodePath()
|
|
|
|
|
|
control.focus_previous = NodePath()
|
|
|
|
|
|
for popup: PopupMenu in [
|
|
|
|
|
|
_brush_option.get_popup(), _grid_option.get_popup(),
|
|
|
|
|
|
]:
|
|
|
|
|
|
popup.unfocusable = true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _add_popup_cursor(popup: PopupMenu) -> ControllerVirtualCursorType:
|
|
|
|
|
|
var cursor := ControllerVirtualCursorType.new()
|
|
|
|
|
|
cursor.z_index = RenderingServer.CANVAS_ITEM_Z_MAX
|
|
|
|
|
|
cursor.z_as_relative = false
|
|
|
|
|
|
popup.add_child(cursor)
|
|
|
|
|
|
return cursor
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func _build_options() -> void:
|
|
|
|
|
|
_brush_option.clear()
|
|
|
|
|
|
for brush_size: int in PlayerArtUnlocks.BRUSH_SIZES:
|
2026-08-15 14:28:13 -04:00
|
|
|
|
_brush_option.add_icon_item(
|
|
|
|
|
|
BRUSH_SIZE_ICONS[brush_size],
|
|
|
|
|
|
"",
|
|
|
|
|
|
brush_size,
|
|
|
|
|
|
)
|
|
|
|
|
|
_brush_option.set_item_tooltip(
|
|
|
|
|
|
_brush_option.item_count - 1,
|
|
|
|
|
|
"%d× marker size" % brush_size,
|
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_grid_option.clear()
|
|
|
|
|
|
for grid_size: int in PlayerArtUnlocks.GRID_SIZES:
|
2026-08-15 14:28:13 -04:00
|
|
|
|
_grid_option.add_icon_item(
|
|
|
|
|
|
GRID_SIZE_ICONS[grid_size],
|
|
|
|
|
|
"",
|
|
|
|
|
|
grid_size,
|
|
|
|
|
|
)
|
|
|
|
|
|
_grid_option.set_item_tooltip(
|
|
|
|
|
|
_grid_option.item_count - 1,
|
|
|
|
|
|
"%d×%d grid" % [grid_size, grid_size],
|
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
for child: Node in _color_list.get_children():
|
|
|
|
|
|
child.queue_free()
|
|
|
|
|
|
_color_buttons.clear()
|
|
|
|
|
|
for color_id: StringName in SurfaceDrawingPalette.get_color_ids():
|
|
|
|
|
|
var button := Button.new()
|
|
|
|
|
|
button.custom_minimum_size = Vector2(34, 34)
|
2026-08-15 20:38:58 -04:00
|
|
|
|
button.focus_mode = Control.FOCUS_NONE
|
2026-08-02 00:45:07 -04:00
|
|
|
|
button.tooltip_text = SurfaceDrawingPalette.get_display_name(color_id)
|
|
|
|
|
|
button.pressed.connect(_select_color.bind(color_id))
|
|
|
|
|
|
_color_list.add_child(button)
|
|
|
|
|
|
_color_buttons[color_id] = button
|
2026-08-19 10:02:28 -04:00
|
|
|
|
_add_color_lock_icon(button)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_apply_color_button_style(button, color_id, false)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 10:02:28 -04:00
|
|
|
|
func _add_color_lock_icon(button: Button) -> void:
|
|
|
|
|
|
var icon_size := Vector2.ONE * (
|
|
|
|
|
|
LockedContentPresentationType.COMPACT_ICON_SIZE
|
|
|
|
|
|
)
|
|
|
|
|
|
var icon := TextureRect.new()
|
|
|
|
|
|
icon.name = "UnlockStateIcon"
|
|
|
|
|
|
icon.texture = LockedContentPresentationType.ICON
|
|
|
|
|
|
icon.set_anchors_preset(Control.PRESET_CENTER)
|
|
|
|
|
|
icon.offset_left = -icon_size.x * 0.5
|
|
|
|
|
|
icon.offset_top = -icon_size.y * 0.5
|
|
|
|
|
|
icon.offset_right = icon_size.x * 0.5
|
|
|
|
|
|
icon.offset_bottom = icon_size.y * 0.5
|
|
|
|
|
|
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
|
|
|
|
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
|
|
|
|
icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
|
|
|
|
|
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
|
|
|
|
icon.modulate = Color(
|
|
|
|
|
|
UtilityPageStyleType.OCEAN_TEXT_SECONDARY,
|
|
|
|
|
|
COLOR_LOCK_ICON_ALPHA,
|
|
|
|
|
|
)
|
|
|
|
|
|
icon.z_index = 1
|
|
|
|
|
|
icon.visible = false
|
|
|
|
|
|
button.add_child(icon)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 20:38:58 -04:00
|
|
|
|
func _apply_marker_color_to_brush_icons(marker_color: Color) -> void:
|
|
|
|
|
|
if _applied_marker_icon_color.is_equal_approx(marker_color):
|
|
|
|
|
|
return
|
|
|
|
|
|
_applied_marker_icon_color = marker_color
|
2026-08-19 10:02:28 -04:00
|
|
|
|
_brush_option_icons.clear()
|
2026-08-15 20:38:58 -04:00
|
|
|
|
for index: int in range(_brush_option.item_count):
|
|
|
|
|
|
var brush_size: int = _brush_option.get_item_id(index)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
_brush_option_icons[brush_size] = _channel_masked_icon(
|
|
|
|
|
|
BRUSH_SIZE_ICONS[brush_size], marker_color
|
2026-08-15 20:38:58 -04:00
|
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
if _unlocks == null or _unlocks.is_brush_size_unlocked(brush_size):
|
|
|
|
|
|
_brush_option.set_item_icon(
|
|
|
|
|
|
index, _brush_option_icons[brush_size]
|
|
|
|
|
|
)
|
2026-08-15 20:38:58 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _channel_masked_icon(source: Texture2D, marker_color: Color) -> Texture2D:
|
|
|
|
|
|
var source_image: Image = source.get_image()
|
|
|
|
|
|
if source_image == null or source_image.is_empty():
|
|
|
|
|
|
return source
|
|
|
|
|
|
var image := source_image.duplicate() as Image
|
|
|
|
|
|
if image.is_compressed():
|
|
|
|
|
|
if image.decompress() != OK:
|
|
|
|
|
|
return source
|
|
|
|
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
|
|
|
|
for y: int in image.get_height():
|
|
|
|
|
|
for x: int in image.get_width():
|
|
|
|
|
|
var source_color: Color = image.get_pixel(x, y)
|
|
|
|
|
|
var red_dominance: float = (
|
|
|
|
|
|
source_color.r - maxf(source_color.g, source_color.b)
|
|
|
|
|
|
)
|
|
|
|
|
|
var marker_mask: float = smoothstep(0.04, 0.18, red_dominance)
|
|
|
|
|
|
image.set_pixel(
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
Color(
|
|
|
|
|
|
lerpf(
|
|
|
|
|
|
source_color.r,
|
|
|
|
|
|
marker_color.r * source_color.r,
|
|
|
|
|
|
marker_mask,
|
|
|
|
|
|
),
|
|
|
|
|
|
lerpf(
|
|
|
|
|
|
source_color.g,
|
|
|
|
|
|
marker_color.g * source_color.r,
|
|
|
|
|
|
marker_mask,
|
|
|
|
|
|
),
|
|
|
|
|
|
lerpf(
|
|
|
|
|
|
source_color.b,
|
|
|
|
|
|
marker_color.b * source_color.r,
|
|
|
|
|
|
marker_mask,
|
|
|
|
|
|
),
|
|
|
|
|
|
source_color.a * marker_color.a,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
var texture := ImageTexture.create_from_image(image)
|
|
|
|
|
|
texture.set_meta(&"channel_mask_source", source.resource_path)
|
|
|
|
|
|
return texture
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func _refresh_unlocks() -> void:
|
|
|
|
|
|
if not is_node_ready() or _unlocks == null:
|
|
|
|
|
|
return
|
|
|
|
|
|
var brush_popup: PopupMenu = _brush_option.get_popup()
|
|
|
|
|
|
for index: int in range(_brush_option.item_count):
|
|
|
|
|
|
var brush_size: int = _brush_option.get_item_id(index)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
_apply_popup_unlock_state(
|
|
|
|
|
|
brush_popup,
|
|
|
|
|
|
index,
|
|
|
|
|
|
_unlocks.is_brush_size_unlocked(brush_size),
|
|
|
|
|
|
_brush_option_icons.get(brush_size, BRUSH_SIZE_ICONS[brush_size]),
|
2026-08-02 00:45:07 -04:00
|
|
|
|
)
|
|
|
|
|
|
var grid_popup: PopupMenu = _grid_option.get_popup()
|
|
|
|
|
|
for index: int in range(_grid_option.item_count):
|
|
|
|
|
|
var grid_size: int = _grid_option.get_item_id(index)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
_apply_popup_unlock_state(
|
|
|
|
|
|
grid_popup,
|
|
|
|
|
|
index,
|
|
|
|
|
|
_unlocks.is_grid_size_unlocked(grid_size),
|
|
|
|
|
|
GRID_SIZE_ICONS[grid_size],
|
2026-08-02 00:45:07 -04:00
|
|
|
|
)
|
|
|
|
|
|
for color_id: StringName in _color_buttons:
|
|
|
|
|
|
var button: Button = _color_buttons[color_id]
|
|
|
|
|
|
button.disabled = not _unlocks.is_color_unlocked(color_id)
|
2026-08-19 10:02:28 -04:00
|
|
|
|
var lock_icon := button.get_node("UnlockStateIcon") as TextureRect
|
|
|
|
|
|
lock_icon.visible = button.disabled
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_apply_color_button_style(
|
|
|
|
|
|
button,
|
|
|
|
|
|
color_id,
|
2026-08-02 09:20:44 -04:00
|
|
|
|
(
|
|
|
|
|
|
_service != null
|
|
|
|
|
|
and not _service.is_eraser_mode()
|
|
|
|
|
|
and _service.get_color_id() == color_id
|
|
|
|
|
|
),
|
2026-08-02 00:45:07 -04:00
|
|
|
|
)
|
2026-08-21 21:01:21 -04:00
|
|
|
|
_stamp_button.disabled = not _unlocks.is_stamp_unlocked()
|
|
|
|
|
|
_stamp_button.tooltip_text = (
|
|
|
|
|
|
"Place saved artwork stamps"
|
|
|
|
|
|
if not _stamp_button.disabled
|
|
|
|
|
|
else "Unlock the stamp tool in Art Supplies"
|
|
|
|
|
|
)
|
|
|
|
|
|
if _stamp_button.disabled:
|
|
|
|
|
|
_close_stamp_library()
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-19 10:02:28 -04:00
|
|
|
|
func _apply_popup_unlock_state(
|
|
|
|
|
|
popup: PopupMenu,
|
|
|
|
|
|
index: int,
|
|
|
|
|
|
unlocked: bool,
|
|
|
|
|
|
unlocked_icon: Texture2D,
|
|
|
|
|
|
) -> void:
|
|
|
|
|
|
popup.set_item_disabled(index, not unlocked)
|
|
|
|
|
|
popup.set_item_icon(index, unlocked_icon if unlocked else _popup_lock_icon)
|
|
|
|
|
|
popup.set_item_icon_modulate(
|
|
|
|
|
|
index,
|
|
|
|
|
|
Color.WHITE
|
|
|
|
|
|
if unlocked
|
|
|
|
|
|
else Color(
|
|
|
|
|
|
UtilityPageStyleType.OCEAN_TEXT_SECONDARY,
|
|
|
|
|
|
POPUP_LOCK_ICON_ALPHA,
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func _apply_color_button_style(
|
|
|
|
|
|
button: Button,
|
|
|
|
|
|
color_id: StringName,
|
|
|
|
|
|
selected: bool,
|
|
|
|
|
|
) -> void:
|
|
|
|
|
|
var color: Color = (
|
|
|
|
|
|
SurfaceDrawingPalette.get_color(color_id)
|
|
|
|
|
|
if not button.disabled
|
|
|
|
|
|
else UtilityPageStyleType.OCEAN_DISABLED
|
|
|
|
|
|
)
|
|
|
|
|
|
for style_name: StringName in [&"normal", &"hover", &"pressed", &"focus"]:
|
|
|
|
|
|
var style := StyleBoxFlat.new()
|
|
|
|
|
|
style.bg_color = color.lightened(0.12) if style_name == &"hover" else color
|
|
|
|
|
|
style.set_border_width_all(3 if selected else 0)
|
|
|
|
|
|
style.border_color = UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
|
|
|
|
|
style.set_corner_radius_all(17)
|
|
|
|
|
|
button.add_theme_stylebox_override(style_name, style)
|
|
|
|
|
|
var disabled_style := StyleBoxFlat.new()
|
2026-08-19 10:02:28 -04:00
|
|
|
|
disabled_style.bg_color = (
|
|
|
|
|
|
LockedContentPresentationType.disabled_background_color()
|
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
disabled_style.set_corner_radius_all(17)
|
|
|
|
|
|
button.add_theme_stylebox_override("disabled", disabled_style)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _toggle_mode() -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.set_placement_mode(not _service.is_placement_mode())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _select_brush(index: int) -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.set_brush_size(_brush_option.get_item_id(index))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _select_grid(index: int) -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.set_grid_size(_grid_option.get_item_id(index))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _select_color(color_id: StringName) -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.set_color_id(color_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 09:20:44 -04:00
|
|
|
|
func _toggle_eraser() -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.set_eraser_mode(not _service.is_eraser_mode())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _undo_last_stroke() -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.request_undo_last_stroke()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _arm_guide_action(action: int) -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
_service.arm_guide_action(action)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 21:01:21 -04:00
|
|
|
|
func _export_aimed_artwork() -> void:
|
|
|
|
|
|
if _service != null:
|
|
|
|
|
|
var export_path: String = _service.export_aimed_canvas()
|
|
|
|
|
|
if not export_path.is_empty() and _stamp_library_panel.visible:
|
|
|
|
|
|
_rebuild_stamp_library()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _toggle_stamp_library() -> void:
|
|
|
|
|
|
if _stamp_button.disabled or _service == null:
|
|
|
|
|
|
return
|
|
|
|
|
|
if _stamp_library_panel.visible:
|
|
|
|
|
|
_close_stamp_library()
|
|
|
|
|
|
return
|
|
|
|
|
|
_rebuild_stamp_library()
|
|
|
|
|
|
_stamp_library_panel.show()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _close_stamp_library() -> void:
|
|
|
|
|
|
_stamp_library_panel.hide()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _rebuild_stamp_library() -> void:
|
|
|
|
|
|
for child: Node in _stamp_grid.get_children():
|
|
|
|
|
|
child.queue_free()
|
|
|
|
|
|
var entries: Array[Dictionary] = (
|
|
|
|
|
|
_service.get_saved_stamp_entries() if _service != null else []
|
|
|
|
|
|
)
|
|
|
|
|
|
_empty_stamp_library.visible = entries.is_empty()
|
|
|
|
|
|
_stamp_scroll.visible = not entries.is_empty()
|
|
|
|
|
|
for entry: Dictionary in entries:
|
|
|
|
|
|
var button := Button.new()
|
|
|
|
|
|
button.custom_minimum_size = Vector2(110.0, 108.0)
|
|
|
|
|
|
button.focus_mode = Control.FOCUS_NONE
|
|
|
|
|
|
button.expand_icon = true
|
|
|
|
|
|
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
|
|
|
|
button.vertical_icon_alignment = VERTICAL_ALIGNMENT_TOP
|
|
|
|
|
|
button.add_theme_constant_override("icon_max_width", 74)
|
|
|
|
|
|
button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
|
|
|
|
|
button.icon = ImageTexture.create_from_image(entry["image"])
|
|
|
|
|
|
button.text = "%d×%d" % [entry["grid_size"], entry["grid_size"]]
|
|
|
|
|
|
button.tooltip_text = str(entry["file_name"])
|
|
|
|
|
|
button.accessibility_name = "saved artwork %s" % str(
|
|
|
|
|
|
entry["file_name"]
|
|
|
|
|
|
)
|
|
|
|
|
|
button.disabled = not bool(entry.get("available", false))
|
|
|
|
|
|
if button.disabled:
|
|
|
|
|
|
button.tooltip_text += "\nrequires its grid, colors, and stamp unlock"
|
|
|
|
|
|
UtilityPageStyleType.apply_ocean_button(button)
|
|
|
|
|
|
button.pressed.connect(
|
|
|
|
|
|
_select_saved_stamp.bind(str(entry["path"]))
|
|
|
|
|
|
)
|
|
|
|
|
|
_stamp_grid.add_child(button)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _select_saved_stamp(path: String) -> void:
|
|
|
|
|
|
if _service != null and _service.select_saved_stamp(path):
|
|
|
|
|
|
_close_stamp_library()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func _on_unlocks_changed(_unlock_mask: int) -> void:
|
|
|
|
|
|
_refresh_unlocks()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_service_state_changed(
|
|
|
|
|
|
is_active: bool,
|
|
|
|
|
|
mode_name: String,
|
|
|
|
|
|
_color_name: String,
|
2026-08-13 16:25:49 -04:00
|
|
|
|
color_value: Color,
|
2026-08-02 00:45:07 -04:00
|
|
|
|
brush_size: int,
|
|
|
|
|
|
grid_size: int,
|
|
|
|
|
|
_status: String,
|
|
|
|
|
|
) -> void:
|
|
|
|
|
|
visible = is_active
|
|
|
|
|
|
if not is_active:
|
2026-08-21 21:01:21 -04:00
|
|
|
|
_close_stamp_library()
|
2026-08-15 20:38:58 -04:00
|
|
|
|
hide_virtual_pointer_overlay()
|
2026-08-02 00:45:07 -04:00
|
|
|
|
return
|
2026-08-13 15:24:23 -04:00
|
|
|
|
_mode_button.icon = (
|
|
|
|
|
|
GRID_MODE_ICON if mode_name == "place grid" else MARKER_MODE_ICON
|
|
|
|
|
|
)
|
2026-08-13 16:25:49 -04:00
|
|
|
|
_marker_mode_material.set_shader_parameter("marker_color", color_value)
|
2026-08-15 20:38:58 -04:00
|
|
|
|
_apply_marker_color_to_brush_icons(color_value)
|
2026-08-13 15:24:23 -04:00
|
|
|
|
_mode_button.accessibility_name = (
|
|
|
|
|
|
"switch to marker mode"
|
|
|
|
|
|
if mode_name == "place grid"
|
|
|
|
|
|
else "switch to grid mode"
|
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_mode_button.tooltip_text = (
|
|
|
|
|
|
"Switch to marker mode"
|
|
|
|
|
|
if mode_name == "place grid"
|
|
|
|
|
|
else "Switch to grid mode"
|
|
|
|
|
|
)
|
|
|
|
|
|
_select_option_by_id(_brush_option, brush_size)
|
|
|
|
|
|
_select_option_by_id(_grid_option, grid_size)
|
2026-08-02 09:20:44 -04:00
|
|
|
|
_refresh_action_state()
|
2026-08-02 00:45:07 -04:00
|
|
|
|
_refresh_unlocks()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 09:20:44 -04:00
|
|
|
|
func _refresh_action_state() -> void:
|
|
|
|
|
|
if _service == null:
|
|
|
|
|
|
return
|
|
|
|
|
|
_eraser_button.button_pressed = _service.is_eraser_mode()
|
|
|
|
|
|
var guide_action: int = _service.get_armed_guide_action()
|
|
|
|
|
|
_hide_guide_button.button_pressed = (
|
|
|
|
|
|
guide_action == NetworkSurfaceDrawingService.GuideAction.HIDE
|
|
|
|
|
|
)
|
|
|
|
|
|
_restore_guide_button.button_pressed = (
|
|
|
|
|
|
guide_action == NetworkSurfaceDrawingService.GuideAction.RESTORE
|
|
|
|
|
|
)
|
|
|
|
|
|
_finalize_guide_button.button_pressed = (
|
|
|
|
|
|
guide_action == NetworkSurfaceDrawingService.GuideAction.FINALIZE
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 00:45:07 -04:00
|
|
|
|
func _select_option_by_id(option: OptionButton, item_id: int) -> void:
|
|
|
|
|
|
for index: int in range(option.item_count):
|
|
|
|
|
|
if option.get_item_id(index) == item_id:
|
|
|
|
|
|
option.select(index)
|
|
|
|
|
|
return
|