1- import { Vector3 , TransformNode , Scene , UtilityLayerRenderer , PointerDragBehavior , AbstractMesh } from '@babylonjs/core'
1+ import {
2+ Vector3 ,
3+ TransformNode ,
4+ Scene ,
5+ UtilityLayerRenderer ,
6+ PointerDragBehavior ,
7+ PickingInfo ,
8+ AbstractMesh
9+ } from '@babylonjs/core'
210import { Entity } from '@dcl/ecs'
311import { EcsEntity } from '../EcsEntity'
412import { IGizmoTransformer } from './types'
5- import { TransformUtils } from './utils'
613
714export class FreeGizmo implements IGizmoTransformer {
8- private dragBehavior : PointerDragBehavior | null = null
9- private isDragging = false
1015 private changeHandlers : ( ( ) => void ) [ ] = [ ]
11- private initialStates = new Map <
12- Entity ,
13- {
14- worldPosition : Vector3
15- offset : Vector3
16- }
17- > ( )
16+ private selectedEntities : EcsEntity [ ] = [ ]
17+ private gizmoNode : TransformNode | null = null
18+ private dragBehavior : PointerDragBehavior
19+ private pivotPosition : Vector3 | null = null
20+ private entityOffsets = new Map < Entity , Vector3 > ( )
21+ private isDragging = false
1822
19- constructor ( private scene : Scene , private utilityLayer : UtilityLayerRenderer = new UtilityLayerRenderer ( scene ) ) { }
23+ constructor ( private scene : Scene , private utilityLayer : UtilityLayerRenderer = new UtilityLayerRenderer ( scene ) ) {
24+ this . dragBehavior = new PointerDragBehavior ( { dragPlaneNormal : new Vector3 ( 0 , 1 , 0 ) } )
25+ this . dragBehavior . useObjectOrientationForDragging = false
26+ this . setupDragObservers ( )
27+ }
2028
2129 setup ( ) : void {
2230 this . cleanup ( )
31+ this . setupSceneObservers ( )
2332 }
2433
2534 cleanup ( ) : void {
35+ this . removeSceneObservers ( )
2636 this . detachDragBehavior ( )
27- this . initialStates . clear ( )
37+ this . selectedEntities = [ ]
38+ this . gizmoNode = null
39+ this . pivotPosition = null
40+ this . entityOffsets . clear ( )
2841 this . isDragging = false
2942 }
3043
31- private detachDragBehavior ( ) {
32- if ( this . dragBehavior && this . dragBehavior . attachedNode ) {
33- this . dragBehavior . detach ( )
34- this . dragBehavior = null
44+ private setupSceneObservers ( ) : void {
45+ this . scene . onPointerDown = ( _event , pickResult ) => {
46+ if ( ! pickResult . pickedMesh || this . selectedEntities . length === 0 ) return
47+ const clickedEntity = this . findClickedEntity ( pickResult . pickedMesh )
48+ if ( ! clickedEntity ) return
49+ this . startDrag ( clickedEntity , pickResult . pickedMesh )
3550 }
36- }
37-
38- onDragStart ( entities : EcsEntity [ ] , gizmoNode : TransformNode ) : void {
39- if ( ! entities . length ) return
40-
41- const firstEntity = entities [ 0 ]
42-
43- // Simple drag behavior in global XZ plane
44- this . dragBehavior = new PointerDragBehavior ( { dragPlaneNormal : Vector3 . Up ( ) } )
45- this . dragBehavior . useObjectOrientationForDragging = false
46-
47- // Store initial states
48- this . initialStates . clear ( )
49- const centroid = firstEntity . getAbsolutePosition ( ) . clone ( )
50-
51- for ( const entity of entities ) {
52- const worldPosition = entity . getAbsolutePosition ( )
53- const offset = worldPosition . subtract ( centroid )
54- this . initialStates . set ( entity . entityId , { worldPosition, offset } )
51+ this . scene . onPointerUp = ( ) => {
52+ if ( this . isDragging ) {
53+ this . endDrag ( )
54+ }
5555 }
56+ }
5657
57- // Attach behavior to first entity
58- this . dragBehavior . attach ( firstEntity as unknown as AbstractMesh )
58+ private removeSceneObservers ( ) : void {
59+ this . scene . onPointerDown = ( ) => { }
60+ this . scene . onPointerUp = ( ) => { }
61+ }
5962
63+ private setupDragObservers ( ) : void {
6064 this . dragBehavior . onDragStartObservable . add ( ( ) => {
6165 this . isDragging = true
6266 this . notifyChange ( )
6367 } )
64-
6568 this . dragBehavior . onDragObservable . add ( ( eventData ) => {
66- if ( ! this . isDragging || ! eventData . delta ) return
67-
68- // Move first entity
69- const delta = eventData . delta . clone ( )
70- delta . y = 0 // Lock Y movement
71- firstEntity . position . addInPlace ( delta )
72-
73- // Move other entities relative to first
74- for ( const entity of entities ) {
75- if ( entity === firstEntity ) continue
76-
77- const state = this . initialStates . get ( entity . entityId )
78- if ( ! state ) continue
79-
80- const newWorldPosition = firstEntity . getAbsolutePosition ( ) . add ( state . offset )
81- const parent = entity . parent instanceof TransformNode ? entity . parent : null
82- const localPosition = TransformUtils . convertToLocalPosition ( newWorldPosition , parent )
83- localPosition . y = state . worldPosition . y // Preserve original Y
84- entity . position . copyFrom ( localPosition )
69+ if ( ! this . isDragging || ! eventData . delta || ! this . pivotPosition ) return
70+ const worldDelta = eventData . delta . clone ( )
71+ worldDelta . y = 0
72+ this . pivotPosition . addInPlace ( worldDelta )
73+ for ( const entity of this . selectedEntities ) {
74+ const offset = this . entityOffsets . get ( entity . entityId )
75+ if ( ! offset ) continue
76+ const newWorldPosition = this . pivotPosition . add ( offset )
77+ this . applyWorldPositionToEntity ( entity , newWorldPosition )
8578 }
86-
87- this . notifyChange ( )
8879 } )
89-
9080 this . dragBehavior . onDragEndObservable . add ( ( ) => {
9181 this . isDragging = false
82+ this . detachDragBehavior ( )
9283 this . notifyChange ( )
9384 } )
9485 }
9586
96- private notifyChange ( ) {
97- for ( const handler of this . changeHandlers ) {
98- handler ( )
87+ private detachDragBehavior ( ) : void {
88+ this . dragBehavior . detach ( )
89+ }
90+
91+ private findClickedEntity ( pickedMesh : AbstractMesh ) : EcsEntity | null {
92+ return (
93+ this . selectedEntities . find ( ( entity ) => {
94+ const isDescendant = pickedMesh . isDescendantOf ( entity )
95+ const isMeshRenderer = entity . meshRenderer === pickedMesh
96+ const isGltfContainer = entity . gltfContainer === pickedMesh
97+ return isDescendant || isMeshRenderer || isGltfContainer
98+ } ) || null
99+ )
100+ }
101+
102+ private startDrag ( clickedEntity : EcsEntity , pickedMesh : AbstractMesh ) : void {
103+ // Calculate pivot (centroid)
104+ this . pivotPosition = new Vector3 ( )
105+ for ( const entity of this . selectedEntities ) {
106+ this . pivotPosition . addInPlace ( entity . getAbsolutePosition ( ) )
107+ }
108+ this . pivotPosition . scaleInPlace ( 1 / this . selectedEntities . length )
109+ // Store offsets
110+ this . entityOffsets . clear ( )
111+ for ( const entity of this . selectedEntities ) {
112+ const offset = entity . getAbsolutePosition ( ) . subtract ( this . pivotPosition )
113+ this . entityOffsets . set ( entity . entityId , offset )
114+ }
115+ // Attach drag behavior to the picked mesh
116+ this . dragBehavior . attach ( pickedMesh )
117+ }
118+
119+ private applyWorldPositionToEntity ( entity : EcsEntity , worldPosition : Vector3 ) : void {
120+ const parent = entity . parent instanceof TransformNode ? entity . parent : null
121+ if ( parent ) {
122+ const parentWorldMatrix = parent . getWorldMatrix ( )
123+ const parentWorldMatrixInverse = parentWorldMatrix . invert ( )
124+ const localPosition = Vector3 . TransformCoordinates ( worldPosition , parentWorldMatrixInverse )
125+ entity . position . copyFrom ( localPosition )
126+ } else {
127+ entity . position . copyFrom ( worldPosition )
128+ }
129+ entity . computeWorldMatrix ( true )
130+ // Only call refreshBoundingInfo if it exists
131+ if ( typeof ( entity as any ) . refreshBoundingInfo === 'function' ) {
132+ ; ( entity as any ) . refreshBoundingInfo ( )
133+ }
134+ if ( typeof entity . getChildMeshes === 'function' ) {
135+ entity . getChildMeshes ( ) . forEach ( ( mesh ) => {
136+ if ( typeof mesh . refreshBoundingInfo === 'function' ) {
137+ mesh . refreshBoundingInfo ( { } )
138+ }
139+ } )
140+ }
141+ if ( ( entity as any ) . boundingInfoMesh ) {
142+ const boundingInfoMesh = ( entity as any ) . boundingInfoMesh
143+ if ( typeof boundingInfoMesh . refreshBoundingInfo === 'function' ) {
144+ boundingInfoMesh . refreshBoundingInfo ( )
145+ }
146+ if ( typeof boundingInfoMesh . computeWorldMatrix === 'function' ) {
147+ boundingInfoMesh . computeWorldMatrix ( true )
148+ }
99149 }
100150 }
101151
102- onChange ( callback : ( ) => void ) {
152+ private endDrag ( ) : void {
153+ this . isDragging = false
154+ this . detachDragBehavior ( )
155+ this . pivotPosition = null
156+ this . entityOffsets . clear ( )
157+ this . notifyChange ( )
158+ }
159+
160+ onDragStart ( entities : EcsEntity [ ] , gizmoNode : TransformNode ) : void {
161+ this . selectedEntities = entities
162+ this . gizmoNode = gizmoNode
163+ this . pivotPosition = null
164+ this . entityOffsets . clear ( )
165+ this . detachDragBehavior ( )
166+ }
167+
168+ onChange ( callback : ( ) => void ) : ( ) => void {
103169 this . changeHandlers . push ( callback )
104170 return ( ) => {
105171 const index = this . changeHandlers . indexOf ( callback )
106- if ( index !== - 1 ) {
107- this . changeHandlers . splice ( index , 1 )
108- }
172+ if ( index !== - 1 ) this . changeHandlers . splice ( index , 1 )
109173 }
110174 }
111175
112176 update ( entities : EcsEntity [ ] , gizmoNode : TransformNode ) : void {
113- // no-op
177+ if ( entities !== this . selectedEntities ) {
178+ this . selectedEntities = entities
179+ this . pivotPosition = null
180+ this . entityOffsets . clear ( )
181+ this . detachDragBehavior ( )
182+ }
183+ if ( gizmoNode !== this . gizmoNode ) {
184+ this . gizmoNode = gizmoNode
185+ }
114186 }
115187
116188 onDragEnd ( ) : void {
@@ -121,4 +193,10 @@ export class FreeGizmo implements IGizmoTransformer {
121193 this . cleanup ( )
122194 this . utilityLayer . dispose ( )
123195 }
196+
197+ private notifyChange ( ) : void {
198+ for ( const handler of this . changeHandlers ) {
199+ handler ( )
200+ }
201+ }
124202}
0 commit comments