@@ -2,14 +2,16 @@ import { Vector3, TransformNode, GizmoManager, Quaternion, Matrix } from '@babyl
22import { Entity } from '@dcl/ecs'
33import { EcsEntity } from '../EcsEntity'
44import { IGizmoTransformer } from './types'
5- import { TransformUtils } from './utils'
65
76export class ScaleGizmo implements IGizmoTransformer {
87 private initialOffsets = new Map < Entity , Vector3 > ( )
98 private initialScales = new Map < Entity , Vector3 > ( )
109 private initialRotations = new Map < Entity , Quaternion > ( )
10+ private initialPositions = new Map < Entity , Vector3 > ( )
11+ private pivotPosition : Vector3 | null = null
1112 private initialGizmoScale : Vector3 | null = null
1213 private changeHandlers : ( ( ) => void ) [ ] = [ ]
14+ private isDragging = false
1315
1416 constructor ( private gizmoManager : GizmoManager ) { }
1517
@@ -25,38 +27,52 @@ export class ScaleGizmo implements IGizmoTransformer {
2527 this . initialOffsets . clear ( )
2628 this . initialScales . clear ( )
2729 this . initialRotations . clear ( )
30+ this . initialPositions . clear ( )
2831 this . initialGizmoScale = null
32+ this . pivotPosition = null
33+ this . isDragging = false
2934 }
3035
3136 onDragStart ( entities : EcsEntity [ ] , gizmoNode : TransformNode ) : void {
32- const centroid = gizmoNode . position . clone ( )
37+ if ( this . isDragging ) return
38+
39+ this . isDragging = true
40+
41+ // Calculate pivot position (centroid of all selected entities)
42+ this . pivotPosition = new Vector3 ( )
43+ for ( const entity of entities ) {
44+ const worldPosition = entity . getAbsolutePosition ( )
45+ this . pivotPosition . addInPlace ( worldPosition )
46+ }
47+ this . pivotPosition . scaleInPlace ( 1 / entities . length )
48+
49+ // Store initial gizmo scale
50+ this . initialGizmoScale = gizmoNode . scaling . clone ( )
51+
52+ // Store initial state for all entities
3353 this . initialOffsets . clear ( )
3454 this . initialScales . clear ( )
3555 this . initialRotations . clear ( )
36- this . initialGizmoScale = gizmoNode . scaling . clone ( )
56+ this . initialPositions . clear ( )
3757
3858 for ( const entity of entities ) {
3959 const worldPosition = entity . getAbsolutePosition ( )
40- const offset = worldPosition . subtract ( centroid )
41- this . initialOffsets . set ( entity . entityId , offset )
4260
43- const worldRotation = TransformUtils . getWorldRotation ( entity )
44- this . initialRotations . set ( entity . entityId , worldRotation )
61+ // Store initial transforms
62+ this . initialPositions . set ( entity . entityId , entity . position . clone ( ) )
63+ this . initialScales . set ( entity . entityId , entity . scaling . clone ( ) )
64+ this . initialRotations . set ( entity . entityId , entity . rotationQuaternion ?. clone ( ) || Quaternion . Identity ( ) )
4565
46- const parent = entity . parent instanceof TransformNode ? entity . parent : null
47- const parentScale = TransformUtils . getParentWorldScale ( parent )
48- const worldScale = new Vector3 (
49- entity . scaling . x * parentScale . x ,
50- entity . scaling . y * parentScale . y ,
51- entity . scaling . z * parentScale . z
52- )
53- this . initialScales . set ( entity . entityId , worldScale )
66+ // Store offset from pivot (for proportional scaling)
67+ const offset = worldPosition . subtract ( this . pivotPosition )
68+ this . initialOffsets . set ( entity . entityId , offset )
5469 }
5570 }
5671
5772 update ( entities : EcsEntity [ ] , gizmoNode : TransformNode ) : void {
58- if ( ! this . initialGizmoScale ) return
73+ if ( ! this . isDragging || ! this . initialGizmoScale || ! this . pivotPosition ) return
5974
75+ // Calculate scale change from gizmo
6076 const scaleChange = new Vector3 (
6177 gizmoNode . scaling . x / this . initialGizmoScale . x ,
6278 gizmoNode . scaling . y / this . initialGizmoScale . y ,
@@ -67,11 +83,15 @@ export class ScaleGizmo implements IGizmoTransformer {
6783 const offset = this . initialOffsets . get ( entity . entityId )
6884 const initialScale = this . initialScales . get ( entity . entityId )
6985 const initialRotation = this . initialRotations . get ( entity . entityId )
70- if ( ! offset || ! initialScale || ! initialRotation ) continue
86+ const initialPosition = this . initialPositions . get ( entity . entityId )
87+
88+ if ( ! offset || ! initialScale || ! initialRotation || ! initialPosition ) continue
7189
90+ // Scale the offset proportionally (like Blender's proportional scaling)
7291 const scaledOffset = new Vector3 ( offset . x * scaleChange . x , offset . y * scaleChange . y , offset . z * scaleChange . z )
73- const newWorldPosition = gizmoNode . position . add ( scaledOffset )
92+ const newWorldPosition = this . pivotPosition . add ( scaledOffset )
7493
94+ // Scale the entity's scale
7595 const newWorldScale = new Vector3 (
7696 initialScale . x * scaleChange . x ,
7797 initialScale . y * scaleChange . y ,
@@ -81,50 +101,35 @@ export class ScaleGizmo implements IGizmoTransformer {
81101 const parent = entity . parent instanceof TransformNode ? entity . parent : null
82102
83103 if ( parent ) {
104+ // For child entities, convert world transforms to local space
84105 const parentWorldMatrix = parent . getWorldMatrix ( )
85- const parentScale = TransformUtils . getParentWorldScale ( parent )
86- const parentRotation =
87- parent . rotationQuaternion ||
88- Quaternion . FromRotationMatrix (
89- Matrix . FromValues (
90- parentWorldMatrix . m [ 0 ] ,
91- parentWorldMatrix . m [ 1 ] ,
92- parentWorldMatrix . m [ 2 ] ,
93- 0 ,
94- parentWorldMatrix . m [ 4 ] ,
95- parentWorldMatrix . m [ 5 ] ,
96- parentWorldMatrix . m [ 6 ] ,
97- 0 ,
98- parentWorldMatrix . m [ 8 ] ,
99- parentWorldMatrix . m [ 9 ] ,
100- parentWorldMatrix . m [ 10 ] ,
101- 0 ,
102- 0 ,
103- 0 ,
104- 0 ,
105- 1
106- )
107- )
108-
109- const localPosition = TransformUtils . convertToLocalPosition ( newWorldPosition , parent )
110- entity . position . copyFrom ( localPosition )
106+ const parentWorldMatrixInverse = parentWorldMatrix . clone ( ) . invert ( )
107+ const parentWorldRotation = parent . rotationQuaternion || Quaternion . FromRotationMatrix ( parentWorldMatrix )
111108
112- entity . scaling . set (
113- newWorldScale . x / parentScale . x ,
114- newWorldScale . y / parentScale . y ,
115- newWorldScale . z / parentScale . z
116- )
109+ // Convert world position to local space
110+ const localPosition = Vector3 . TransformCoordinates ( newWorldPosition , parentWorldMatrixInverse )
117111
118- const scaleCompensationMatrix = Matrix . Scaling ( 1 / parentScale . x , 1 / parentScale . y , 1 / parentScale . z )
119- const compensatedRotation = initialRotation . multiply ( Quaternion . FromRotationMatrix ( scaleCompensationMatrix ) )
120- const localRotation = parentRotation . invert ( ) . multiply ( compensatedRotation )
112+ // Convert world rotation to local space
113+ const localRotation = parentWorldRotation . invert ( ) . multiply ( initialRotation )
121114
115+ // Apply scale directly to the child without considering parent's scale
116+ // This maintains the local scale as intended by the user
117+ const localScale = new Vector3 (
118+ initialScale . x * scaleChange . x ,
119+ initialScale . y * scaleChange . y ,
120+ initialScale . z * scaleChange . z
121+ )
122+
123+ // Apply transforms
124+ entity . position . copyFrom ( localPosition )
125+ entity . scaling . copyFrom ( localScale )
122126 if ( ! entity . rotationQuaternion ) {
123127 entity . rotationQuaternion = new Quaternion ( )
124128 }
125129 entity . rotationQuaternion . copyFrom ( localRotation )
126130 entity . rotationQuaternion . normalize ( )
127131 } else {
132+ // For entities without parent, apply world transforms directly
128133 entity . position . copyFrom ( newWorldPosition )
129134 entity . scaling . copyFrom ( newWorldScale )
130135 if ( ! entity . rotationQuaternion ) {
@@ -134,15 +139,22 @@ export class ScaleGizmo implements IGizmoTransformer {
134139 entity . rotationQuaternion . normalize ( )
135140 }
136141
137- this . changeHandlers . forEach ( ( handler ) => handler ( ) )
142+ // Force update world matrix
143+ entity . computeWorldMatrix ( true )
138144 }
145+
146+ // Notify change handlers
147+ this . changeHandlers . forEach ( ( handler ) => handler ( ) )
139148 }
140149
141150 onDragEnd ( ) : void {
151+ this . isDragging = false
152+ this . initialGizmoScale = null
153+ this . pivotPosition = null
142154 this . initialOffsets . clear ( )
143155 this . initialScales . clear ( )
144156 this . initialRotations . clear ( )
145- this . initialGizmoScale = null
157+ this . initialPositions . clear ( )
146158 }
147159
148160 onChange ( callback : ( ) => void ) : ( ) => void {
0 commit comments