11<script setup lang="ts">
2- import { computed , nextTick , onBeforeUnmount , ref , shallowRef , toRaw , toRefs , watch } from ' vue'
2+ import { computed , nextTick , onBeforeUnmount , ref , shallowRef , toRefs , watch } from ' vue'
33import { DecalGeometry } from ' three-stdlib'
4- import { Mesh , MeshBasicMaterial , Vector3 } from ' three'
5- import { useLoop } from ' @tresjs/core'
4+ import {
5+ EdgesGeometry ,
6+ LineBasicMaterial ,
7+ LineSegments ,
8+ Mesh ,
9+ Vector3 ,
10+ } from ' three'
611import { decalBus } from ' ./DecalBus'
12+ // 1. On réimporte useLoop pour l'animation
13+ import { dispose , useLoop } from ' @tresjs/core'
714
815const props = defineProps <{
916 parent: Mesh
@@ -14,59 +21,101 @@ const props = defineProps<{
1421 isSelected: boolean
1522}>()
1623
17- const emit = defineEmits ([' select' ])
18-
19- let localElapsed = 0
20- const wasSelected = ref (false )
24+ const emit = defineEmits ([' select' , ' hover' ])
2125
2226const { parent, decal, map, isSelected, highlight } = toRefs (props )
2327
2428const meshRef = ref <Mesh | null >(null )
25- const raycastMesh = ref <Mesh | null >(null )
26- const boxHelper = shallowRef (null )
2729const isHovered = ref (false )
2830
31+ const helperLineRef = shallowRef <LineSegments | null >(null )
32+
2933const originalScale = new Mesh ().scale .clone ()
3034
31- // watch(() => [meshRef.value, parent.value], () => {
32- // const p = toRaw(parent.value)
33- // const m = toRaw(meshRef.value)
34-
35- // // Si on a le parent et le mesh du sticker
36- // if (p && m) {
37- // // On force l'attachement parent-enfant natif de Three.js
38- // // Cela corrige instantanément la position et l'échelle
39- // p.attach(m)
40- // }
41- // }, { immediate: true })
42-
43- const updateRaycastClone = () => {
44- if (! props .groupTest || ! meshRef .value ) { return }
45-
46- if (! raycastMesh .value ) {
47- raycastMesh .value = new Mesh (
48- meshRef .value .geometry ,
49- new MeshBasicMaterial ({ visible: false }),
50- )
51-
52- raycastMesh .value .name = ` raycast-decal-${decal .value .id } `
53- raycastMesh .value .userData = {
54- id: decal .value .id ,
55- originalUuid: decal .value .uuid ,
35+ // --- GESTION DE LA MÉMOIRE AVEC DISPOSE() ---
36+
37+ const clearHelper = () => {
38+ if (helperLineRef .value ) {
39+ if (meshRef .value ) {
40+ meshRef .value .remove (helperLineRef .value )
5641 }
42+ dispose (helperLineRef .value )
43+ helperLineRef .value = null
44+ }
45+ }
46+
47+ // --- CRÉATION OBJETS ---
48+
49+ const createHelper = () => {
50+ if (! meshRef .value || ! meshRef .value .geometry ) { return }
51+
52+ clearHelper ()
53+
54+ const edgesGeometry = new EdgesGeometry (meshRef .value .geometry )
55+
56+ // 2. On active la transparence pour permettre l'animation d'opacité
57+ const lineMaterial = new LineBasicMaterial ({
58+ color: 0x0000FF , // Reste toujours BLEU
59+ depthTest: false ,
60+ linewidth: 2 ,
61+ transparent: true , // Important pour le clignotement
62+ opacity: 1 ,
63+ })
64+
65+ const line = new LineSegments (edgesGeometry , lineMaterial )
66+
67+ line .raycast = () => {}
68+
69+ meshRef .value .add (line )
70+ helperLineRef .value = line
71+
72+ updateHelperVisuals ()
73+ }
5774
58- props .groupTest .add (raycastMesh .value )
75+ const updateHelperVisuals = () => {
76+ if (! helperLineRef .value ) { return }
77+
78+ // Gère uniquement la visibilité globale (on/off)
79+ const visible = isHovered .value || isSelected .value
80+ helperLineRef .value .visible = visible
81+
82+ // J'ai supprimé le changement de couleur ici, cela reste bleu (défini dans createHelper)
83+ }
84+
85+ // --- ANIMATION (CLIGNOTEMENT) ---
86+
87+ const { onBeforeRender } = useLoop ()
88+
89+ onBeforeRender (({ elapsed }) => {
90+ // Si le helper n'existe pas ou n'est pas visible, on ne fait rien
91+ if (! helperLineRef .value || ! helperLineRef .value .visible ) { return }
92+
93+ const material = helperLineRef .value .material as LineBasicMaterial
94+
95+ if (isSelected .value ) {
96+ // 3. Calcul du clignotement (Sinusoidale)
97+ // elapsed * 10 contrôle la vitesse
98+ const t = (Math .sin (elapsed * 10 ) + 1 ) / 2
99+ // Opacité oscille entre 0.2 et 1
100+ material .opacity = 0.2 + t * 0.8
59101 }
60102 else {
61- raycastMesh .value .geometry = meshRef .value .geometry
103+ // Si juste survolé (pas sélectionné), opacité fixe à 1
104+ if (material .opacity !== 1 ) { material .opacity = 1 }
62105 }
63- }
106+ })
107+
108+ // --- LOGIQUE GEOMETRIE (Inchangée) ---
64109
65110const buildGeometry = () => {
66111 if (! meshRef .value || ! parent .value ) { return }
67112
68113 parent .value .updateMatrixWorld (true )
69114
115+ if (meshRef .value .geometry ) {
116+ meshRef .value .geometry .dispose ()
117+ }
118+
70119 const geometry = new DecalGeometry (
71120 parent .value ,
72121 decal .value .position ,
@@ -75,7 +124,6 @@ const buildGeometry = () => {
75124 )
76125
77126 const inverseMatrix = parent .value .matrixWorld .clone ().invert ()
78-
79127 geometry .applyMatrix4 (inverseMatrix )
80128
81129 const worldNormal = new Vector3 (0 , 0 , 1 ).applyEuler (decal .value .orientation )
@@ -84,7 +132,7 @@ const buildGeometry = () => {
84132 const baseOffset = 0.01
85133 const layerGap = 0.001
86134 const currentZ = decal .value .zIndex ?? 0
87- const parentScale = parent .value .scale .x || 1 // Approximation uniforme
135+ const parentScale = parent .value .scale .x || 1
88136 const totalOffset = (baseOffset + currentZ * layerGap ) / parentScale
89137
90138 geometry .translate (
@@ -93,75 +141,24 @@ const buildGeometry = () => {
93141 localNormal .z * totalOffset ,
94142 )
95143
96- if (meshRef .value .geometry ) { meshRef .value .geometry .dispose () }
97144 meshRef .value .geometry = geometry
98145
146+ createHelper ()
147+
99148 meshRef .value .position .set (0 , 0 , 0 )
100149 meshRef .value .rotation .set (0 , 0 , 0 )
101150 meshRef .value .scale .set (1 , 1 , 1 )
102151 meshRef .value .renderOrder = decal .value .zIndex ?? 0
103-
104- updateRaycastClone ()
105152}
106153
107154watch (() => decal .value .zIndex , () => {
108155 buildGeometry ()
109156})
110157
111- watch (isSelected , (selected ) => {
112- if (! meshRef .value ) { return }
113- const material = meshRef .value .material as any
114-
115- if (! selected ) {
116- material .opacity = 1
117- material .needsUpdate = true
118- }
119- })
120-
121- watch (isSelected , (selected ) => {
158+ watch ([isSelected , isHovered ], () => {
159+ updateHelperVisuals ()
122160 if (! meshRef .value ) { return }
123- if (! selected ) { meshRef .value .scale .copy (originalScale ) }
124- })
125-
126- const { onBeforeRender } = useLoop ()
127-
128- onBeforeRender (({ delta }) => {
129- if (raycastMesh .value && parent .value ) {
130- parent .value .updateMatrixWorld ()
131-
132- parent .value .matrixWorld .decompose (
133- raycastMesh .value .position ,
134- raycastMesh .value .quaternion ,
135- raycastMesh .value .scale ,
136- )
137-
138- raycastMesh .value .updateMatrixWorld ()
139- }
140-
141- if (! meshRef .value ) { return }
142- const material = meshRef .value .material as any
143-
144- if (isSelected .value && ! wasSelected .value ) {
145- localElapsed = 0
146- wasSelected .value = true
147- }
148-
149- if (! isSelected .value && wasSelected .value ) {
150- localElapsed = 0
151- wasSelected .value = false
152- }
153-
154- localElapsed += delta
155-
156- if (isSelected .value || isHovered .value ) {
157- const speed = 5
158- const minO = 0.2
159- const maxO = 1
160- const t = (Math .sin (localElapsed * speed ) + 1 ) / 2
161- const opacity = minO + t * (maxO - minO )
162- material .opacity = opacity
163- material .needsUpdate = true
164- }
161+ if (! isSelected .value ) { meshRef .value .scale .copy (originalScale ) }
165162})
166163
167164watch (
@@ -174,15 +171,13 @@ watch(
174171
175172watch ([meshRef , () => props .groupTest ], () => {
176173 if (! meshRef .value || ! props .groupTest ) { return }
177-
178174 buildGeometry ()
179175 originalScale .copy (meshRef .value .scale )
180176}, { immediate: true })
181177
182178const stop = decalBus .on ((payload ) => {
183179 if (payload .type === ' refresh-raycasts' ) {
184180 buildGeometry ()
185- updateRaycastClone ()
186181 }
187182})
188183
@@ -203,24 +198,28 @@ const onClickMesh = (event: MouseEvent) => {
203198}
204199
205200const onPointerEnter = (event : PointerEvent ) => {
206- console .log (' pointer enter decal item' )
207- event .stopPropagation ()
201+ event .stopPropagation () // Empêche le raycaster de traverser vers le parent
208202 isHovered .value = true
203+ document .body .style .cursor = ' pointer'
204+
205+ // PRÉVENIR LE PARENT
206+ emit (' hover' , true )
209207}
210208
211209const onPointerLeave = (event : PointerEvent ) => {
212- console . log ( ' pointer leave decal item ' )
210+ // event.stopPropagation() // Pas nécessaire ici
213211 isHovered .value = false
212+ document .body .style .cursor = ' auto'
213+
214+ // PRÉVENIR LE PARENT
215+ emit (' hover' , false )
214216}
215217
216218onBeforeUnmount (() => {
217- if (raycastMesh .value && props .groupTest ) {
218- props .groupTest .remove (raycastMesh .value )
219-
220- if (raycastMesh .value .geometry ) { raycastMesh .value .geometry .dispose () }
221- if (raycastMesh .value .material ) { (raycastMesh .value .material as any ).dispose () }
219+ clearHelper ()
222220
223- raycastMesh .value = null
221+ if (meshRef .value && meshRef .value .geometry ) {
222+ meshRef .value .geometry .dispose ()
224223 }
225224
226225 stop ()
@@ -239,7 +238,6 @@ defineExpose({ meshRef })
239238 :material-polygonOffsetFactor =" - (10 + (decal .zIndex ?? 0 ))"
240239 :material-depthTest =" true "
241240 :material-map =" map "
242- :material-opacity =" isSelected ? 0.5 : 1 "
243241 @click =" onClickMesh "
244242 @pointerenter =" onPointerEnter "
245243 @pointerleave =" onPointerLeave "
0 commit comments