@@ -5,13 +5,16 @@ import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
55import { useLive2d } from ' @proj-airi/stage-ui/stores/live2d'
66import { useMcpStore } from ' @proj-airi/stage-ui/stores/mcp'
77import { connectServer } from ' @proj-airi/tauri-plugin-mcp'
8+ import { watchThrottled } from ' @vueuse/core'
89import { storeToRefs } from ' pinia'
910import { computed , onMounted , onUnmounted , ref , watch } from ' vue'
1011
1112import ResourceStatusIsland from ' ../components/Widgets/ResourceStatusIsland/index.vue'
1213
13- import { useTauriCore , useTauriEvent } from ' ../composables/tauri'
14+ import { commands as passThroughCommands } from ' ../bindings/tauri-plugins/window-pass-through-on-hover'
15+ import { useTauriCore , useTauriEvent , useTauriWindow } from ' ../composables/tauri'
1416import { useTauriGlobalShortcuts } from ' ../composables/tauri-global-shortcuts'
17+ import { useRdevMouse } from ' ../composables/use-rdev-mouse'
1518import { useResourcesStore } from ' ../stores/resources'
1619import { useWindowStore } from ' ../stores/window'
1720import { useWindowControlStore } from ' ../stores/window-controls'
@@ -21,14 +24,117 @@ useTauriGlobalShortcuts()
2124const windowControlStore = useWindowControlStore ()
2225const resourcesStore = useResourcesStore ()
2326const mcpStore = useMcpStore ()
27+ const { getPosition } = useTauriWindow ()
28+ const { mouseX, mouseY } = useRdevMouse ()
2429
2530const { listen } = useTauriEvent <AiriTamagotchiEvents >()
2631const { invoke } = useTauriCore ()
2732const { connected, serverCmd, serverArgs } = storeToRefs (mcpStore )
2833const { scale, positionInPercentageString } = storeToRefs (useLive2d ())
2934
30- const { centerPos, live2dLookAtX, live2dLookAtY, shouldHideView } = storeToRefs (useWindowStore ())
35+ const { centerPos, live2dLookAtX, live2dLookAtY } = storeToRefs (useWindowStore ())
3136const live2dFocusAt = ref <Point >(centerPos .value )
37+ const widgetStageRef = ref <{ canvasElement: () => HTMLCanvasElement }>()
38+ const resourceStatusIslandRef = ref <InstanceType <typeof ResourceStatusIsland >>()
39+ const buttonsContainerRef = ref <HTMLDivElement >()
40+ const windowX = ref (0 )
41+ const windowY = ref (0 )
42+ const isClickThrough = ref (false )
43+ const isPassingThrough = ref (false )
44+ const isOverUI = ref (false )
45+
46+ watchThrottled ([mouseX , mouseY ], async ([x , y ]) => {
47+ const canvas = widgetStageRef .value ?.canvasElement ()
48+ if (! canvas )
49+ return
50+
51+ if (windowControlStore .controlMode === WindowControlMode .RESIZE || windowControlStore .controlMode === WindowControlMode .MOVE ) {
52+ if (isPassingThrough .value ) {
53+ passThroughCommands .stopPassThrough ()
54+ isPassingThrough .value = false
55+ }
56+ return
57+ }
58+
59+ const relativeX = x - windowX .value
60+ const relativeY = y - windowY .value
61+
62+ const islandEl = resourceStatusIslandRef .value ?.$el as HTMLElement
63+ const buttonsEl = buttonsContainerRef .value
64+
65+ isOverUI .value = false
66+ if (! windowControlStore .isIgnoringMouseEvent ) {
67+ if (islandEl ) {
68+ const rect = islandEl .getBoundingClientRect ()
69+ if (relativeX >= rect .left && relativeX <= rect .right && relativeY >= rect .top && relativeY <= rect .bottom )
70+ isOverUI .value = true
71+ }
72+ if (! isOverUI .value && buttonsEl ) {
73+ const rect = buttonsEl .getBoundingClientRect ()
74+ if (relativeX >= rect .left && relativeX <= rect .right && relativeY >= rect .top && relativeY <= rect .bottom )
75+ isOverUI .value = true
76+ }
77+
78+ if (isOverUI .value ) {
79+ if (isPassingThrough .value ) {
80+ passThroughCommands .stopPassThrough ()
81+ isPassingThrough .value = false
82+ }
83+ return
84+ }
85+ }
86+
87+ let isTransparent = false
88+ if (
89+ ! isOverUI .value
90+ && relativeX >= 0
91+ && relativeX < canvas .clientWidth
92+ && relativeY >= 0
93+ && relativeY < canvas .clientHeight
94+ ) {
95+ const gl = canvas .getContext (' webgl2' ) || canvas .getContext (' webgl' )
96+ if (gl ) {
97+ const pixelX = relativeX * (gl .drawingBufferWidth / canvas .clientWidth )
98+ const pixelY
99+ = gl .drawingBufferHeight
100+ - relativeY * (gl .drawingBufferHeight / canvas .clientHeight )
101+
102+ const data = new Uint8Array (4 )
103+ gl .readPixels (
104+ Math .floor (pixelX ),
105+ Math .floor (pixelY ),
106+ 1 ,
107+ 1 ,
108+ gl .RGBA ,
109+ gl .UNSIGNED_BYTE ,
110+ data ,
111+ )
112+ isTransparent = data [3 ] < 100 // Use a small threshold for anti-aliasing
113+ }
114+ }
115+ else {
116+ isTransparent = true
117+ }
118+
119+ isClickThrough .value = isTransparent
120+
121+ if (windowControlStore .isIgnoringMouseEvent ) {
122+ if (! isPassingThrough .value ) {
123+ passThroughCommands .startPassThrough ()
124+ isPassingThrough .value = true
125+ }
126+ return
127+ }
128+
129+ if (isTransparent && ! isPassingThrough .value ) {
130+ passThroughCommands .startPassThrough ()
131+ isPassingThrough .value = true
132+ }
133+ else if (! isTransparent && isPassingThrough .value ) {
134+ passThroughCommands .stopPassThrough ()
135+ isPassingThrough .value = false
136+ }
137+ }, { throttle: 33 })
32138
33139watch ([live2dLookAtX , live2dLookAtY ], ([x , y ]) => live2dFocusAt .value = { x , y }, { immediate: true })
34140
@@ -82,6 +188,16 @@ async function setupWhisperModel() {
82188}
83189
84190onMounted (async () => {
191+ const pos = await getPosition ()
192+ if (pos ) {
193+ windowX .value = pos .x
194+ windowY .value = pos .y
195+ }
196+ unListenFuncs .push (await listen (' tauri://move' , (event ) => {
197+ windowX .value = event .payload .payload .x
198+ windowY .value = event .payload .payload .y
199+ }))
200+
85201 await setupVADModel ()
86202 await setupWhisperModel ()
87203
@@ -118,7 +234,8 @@ if (import.meta.hot) { // For better DX
118234<template >
119235 <div
120236 :class =" [modeIndicatorClass, {
121- 'op-0': shouldHideView,
237+ 'op-0': windowControlStore.isIgnoringMouseEvent && !isClickThrough,
238+ 'pointer-events-none': !isClickThrough,
122239 }]"
123240 max-h =" [100vh]"
124241 max-w =" [100vw]"
@@ -128,17 +245,19 @@ if (import.meta.hot) { // For better DX
128245 >
129246 <div relative h-full w-full items-end gap-2 class =" view" >
130247 <WidgetStage
248+ ref =" widgetStageRef"
131249 h-full w-full flex-1
132250 :focus-at =" live2dFocusAt" :scale =" scale"
133251 :x-offset =" positionInPercentageString.x"
134252 :y-offset =" positionInPercentageString.y" mb =" <md:18"
135253 />
136- <ResourceStatusIsland />
254+ <ResourceStatusIsland ref = " resourceStatusIslandRef " />
137255 <div
256+ ref =" buttonsContainerRef"
138257 absolute bottom-4 left-4 flex gap-1 op-0 transition =" opacity duration-500"
139258 :class =" {
140- 'pointer-events-none': windowControlStore.isControlActive ,
141- 'show-on-hover': !windowControlStore.isIgnoringMouseEvent,
259+ 'pointer-events-none': isClickThrough && !isOverUI ,
260+ 'show-on-hover': !windowControlStore.isIgnoringMouseEvent && (!isClickThrough || isOverUI) ,
142261 }"
143262 >
144263 <div
@@ -212,10 +331,8 @@ if (import.meta.hot) { // For better DX
212331.view {
213332 transition : opacity 0.5s ease-in-out ;
214333
215- &:hover {
216- .show-on-hover {
217- opacity: 1 ;
218- }
334+ .show-on-hover {
335+ opacity : 1 ;
219336 }
220337}
221338
0 commit comments