From 2e13582ae89e1bf892b27b8a4e81fd0e6c936f81 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 30 Aug 2025 10:41:28 -0700 Subject: [PATCH 1/3] fix: Preserve world transform on graph parenting --- editor/src/editor/layout/graph.tsx | 34 +++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 248ddf862..c48b7ee8f 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -14,7 +14,7 @@ import { IoCheckmark, IoSparklesSharp } from "react-icons/io5"; import { SiAdobeindesign, SiBabylondotjs } from "react-icons/si"; import { AdvancedDynamicTexture } from "babylonjs-gui"; -import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, ParticleSystem } from "babylonjs"; +import { BaseTexture, Node, Scene, Sound, Tools, TransformNode, IParticleSystem, ParticleSystem } from "babylonjs"; import { Editor } from "../main"; @@ -399,7 +399,7 @@ export class EditorGraph extends Component instance.rotation.copyFrom(object.rotation); instance.scaling.copyFrom(object.scaling); instance.rotationQuaternion = object.rotationQuaternion?.clone() ?? null; - instance.parent = object.parent; + this._setParentPreservingWorldTransform(instance, object.parent); const collisionMesh = getCollisionMeshFor(instance.sourceMesh); collisionMesh?.updateInstances(instance.sourceMesh); @@ -427,7 +427,7 @@ export class EditorGraph extends Component node.uniqueId = UniqueNumber.Get(); if (parent && isNode(node)) { - node.parent = parent; + this._setParentPreservingWorldTransform(node, parent); } if (isAbstractMesh(node)) { @@ -899,4 +899,32 @@ export class EditorGraph extends Component this.refresh(); } + + private _setParentPreservingWorldTransform(node: Node, newParent: Node | null): void { + if (!(node instanceof TransformNode)) { + // For non-transform nodes, just set the parent directly + node.parent = newParent; + return; + } + + // Store the current world transform + const worldPosition = node.getAbsolutePosition(); + const worldRotation = node.rotationQuaternion || node.rotation.toQuaternion(); + const worldScaling = node.absoluteScaling; + + // Set the new parent + node.parent = newParent; + + // Restore the world transform + node.setAbsolutePosition(worldPosition); + + if (node.rotationQuaternion) { + node.absoluteRotationQuaternion.copyFrom(worldRotation); + } else { + const rotationVector = worldRotation.toEulerAngles(); + node.rotation.copyFrom(rotationVector); + } + + node.scaling.copyFrom(worldScaling); + } } From 39a5c67d781532bc2e66f0b3e54d9ff8029a9689 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Thu, 23 Oct 2025 16:24:54 -0700 Subject: [PATCH 2/3] Avoid using absoluteRotation --- editor/src/editor/layout/graph.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index c48b7ee8f..61efb5ed5 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -910,7 +910,7 @@ export class EditorGraph extends Component // Store the current world transform const worldPosition = node.getAbsolutePosition(); const worldRotation = node.rotationQuaternion || node.rotation.toQuaternion(); - const worldScaling = node.absoluteScaling; + const worldScaling = node.absoluteScaling.clone(); // Set the new parent node.parent = newParent; @@ -918,11 +918,17 @@ export class EditorGraph extends Component // Restore the world transform node.setAbsolutePosition(worldPosition); + // Compute the local rotation based on the parent's rotation + let localRotation = worldRotation; + if (newParent instanceof TransformNode) { + const parentRotation = newParent.absoluteRotationQuaternion; + localRotation = parentRotation.conjugate().multiply(worldRotation); + } + if (node.rotationQuaternion) { - node.absoluteRotationQuaternion.copyFrom(worldRotation); + node.rotationQuaternion.copyFrom(localRotation); } else { - const rotationVector = worldRotation.toEulerAngles(); - node.rotation.copyFrom(rotationVector); + node.rotation.copyFrom(localRotation.toEulerAngles()); } node.scaling.copyFrom(worldScaling); From ba773a5ee48a59fd8be71cd2d11bea5850fd8806 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Thu, 23 Oct 2025 16:51:16 -0700 Subject: [PATCH 3/3] Handle Cameras and Lights --- editor/src/editor/layout/graph.tsx | 85 ++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/editor/src/editor/layout/graph.tsx b/editor/src/editor/layout/graph.tsx index 61efb5ed5..72849b65c 100644 --- a/editor/src/editor/layout/graph.tsx +++ b/editor/src/editor/layout/graph.tsx @@ -901,36 +901,77 @@ export class EditorGraph extends Component } private _setParentPreservingWorldTransform(node: Node, newParent: Node | null): void { - if (!(node instanceof TransformNode)) { - // For non-transform nodes, just set the parent directly + // TransformNodes (including Meshes) support full world transform preservation + if (node instanceof TransformNode) { + // Store the current world transform + const worldPosition = node.getAbsolutePosition(); + const worldRotation = node.rotationQuaternion || node.rotation.toQuaternion(); + const worldScaling = node.absoluteScaling.clone(); + + // Set the new parent node.parent = newParent; - return; - } - // Store the current world transform - const worldPosition = node.getAbsolutePosition(); - const worldRotation = node.rotationQuaternion || node.rotation.toQuaternion(); - const worldScaling = node.absoluteScaling.clone(); + // Restore the world transform + node.position.copyFrom(worldPosition); - // Set the new parent - node.parent = newParent; + // Compute the local rotation based on the parent's rotation + let localRotation = worldRotation; + if (newParent instanceof TransformNode) { + const parentRotation = newParent.absoluteRotationQuaternion; + localRotation = parentRotation.conjugate().multiply(worldRotation); + } - // Restore the world transform - node.setAbsolutePosition(worldPosition); + if (node.rotationQuaternion) { + node.rotationQuaternion.copyFrom(localRotation); + } else { + node.rotation.copyFrom(localRotation.toEulerAngles()); + } - // Compute the local rotation based on the parent's rotation - let localRotation = worldRotation; - if (newParent instanceof TransformNode) { - const parentRotation = newParent.absoluteRotationQuaternion; - localRotation = parentRotation.conjugate().multiply(worldRotation); + node.scaling.copyFrom(worldScaling); + return; } - if (node.rotationQuaternion) { - node.rotationQuaternion.copyFrom(localRotation); - } else { - node.rotation.copyFrom(localRotation.toEulerAngles()); + // Cameras and Lights have position and rotation but not the full transform methods + if (isCamera(node) || isLight(node)) { + // Store current world position and rotation + const worldPosition = (node as any).position.clone(); + const worldRotation = (node as any).rotationQuaternion || (node as any).rotation.toQuaternion(); + + // Set the new parent + node.parent = newParent; + + // For cameras and lights, we need to compute local position manually + if (newParent instanceof TransformNode) { + // Compute local position from world position + const localPosition = worldPosition.subtract(newParent.getAbsolutePosition()); + const parentRotationInverse = newParent.absoluteRotationQuaternion.conjugate(); + localPosition.applyRotationQuaternionInPlace(parentRotationInverse); + localPosition.divideInPlace(newParent.absoluteScaling); + (node as any).position.copyFrom(localPosition); + + // Compute local rotation + const parentRotation = newParent.absoluteRotationQuaternion; + const localRotation = parentRotation.conjugate().multiply(worldRotation); + + if ((node as any).rotationQuaternion) { + (node as any).rotationQuaternion.copyFrom(localRotation); + } else { + (node as any).rotation.copyFrom(localRotation.toEulerAngles()); + } + } else { + // No parent, world position/rotation equals local position/rotation + (node as any).position.copyFrom(worldPosition); + + if ((node as any).rotationQuaternion) { + (node as any).rotationQuaternion.copyFrom(worldRotation); + } else { + (node as any).rotation.copyFrom(worldRotation.toEulerAngles()); + } + } + return; } - node.scaling.copyFrom(worldScaling); + // For other node types, just set the parent directly + node.parent = newParent; } }