@@ -158,41 +158,156 @@ export function createGizmoManager(context: SceneContext) {
158158 }
159159 }
160160
161- function updateEntityTransform ( entity : Entity , newTransform : TransformType ) {
162- const { position, scale, rotation, parent } = newTransform
163- context . operations . updateValue ( context . Transform , entity , {
164- position : DclVector3 . create ( position . x , position . y , position . z ) ,
165- rotation : DclQuaternion . create ( rotation . x , rotation . y , rotation . z , rotation . w ) ,
166- scale : DclVector3 . create ( scale . x , scale . y , scale . z ) ,
167- parent
168- } )
169- }
170-
171161 /**
172162 * Updates the transform of all selected entities after a gizmo operation
173163 *
174- * 1. Fixes rotation gizmo alignment based on the first selected entity's transform
175- * 2. For each selected entity:
176- * - Gets the original parent and resolves it to a valid entity or root node
177- * - Temporarily sets the entity's parent to handle transform calculations
178- * - Updates the entity's transform:
179- * - If parent is root node: Uses world space transform with snapping
180- * - Otherwise: Uses local space transform
181- * - Preserves the original parent relationship
182- * 3. Dispatches the transform updates to persist changes
164+ * This function correctly applies gizmo transformations while maintaining
165+ * proper parent-child relationships and preventing scale distortion.
183166 */
184167 function updateTransform ( ) {
185- fixRotationGizmoAlignment ( getTransform ( getFirstEntity ( ) ) )
168+ const firstEntity = getFirstEntity ( )
169+ if ( ! firstEntity ) return
170+
171+ fixRotationGizmoAlignment ( getTransform ( firstEntity ) )
172+
173+ // Get the gizmo dummy node
174+ const dummyNode = getDummyNode ( )
175+
176+ // Store which gizmo is active
177+ const { positionGizmoEnabled, rotationGizmoEnabled, scaleGizmoEnabled } = gizmoManager
178+
179+ // First measure the initial offsets when position gizmo is active
180+ // This allows us to maintain relative positions of multiple selected entities
181+ const initialOffsets = new Map < Entity , Vector3 > ( )
182+ const initialDummyPos = dummyNode . position . clone ( )
183+
184+ if ( positionGizmoEnabled && selectedEntities . length > 1 ) {
185+ // Calculate relative offsets from the dummy node for each entity
186+ for ( const entity of selectedEntities ) {
187+ const entityWorldPos = new Vector3 ( )
188+ const entityRot = new Quaternion ( )
189+ const entityScale = new Vector3 ( )
190+ entity . computeWorldMatrix ( true ) . decompose ( entityScale , entityRot , entityWorldPos )
191+
192+ // Store the offset from dummy node center
193+ initialOffsets . set ( entity . entityId , entityWorldPos . subtract ( initialDummyPos ) )
194+ }
195+ }
196+
197+ // Process each selected entity
186198 for ( const entity of selectedEntities ) {
187199 const originalParent = getParent ( entity )
188- const parent = context . getEntityOrNull ( originalParent ?? context . rootNode . entityId )
200+ const parentEntity = context . getEntityOrNull ( originalParent ?? context . rootNode . entityId )
201+ if ( ! parentEntity ) continue
202+
203+ // Get the entity's original transform from ECS
204+ const originalTransform = entity . ecsComponentValues . transform
205+ if ( ! originalTransform ) continue
206+
207+ // Create new transform object with original values
208+ const newTransform = {
209+ position : DclVector3 . create (
210+ originalTransform . position . x ,
211+ originalTransform . position . y ,
212+ originalTransform . position . z
213+ ) ,
214+ rotation : DclQuaternion . create (
215+ originalTransform . rotation . x ,
216+ originalTransform . rotation . y ,
217+ originalTransform . rotation . z ,
218+ originalTransform . rotation . w
219+ ) ,
220+ scale : DclVector3 . create ( originalTransform . scale . x , originalTransform . scale . y , originalTransform . scale . z ) ,
221+ parent : originalParent
222+ }
189223
190- entity . setParent ( parent )
191- const transform = parent === context . rootNode ? computeWorldTransform ( entity ) : getTransform ( entity )
224+ if ( positionGizmoEnabled ) {
225+ // Calculate the final world position including offsets if multiple entities are selected
226+ let finalWorldPos : Vector3
227+
228+ if ( selectedEntities . length > 1 && initialOffsets . has ( entity . entityId ) ) {
229+ // For multiple entities: position = dummyNode.position + entity's offset from dummy
230+ const offset = initialOffsets . get ( entity . entityId ) !
231+ finalWorldPos = dummyNode . position . add ( offset )
232+ } else {
233+ // For single entity: use the entity's exact world position
234+ const entityWorldMatrix = entity . computeWorldMatrix ( true )
235+ finalWorldPos = new Vector3 ( )
236+ const entityWorldRot = new Quaternion ( )
237+ const entityWorldScale = new Vector3 ( )
238+ entityWorldMatrix . decompose ( entityWorldScale , entityWorldRot , finalWorldPos )
239+ }
240+
241+ // Calculate parent's world matrix
242+ const parentWorldMatrix = parentEntity . computeWorldMatrix ( true )
243+ const invParentWorld = parentWorldMatrix . clone ( )
244+ invParentWorld . invert ( )
245+
246+ // Convert world position to local position in parent's space
247+ const localPos = Vector3 . TransformCoordinates ( finalWorldPos , invParentWorld )
248+
249+ // Apply snapping
250+ const snappedLocalPos = snapPosition ( localPos )
251+
252+ // Update the position in the transform
253+ newTransform . position = DclVector3 . create ( snappedLocalPos . x , snappedLocalPos . y , snappedLocalPos . z )
254+ } else if ( rotationGizmoEnabled ) {
255+ // For rotation, we need a more accurate approach to handle parent-child rotations
256+
257+ // Get the current entity's world position and world rotation after gizmo operation
258+ const entityWorldMatrix = entity . computeWorldMatrix ( true )
259+ const entityWorldRotation = new Quaternion ( )
260+ const entityWorldScale = new Vector3 ( )
261+ const entityWorldPos = new Vector3 ( )
262+ entityWorldMatrix . decompose ( entityWorldScale , entityWorldRotation , entityWorldPos )
263+
264+ // Get parent's world matrix to calculate local rotation
265+ const parentWorldMatrix = parentEntity . computeWorldMatrix ( true )
266+ const invParentMatrix = parentWorldMatrix . clone ( )
267+ invParentMatrix . invert ( )
268+
269+ // Calculate local rotation by transforming world rotation to local space
270+ const invParentRotation = new Quaternion ( )
271+ const parentScale = new Vector3 ( )
272+ const parentPos = new Vector3 ( )
273+ invParentMatrix . decompose ( parentScale , invParentRotation , parentPos )
274+
275+ // To get local rotation, multiply world rotation by inverse parent rotation
276+ // This is equivalent to: worldRot = parentRot * localRot, so localRot = parentRot^-1 * worldRot
277+ const localRotation = invParentRotation . multiply ( entityWorldRotation )
278+
279+ // Apply snapping and update
280+ const snappedLocalRot = snapRotation ( localRotation )
281+ newTransform . rotation = DclQuaternion . create (
282+ snappedLocalRot . x ,
283+ snappedLocalRot . y ,
284+ snappedLocalRot . z ,
285+ snappedLocalRot . w
286+ )
287+ } else if ( scaleGizmoEnabled ) {
288+ // Calculate dummy node world matrix
289+ const dummyWorldMatrix = dummyNode . computeWorldMatrix ( true )
290+
291+ // Extract entity's current scale from the world matrix
292+ const worldScale = new Vector3 ( )
293+ const worldRot = new Quaternion ( )
294+ const worldPos = new Vector3 ( )
295+ dummyWorldMatrix . decompose ( worldScale , worldRot , worldPos )
296+
297+ // Apply snapping and update
298+ const snappedScale = snapScale ( worldScale )
299+ newTransform . scale = DclVector3 . create ( snappedScale . x , snappedScale . y , snappedScale . z )
300+ }
192301
193- updateEntityTransform ( entity . entityId , { ...transform , parent : originalParent } )
302+ // Apply the transform update
303+ context . operations . updateValue ( context . Transform , entity . entityId , newTransform )
194304 }
195305
306+ // Make sure the entities are reparented properly
307+ restoreParents ( )
308+ repositionGizmoOnCentroid ( )
309+
310+ // Dispatch all transform updates
196311 void context . operations . dispatch ( )
197312 }
198313
0 commit comments