Skip to content

Commit 9fd3614

Browse files
committed
fix: FreeGizmo behaviour
1 parent cee4a98 commit 9fd3614

4 files changed

Lines changed: 160 additions & 80 deletions

File tree

packages/@dcl/inspector/src/lib/babylon/decentraland/GizmoManager.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export function createGizmoManager(context: SceneContext) {
221221
if (!node.rotationQuaternion) {
222222
node.rotationQuaternion = Quaternion.Identity()
223223
}
224+
// Don't reset rotation if it already exists - let the gizmo transformers handle rotation
224225

225226
node.computeWorldMatrix(true)
226227
gizmoManager.attachToNode(node)
@@ -274,7 +275,7 @@ export function createGizmoManager(context: SceneContext) {
274275

275276
const setupFreeTransformerChangeHandler = (transformer: IGizmoTransformer) => {
276277
transformer.onChange(() => {
277-
selectedEntities.forEach(updateEntityTransform)
278+
selectedEntities.forEach(updateEntityPosition)
278279
void context.operations.dispatch()
279280
})
280281
}
@@ -346,8 +347,6 @@ export function createGizmoManager(context: SceneContext) {
346347
return [GizmoType.POSITION, GizmoType.ROTATION, GizmoType.SCALE, GizmoType.FREE] as const
347348
},
348349
setGizmoType(type: GizmoType) {
349-
console.log('[GizmoManager] Setting gizmo type:', type)
350-
351350
// First clean up all observers
352351
cleanupAllGizmoObservers()
353352

@@ -366,6 +365,7 @@ export function createGizmoManager(context: SceneContext) {
366365
switch (type) {
367366
case GizmoType.POSITION: {
368367
currentTransformer = positionTransformer
368+
currentTransformer.setup()
369369
gizmoManager.positionGizmoEnabled = true
370370
if (gizmoManager.gizmos.positionGizmo) {
371371
const positionGizmo = gizmoManager.gizmos.positionGizmo
@@ -400,6 +400,7 @@ export function createGizmoManager(context: SceneContext) {
400400
}
401401
case GizmoType.ROTATION: {
402402
currentTransformer = rotationTransformer
403+
currentTransformer.setup()
403404
gizmoManager.rotationGizmoEnabled = true
404405
if (gizmoManager.gizmos.rotationGizmo) {
405406
const rotationGizmo = gizmoManager.gizmos.rotationGizmo
@@ -476,6 +477,7 @@ export function createGizmoManager(context: SceneContext) {
476477
}
477478
case GizmoType.SCALE: {
478479
currentTransformer = scaleTransformer
480+
currentTransformer.setup()
479481
gizmoManager.scaleGizmoEnabled = true
480482
if (gizmoManager.gizmos.scaleGizmo) {
481483
const scaleGizmo = gizmoManager.gizmos.scaleGizmo
@@ -508,14 +510,14 @@ export function createGizmoManager(context: SceneContext) {
508510
}
509511
break
510512
}
511-
case GizmoType.FREE:
513+
case GizmoType.FREE: {
512514
currentTransformer = freeTransformer
515+
currentTransformer.setup()
516+
const node = getGizmoNode()
517+
currentTransformer.onDragStart(selectedEntities, node)
518+
currentTransformer.update(selectedEntities, node)
513519
break
514-
}
515-
516-
// Setup the new transformer
517-
if (currentTransformer) {
518-
currentTransformer.setup()
520+
}
519521
}
520522
},
521523
isPositionGizmoWorldAligned() {
Lines changed: 147 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,188 @@
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'
210
import { Entity } from '@dcl/ecs'
311
import { EcsEntity } from '../EcsEntity'
412
import { IGizmoTransformer } from './types'
5-
import { TransformUtils } from './utils'
613

714
export 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
}

packages/@dcl/inspector/src/lib/babylon/decentraland/gizmos/PositionGizmo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Vector3, TransformNode, GizmoManager, Matrix, Quaternion } from '@babylonjs/core'
1+
import { Vector3, TransformNode, GizmoManager, Quaternion } from '@babylonjs/core'
22
import { Entity } from '@dcl/ecs'
33
import { EcsEntity } from '../EcsEntity'
44
import { IGizmoTransformer } from './types'

packages/@dcl/inspector/src/lib/babylon/decentraland/gizmos/ScaleGizmo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Vector3, TransformNode, GizmoManager, Quaternion, Matrix } from '@babylonjs/core'
1+
import { Vector3, TransformNode, GizmoManager, Quaternion } from '@babylonjs/core'
22
import { Entity } from '@dcl/ecs'
33
import { EcsEntity } from '../EcsEntity'
44
import { IGizmoTransformer } from './types'

0 commit comments

Comments
 (0)