143 lines
4.1 KiB
GDScript
143 lines
4.1 KiB
GDScript
extends Node
|
||
## Maintains the window mouse passthrough polygon from the rendered viewport alpha.
|
||
## Only pixels where the avatar is actually drawn capture mouse events.
|
||
## Clicks on transparent areas fall through to the desktop at the OS level.
|
||
##
|
||
## Uses DisplayServer.window_set_mouse_passthrough(polygon) where the polygon
|
||
## encloses the opaque region of the viewport (i.e., where Miku is visible).
|
||
|
||
const GRID_SIZE := 24 # sample resolution (NxN grid across the viewport)
|
||
const ALPHA_THRESHOLD := 0.05 # pixels below this alpha are transparent
|
||
const UPDATE_INTERVAL := 1.0 # seconds between polygon recalculations
|
||
const INITIAL_DELAY := 2.0 # wait for VRM to load and first frame to render
|
||
|
||
var _elapsed := 0.0
|
||
var _initialized := false
|
||
var _paused := false
|
||
|
||
|
||
func _ready() -> void:
|
||
EventBus.drag_started.connect(_on_interaction_started)
|
||
EventBus.drag_ended.connect(_on_interaction_ended)
|
||
EventBus.rotate_started.connect(_on_interaction_started)
|
||
EventBus.rotate_ended.connect(_on_interaction_ended)
|
||
|
||
|
||
func _on_interaction_started() -> void:
|
||
_paused = true
|
||
|
||
|
||
func _on_interaction_ended() -> void:
|
||
_paused = false
|
||
_elapsed = 0.0
|
||
_update_passthrough()
|
||
|
||
|
||
func _process(delta: float) -> void:
|
||
if _paused:
|
||
return
|
||
_elapsed += delta
|
||
if not _initialized:
|
||
if _elapsed >= INITIAL_DELAY:
|
||
_initialized = true
|
||
_elapsed = 0.0
|
||
_update_passthrough()
|
||
return
|
||
if _elapsed >= UPDATE_INTERVAL:
|
||
_elapsed = 0.0
|
||
_update_passthrough()
|
||
|
||
|
||
func _update_passthrough() -> void:
|
||
var vp := get_viewport()
|
||
if vp == null:
|
||
return
|
||
var img := vp.get_texture().get_image()
|
||
if img == null or img.is_empty():
|
||
return
|
||
var polygon := _build_polygon(img)
|
||
if polygon.is_empty():
|
||
DisplayServer.window_set_mouse_passthrough(polygon)
|
||
return
|
||
# Viewport renders at base resolution (e.g. 720×1440) but window_set_mouse_passthrough
|
||
# expects window-space coordinates. Scale the polygon to match the actual window size.
|
||
var win := Vector2(DisplayServer.window_get_size())
|
||
var img_sz := Vector2(img.get_width(), img.get_height())
|
||
var scale := win / img_sz
|
||
var scaled := PackedVector2Array()
|
||
scaled.resize(polygon.size())
|
||
for i: int in polygon.size():
|
||
scaled[i] = polygon[i] * scale
|
||
DisplayServer.window_set_mouse_passthrough(scaled)
|
||
|
||
|
||
## Samples the image at GRID_SIZE×GRID_SIZE, collects corners of opaque cells,
|
||
## then wraps them in a convex hull that becomes the clickable region.
|
||
func _build_polygon(img: Image) -> PackedVector2Array:
|
||
var w := img.get_width()
|
||
var h := img.get_height()
|
||
if w == 0 or h == 0:
|
||
return PackedVector2Array()
|
||
|
||
var step_x := float(w) / GRID_SIZE
|
||
var step_y := float(h) / GRID_SIZE
|
||
|
||
var samples := PackedVector2Array()
|
||
for gy in range(GRID_SIZE):
|
||
for gx in range(GRID_SIZE):
|
||
var px := clampi(int((gx + 0.5) * step_x), 0, w - 1)
|
||
var py := clampi(int((gy + 0.5) * step_y), 0, h - 1)
|
||
if img.get_pixel(px, py).a > ALPHA_THRESHOLD:
|
||
# Push all four corners of this grid cell as hull candidates
|
||
var x0 := gx * step_x
|
||
var y0 := gy * step_y
|
||
var x1 := (gx + 1) * step_x
|
||
var y1 := (gy + 1) * step_y
|
||
samples.append(Vector2(x0, y0))
|
||
samples.append(Vector2(x1, y0))
|
||
samples.append(Vector2(x0, y1))
|
||
samples.append(Vector2(x1, y1))
|
||
|
||
if samples.is_empty():
|
||
# No opaque pixels — fall back to capturing the full window rather than
|
||
# making it completely unclickable.
|
||
return PackedVector2Array(
|
||
[
|
||
Vector2(0, 0),
|
||
Vector2(w, 0),
|
||
Vector2(w, h),
|
||
Vector2(0, h),
|
||
]
|
||
)
|
||
|
||
return _convex_hull(samples)
|
||
|
||
|
||
## Jarvis march (gift wrapping) convex hull.
|
||
## Returns the convex hull of pts in CW order (screen-space, y-down).
|
||
func _convex_hull(pts: PackedVector2Array) -> PackedVector2Array:
|
||
var n := pts.size()
|
||
if n < 3:
|
||
return pts
|
||
|
||
# Start from the leftmost point (guaranteed on the hull)
|
||
var start := 0
|
||
for i in range(1, n):
|
||
if pts[i].x < pts[start].x:
|
||
start = i
|
||
|
||
var hull := PackedVector2Array()
|
||
var cur := start
|
||
while true:
|
||
hull.append(pts[cur])
|
||
var nxt := (cur + 1) % n
|
||
for i in range(n):
|
||
# cross < 0: pts[i] is clockwise of cur→nxt in screen coords (y-down)
|
||
var cross := (pts[nxt] - pts[cur]).cross(pts[i] - pts[cur])
|
||
if cross < 0:
|
||
nxt = i
|
||
cur = nxt
|
||
if cur == start or hull.size() > n:
|
||
break
|
||
|
||
return hull
|