Skip to content

Commit 2d02252

Browse files
committed
feat: add support of copy/past transforms
1 parent f62de5b commit 2d02252

File tree

4 files changed

+149
-17
lines changed

4 files changed

+149
-17
lines changed

editor/src/editor/layout/graph.tsx

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { FaCamera, FaImage, FaLightbulb } from "react-icons/fa";
1515
import { SiAdobeindesign, SiBabylondotjs } from "react-icons/si";
1616

1717
import { AdvancedDynamicTexture } from "babylonjs-gui";
18-
import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, ParticleSystem, Sprite } from "babylonjs";
18+
import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, Sprite } from "babylonjs";
1919

2020
import { Editor } from "../main";
2121

@@ -117,7 +117,9 @@ export interface IEditorGraphState {
117117

118118
export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState> {
119119
private _soundsList: Sound[] = [];
120-
private _objectsToCopy: TreeNodeInfo<unknown>[] = [];
120+
121+
public _nodeToCopyTransform: Node | null = null;
122+
public _objectsToCopy: TreeNodeInfo<unknown>[] = [];
121123

122124
public constructor(props: IEditorGraphProps) {
123125
super(props);
@@ -392,6 +394,7 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
392394
*/
393395
public copySelectedNodes(): void {
394396
this._objectsToCopy = this.props.editor.layout.graph.getSelectedNodes();
397+
this.refresh();
395398
}
396399

397400
/**
@@ -402,7 +405,7 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
402405
return;
403406
}
404407

405-
const newNodes: (Node | ParticleSystem | Sprite)[] = [];
408+
const newNodes: (Node | IParticleSystem | Sprite)[] = [];
406409
const nodesToCopy = this._objectsToCopy.map((n) => n.nodeData);
407410

408411
registerUndoRedo({
@@ -432,7 +435,7 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
432435
},
433436
redo: () => {
434437
nodesToCopy.forEach((object) => {
435-
let node: Node | ParticleSystem | Sprite | null = null;
438+
let node: Node | IParticleSystem | Sprite | null = null;
436439

437440
defer: {
438441
if (isAbstractMesh(object)) {
@@ -491,6 +494,91 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
491494
});
492495
}
493496

497+
public copySelectedNodeTransform(node: Node): void {
498+
this._nodeToCopyTransform = node;
499+
this.refresh();
500+
}
501+
502+
public pasteSelectedNodeTransform(node: Node): void {
503+
if (!this._nodeToCopyTransform) {
504+
return;
505+
}
506+
507+
const sourcePosition = this._nodeToCopyTransform["position"];
508+
const sourceRotation = this._nodeToCopyTransform["rotation"];
509+
const sourceScaling = this._nodeToCopyTransform["scaling"];
510+
const sourceRotationQuaternion = this._nodeToCopyTransform["rotationQuaternion"];
511+
const sourceDirection = this._nodeToCopyTransform["direction"];
512+
513+
const targetPosition = node["position"];
514+
const targetRotation = node["rotation"];
515+
const targetScaling = node["scaling"];
516+
const targetRotationQuaternion = node["rotationQuaternion"];
517+
const targetDirection = node["direction"];
518+
519+
const savedTargetPosition = targetPosition?.clone();
520+
const savedTargetRotation = targetRotation?.clone();
521+
const savedTargetScaling = targetScaling?.clone();
522+
const savedTargetRotationQuaternion = targetRotationQuaternion?.clone();
523+
const savedTargetDirection = targetDirection?.clone();
524+
525+
registerUndoRedo({
526+
executeRedo: true,
527+
undo: () => {
528+
if (savedTargetPosition && targetPosition) {
529+
targetPosition.copyFrom(savedTargetPosition);
530+
}
531+
532+
if (savedTargetRotation && targetRotation) {
533+
targetRotation.copyFrom(savedTargetRotation);
534+
}
535+
536+
if (savedTargetScaling && targetScaling) {
537+
targetScaling.copyFrom(savedTargetScaling);
538+
}
539+
540+
if (targetRotationQuaternion) {
541+
if (!savedTargetRotationQuaternion) {
542+
node["rotationQuaternion"] = null;
543+
} else {
544+
targetRotationQuaternion.copyFrom(savedTargetRotationQuaternion);
545+
}
546+
}
547+
548+
if (savedTargetDirection && targetDirection) {
549+
targetDirection.copyFrom(savedTargetDirection);
550+
}
551+
},
552+
redo: () => {
553+
if (sourcePosition && targetPosition) {
554+
targetPosition.copyFrom(sourcePosition);
555+
}
556+
557+
if (sourceRotation && targetRotation) {
558+
targetRotation.copyFrom(sourceRotation);
559+
}
560+
561+
if (sourceScaling && targetScaling) {
562+
targetScaling.copyFrom(sourceScaling);
563+
}
564+
565+
if (sourceRotationQuaternion) {
566+
if (targetRotationQuaternion) {
567+
targetRotationQuaternion.copyFrom(sourceRotationQuaternion);
568+
} else {
569+
node["rotationQuaternion"] = sourceRotationQuaternion.clone();
570+
}
571+
}
572+
573+
if (sourceDirection && targetDirection) {
574+
targetDirection.copyFrom(sourceDirection);
575+
}
576+
},
577+
});
578+
579+
this.props.editor.layout.inspector.forceUpdate();
580+
}
581+
494582
private _handleSearch(search: string) {
495583
this.setState({ search }, () => {
496584
this.refresh();

editor/src/editor/layout/graph/graph.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Component, PropsWithChildren, ReactNode } from "react";
55
import { IoMdCube } from "react-icons/io";
66
import { AiOutlinePlus, AiOutlineClose } from "react-icons/ai";
77

8-
import { Mesh, SubMesh, Node, InstancedMesh, Sprite } from "babylonjs";
8+
import { Mesh, SubMesh, Node, InstancedMesh, Sprite, IParticleSystem } from "babylonjs";
99

1010
import {
1111
ContextMenu,
@@ -35,6 +35,7 @@ import { reloadSound } from "../../../tools/sound/tools";
3535
import { registerUndoRedo } from "../../../tools/undoredo";
3636
import { waitNextAnimationFrame } from "../../../tools/tools";
3737
import { createMeshInstance } from "../../../tools/mesh/instance";
38+
import { isAnyParticleSystem } from "../../../tools/guards/particles";
3839
import { isScene, isSceneLinkNode } from "../../../tools/guards/scene";
3940
import { cloneNode, ICloneNodeOptions } from "../../../tools/node/clone";
4041
import { isSprite, isSpriteMapNode } from "../../../tools/guards/sprites";
@@ -88,12 +89,29 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
8889
</ContextMenuItem>
8990

9091
{isNode(this.props.object) && (
91-
<ContextMenuItem onClick={() => this.props.editor.layout.graph.pasteSelectedNodes(this.props.object)}>
92+
<ContextMenuItem
93+
disabled={this.props.editor.layout.graph._objectsToCopy.length === 0}
94+
onClick={() => this.props.editor.layout.graph.pasteSelectedNodes(this.props.object)}
95+
>
9296
Paste <ContextMenuShortcut>{platform() === "darwin" ? "⌘+V" : "CTRL+V"}</ContextMenuShortcut>
9397
</ContextMenuItem>
9498
)}
9599

96-
<ContextMenuSeparator />
100+
{isNode(this.props.object) && (
101+
<>
102+
<ContextMenuSeparator />
103+
<ContextMenuItem onClick={() => this.props.editor.layout.graph.copySelectedNodeTransform(this.props.object)}>
104+
Copy Transform
105+
</ContextMenuItem>
106+
<ContextMenuItem
107+
disabled={this.props.editor.layout.graph._nodeToCopyTransform === null}
108+
onClick={() => this.props.editor.layout.graph.pasteSelectedNodeTransform(this.props.object)}
109+
>
110+
Paste Transform
111+
</ContextMenuItem>
112+
<ContextMenuSeparator />
113+
</>
114+
)}
97115
</>
98116
)}
99117

@@ -170,7 +188,7 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
170188
</ContextMenuSub>
171189
)}
172190

173-
{!isScene(this.props.object) && !isSound(this.props.object) && !isSprite(this.props.object) && (
191+
{!isScene(this.props.object) && !isSound(this.props.object) && !isSprite(this.props.object) && !isAnyParticleSystem(this.props.object) && (
174192
<>
175193
<ContextMenuSeparator />
176194

@@ -284,7 +302,7 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
284302
node = node.parent;
285303
}
286304

287-
let clone: Node | Sprite | null = null;
305+
let clone: Node | Sprite | IParticleSystem | null = null;
288306

289307
const cloneOptions: ICloneNodeOptions = {
290308
shareGeometry: true,
@@ -344,7 +362,10 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
344362
}
345363

346364
this.props.editor.layout.inspector.setEditedObject(clone);
347-
this.props.editor.layout.preview.gizmo.setAttachedObject(clone);
365+
366+
if (isNode(clone) || isSprite(clone)) {
367+
this.props.editor.layout.preview.gizmo.setAttachedObject(clone);
368+
}
348369
});
349370
},
350371
undo: () => {

editor/src/editor/layout/preview/import/import.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,27 @@ import { pathExists, readFile, readJSON, writeFile } from "fs-extra";
55
import axios from "axios";
66
import { toast } from "sonner";
77

8-
import { CubeTexture, ISceneLoaderAsyncResult, Material, Node, Scene, SceneLoader, Texture, Tools, ColorGradingTexture, Vector3, Quaternion, Sprite } from "babylonjs";
8+
import {
9+
CubeTexture,
10+
ISceneLoaderAsyncResult,
11+
Material,
12+
Node,
13+
Scene,
14+
SceneLoader,
15+
Texture,
16+
Tools,
17+
ColorGradingTexture,
18+
Vector3,
19+
Quaternion,
20+
Sprite,
21+
IParticleSystem,
22+
} from "babylonjs";
923

1024
import { UniqueNumber } from "../../../../tools/tools";
25+
import { isMesh } from "../../../../tools/guards/nodes";
26+
import { isSprite } from "../../../../tools/guards/sprites";
1127
import { isTexture } from "../../../../tools/guards/texture";
1228
import { executeSimpleWorker } from "../../../../tools/worker";
13-
import { isMesh, isNode } from "../../../../tools/guards/nodes";
1429
import { isMultiMaterial } from "../../../../tools/guards/material";
1530
import { configureSimultaneousLightsForMaterial } from "../../../../tools/material/material";
1631
import { onNodesAddedObservable, onTextureAddedObservable } from "../../../../tools/observables";
@@ -158,8 +173,8 @@ export async function loadImportedSceneFile(scene: Scene, absolutePath: string):
158173
return result;
159174
}
160175

161-
export function configureImportedNodeIds(node: Node | Sprite): void {
162-
if (isNode(node)) {
176+
export function configureImportedNodeIds(node: Node | Sprite | IParticleSystem): void {
177+
if (!isSprite(node)) {
163178
node.id = Tools.RandomId();
164179
}
165180

editor/src/tools/node/clone.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Node, Tools, Sprite } from "babylonjs";
1+
import { Node, Tools, Sprite, ParticleSystem, GPUParticleSystem } from "babylonjs";
22

33
import { Editor } from "../../editor/main";
44

@@ -14,6 +14,7 @@ import { UniqueNumber } from "../tools";
1414
import { cloneSprite } from "../sprite/tools";
1515

1616
import { isTexture } from "../guards/texture";
17+
import { isAnyParticleSystem } from "../guards/particles";
1718
import { isSprite, isSpriteManagerNode, isSpriteMapNode } from "../guards/sprites";
1819
import { isCamera, isInstancedMesh, isLight, isMesh, isNode, isTransformNode } from "../guards/nodes";
1920

@@ -26,10 +27,10 @@ export interface ICloneNodeOptions {
2627
cloneThinInstances?: boolean;
2728
}
2829

29-
export function cloneNode(editor: Editor, node: Node | Sprite, options?: ICloneNodeOptions) {
30+
export function cloneNode(editor: Editor, node: Node | Sprite | ParticleSystem | GPUParticleSystem, options?: ICloneNodeOptions) {
3031
const suffix = "(Clone)";
3132

32-
let clone: Node | Sprite | null = null;
33+
let clone: Node | Sprite | ParticleSystem | GPUParticleSystem | null = null;
3334

3435
defer: {
3536
if (isMesh(node)) {
@@ -81,6 +82,13 @@ export function cloneNode(editor: Editor, node: Node | Sprite, options?: ICloneN
8182

8283
break defer;
8384
}
85+
86+
if (isAnyParticleSystem(node)) {
87+
const name = `${node.name.replace(` ${suffix}`, "")} ${suffix}`;
88+
clone = node.clone(name, node.emitter);
89+
90+
break defer;
91+
}
8492
}
8593

8694
if (!clone) {

0 commit comments

Comments
 (0)