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:
Claude Code 2026-03-27 21:03:09 -07:00
parent 5ff756d975
commit 20de34b8cb
4 changed files with 154 additions and 9 deletions

View 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

View file

@ -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()

View 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

View file

@ -0,0 +1 @@
uid://bwevi2bwdn3bp