Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions editor/src/editor/layout/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -399,7 +399,7 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
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);
Expand Down Expand Up @@ -427,7 +427,7 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
node.uniqueId = UniqueNumber.Get();

if (parent && isNode(node)) {
node.parent = parent;
this._setParentPreservingWorldTransform(node, parent);
}

if (isAbstractMesh(node)) {
Expand Down Expand Up @@ -899,4 +899,79 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>

this.refresh();
}

private _setParentPreservingWorldTransform(node: Node, newParent: Node | null): void {
// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should manage for lights and cameras too, that they don't extend TransformNode

Copy link
Contributor Author

@yuripourre yuripourre Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's good suggestion. I used Copilot, I will properly test is during the weekend


// Restore the world transform
node.position.copyFrom(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.rotationQuaternion.copyFrom(localRotation);
} else {
node.rotation.copyFrom(localRotation.toEulerAngles());
}

node.scaling.copyFrom(worldScaling);
return;
}

// 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;
}

// For other node types, just set the parent directly
node.parent = newParent;
}
}