refactor(screen-cursor): ♻️ Optimize cursor UI consistency and performance by updating shape handling, UI definitions, and path resolution utilities
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
5ff756d975
commit
20de34b8cb
4 changed files with 154 additions and 9 deletions
49
godot/scripts/util/config_paths.gd
Normal file
49
godot/scripts/util/config_paths.gd
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
extends Node
|
||||
## Cross-platform config directory utility.
|
||||
## Resolves config directories following OS conventions:
|
||||
## - Windows: %APPDATA%\chobit\
|
||||
## - macOS: ~/Library/Application Support/chobit/
|
||||
## - Linux: $XDG_CONFIG_HOME/chobit/ or ~/.config/chobit/
|
||||
|
||||
class_name ConfigPaths
|
||||
|
||||
|
||||
## Get the base config directory for the current OS.
|
||||
static func get_config_dir(app_name: String = "chobit") -> String:
|
||||
match OS.get_name():
|
||||
"Windows":
|
||||
var appdata := OS.get_environment("APPDATA")
|
||||
if appdata.is_empty():
|
||||
appdata = OS.get_environment("USERPROFILE") + "\\AppData\\Roaming"
|
||||
return appdata + "\\" + app_name
|
||||
"macOS":
|
||||
var home := OS.get_environment("HOME")
|
||||
if home.is_empty():
|
||||
push_error("ConfigPaths: HOME environment variable not set")
|
||||
return ""
|
||||
return home + "/Library/Application Support/" + app_name
|
||||
_: # Linux and others
|
||||
var xdg_config := OS.get_environment("XDG_CONFIG_HOME")
|
||||
if not xdg_config.is_empty():
|
||||
return xdg_config + "/" + app_name
|
||||
var home := OS.get_environment("HOME")
|
||||
if home.is_empty():
|
||||
push_error("ConfigPaths: HOME environment variable not set")
|
||||
return ""
|
||||
return home + "/.config/" + app_name
|
||||
|
||||
|
||||
## Get full path to a config file in the config directory.
|
||||
## Ensures the directory exists.
|
||||
static func get_config_file(filename: String, app_name: String = "chobit") -> String:
|
||||
var config_dir := get_config_dir(app_name)
|
||||
if config_dir.is_empty():
|
||||
return ""
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
var err := DirAccess.make_dir_recursive_absolute(config_dir)
|
||||
if err != OK:
|
||||
push_error("ConfigPaths: Failed to create directory %s (error: %d)" % [config_dir, err])
|
||||
return ""
|
||||
|
||||
return config_dir + ("/" if OS.get_name() != "Windows" else "\\") + filename
|
||||
|
|
@ -3,9 +3,7 @@ extends RefCounted
|
|||
## Static utility methods for node tree traversal.
|
||||
|
||||
|
||||
static func find_child_of_type(
|
||||
node: Node, type_name: String
|
||||
) -> Node:
|
||||
static func find_child_of_type(node: Node, type_name: String) -> Node:
|
||||
if node.get_class() == type_name:
|
||||
return node
|
||||
for child: Node in node.get_children():
|
||||
|
|
@ -29,9 +27,7 @@ static func find_first_mesh_with_blendshapes(
|
|||
return null
|
||||
|
||||
|
||||
static func find_blend_shape_index(
|
||||
mesh: MeshInstance3D, shape_name: String
|
||||
) -> int:
|
||||
static func find_blend_shape_index(mesh: MeshInstance3D, shape_name: String) -> int:
|
||||
if mesh == null or mesh.mesh == null:
|
||||
return -1
|
||||
for i: int in range(mesh.mesh.get_blend_shape_count()):
|
||||
|
|
@ -40,12 +36,50 @@ static func find_blend_shape_index(
|
|||
return -1
|
||||
|
||||
|
||||
static func find_bone_case_insensitive(
|
||||
skeleton: Skeleton3D, bone_name: String
|
||||
) -> int:
|
||||
static func find_bone_case_insensitive(skeleton: Skeleton3D, bone_name: String) -> int:
|
||||
var idx := skeleton.find_bone(bone_name)
|
||||
if idx == -1:
|
||||
idx = skeleton.find_bone(bone_name.to_lower())
|
||||
if idx == -1:
|
||||
idx = skeleton.find_bone(bone_name.to_upper())
|
||||
return idx
|
||||
|
||||
|
||||
static func read_vrm_morph_names(vrm_path: String) -> PackedStringArray:
|
||||
## Reads original morph target names from a .vrm (GLB) file's JSON chunk.
|
||||
## VRM4Godot renames blendshapes to morph_N during import, but the
|
||||
## original names (e.g. Japanese MMD names) are preserved in
|
||||
## mesh.primitives[].extras.targetNames inside the GLB JSON.
|
||||
var f := FileAccess.open(vrm_path, FileAccess.READ)
|
||||
if f == null:
|
||||
return PackedStringArray()
|
||||
|
||||
# GLB header: magic(4) + version(4) + length(4)
|
||||
f.get_32()
|
||||
f.get_32()
|
||||
f.get_32()
|
||||
|
||||
# First chunk: length(4) + type(4) + JSON data
|
||||
var chunk_length := f.get_32()
|
||||
f.get_32()
|
||||
var json_bytes := f.get_buffer(chunk_length)
|
||||
f.close()
|
||||
|
||||
var json := JSON.new()
|
||||
if json.parse(json_bytes.get_string_from_utf8()) != OK:
|
||||
return PackedStringArray()
|
||||
|
||||
var data: Dictionary = json.data
|
||||
var meshes: Array = data.get("meshes", [])
|
||||
for mesh_data: Variant in meshes:
|
||||
var primitives: Array = mesh_data.get("primitives", [])
|
||||
for prim: Variant in primitives:
|
||||
var extras: Dictionary = prim.get("extras", {})
|
||||
var target_names: Array = extras.get("targetNames", [])
|
||||
if target_names.size() > 0:
|
||||
var result := PackedStringArray()
|
||||
for n: Variant in target_names:
|
||||
result.append(str(n))
|
||||
return result
|
||||
|
||||
return PackedStringArray()
|
||||
|
|
|
|||
61
godot/scripts/util/screen_cursor.gd
Normal file
61
godot/scripts/util/screen_cursor.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
class_name ScreenCursor
|
||||
extends RefCounted
|
||||
## Maps the global mouse position to a normalized -1..1 range
|
||||
## relative to the window center, using screen edges as bounds.
|
||||
## Handles multi-monitor and Wayland focus limitations.
|
||||
|
||||
## Max pixel distance from window center that maps to 1.0.
|
||||
## Prevents over-sensitive tracking on ultra-wide setups.
|
||||
const MAX_RANGE_PX: float = 2000.0
|
||||
|
||||
static var _last_valid: Vector2 = Vector2.ZERO
|
||||
|
||||
|
||||
static func get_normalized_position() -> Vector2:
|
||||
var ds := DisplayServer
|
||||
var mouse := ds.mouse_get_position()
|
||||
var win_pos := ds.window_get_position()
|
||||
var win_size := ds.window_get_size()
|
||||
|
||||
var cx := win_pos.x + win_size.x / 2
|
||||
var cy := win_pos.y + win_size.y / 2
|
||||
|
||||
var dx := float(mouse.x - cx)
|
||||
var dy := float(mouse.y - cy)
|
||||
|
||||
# On Wayland, mouse position may freeze at (0,0) when unfocused
|
||||
if mouse.x == 0 and mouse.y == 0:
|
||||
return _last_valid
|
||||
|
||||
# Use screen the window is on for bounds
|
||||
var screen_idx := ds.window_get_current_screen()
|
||||
var screen_rect := ds.screen_get_usable_rect(screen_idx)
|
||||
var screen_pos := screen_rect.position
|
||||
var screen_size := screen_rect.size
|
||||
|
||||
# Distance from window center to each screen edge
|
||||
var dist_left := maxf(float(cx - screen_pos.x), 1.0)
|
||||
var dist_right := maxf(float(screen_pos.x + screen_size.x - cx), 1.0)
|
||||
var dist_up := maxf(float(cy - screen_pos.y), 1.0)
|
||||
var dist_down := maxf(float(screen_pos.y + screen_size.y - cy), 1.0)
|
||||
|
||||
# Cap range so ultra-wide screens don't make gaze too sensitive
|
||||
dist_left = minf(dist_left, MAX_RANGE_PX)
|
||||
dist_right = minf(dist_right, MAX_RANGE_PX)
|
||||
dist_up = minf(dist_up, MAX_RANGE_PX)
|
||||
dist_down = minf(dist_down, MAX_RANGE_PX)
|
||||
|
||||
var nx: float = 0.0
|
||||
if dx > 0.0:
|
||||
nx = clampf(dx / dist_right, 0.0, 1.0)
|
||||
elif dx < 0.0:
|
||||
nx = clampf(dx / dist_left, -1.0, 0.0)
|
||||
|
||||
var ny: float = 0.0
|
||||
if dy > 0.0:
|
||||
ny = -clampf(dy / dist_down, 0.0, 1.0)
|
||||
elif dy < 0.0:
|
||||
ny = -clampf(dy / dist_up, -1.0, 0.0)
|
||||
|
||||
_last_valid = Vector2(nx, ny)
|
||||
return _last_valid
|
||||
1
godot/scripts/util/screen_cursor.gd.uid
Normal file
1
godot/scripts/util/screen_cursor.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bwevi2bwdn3bp
|
||||
Loading…
Add table
Reference in a new issue