Skip to content

Commit 616af57

Browse files
committed
Add menu item to export .babylon
1 parent 89c206b commit 616af57

File tree

2 files changed

+129
-4
lines changed

2 files changed

+129
-4
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Node, SceneSerializer } from "babylonjs";
2+
3+
import { saveSingleFileDialog } from "../../../tools/dialog";
4+
import { writeJSON } from "fs-extra";
5+
import { toast } from "sonner";
6+
7+
import { Editor } from "../../main";
8+
9+
const JSON_CONFIG = {
10+
spaces: 4,
11+
encoding: "utf8",
12+
};
13+
/**
14+
* Exports the entire scene to a .babylon file.
15+
* @param editor defines the reference to the editor used to get the scene.
16+
*/
17+
export async function exportScene(editor: Editor): Promise<void> {
18+
const filePath = saveSingleFileDialog({
19+
title: "Export Scene",
20+
filters: [{ name: "Babylon Scene Files", extensions: ["babylon"] }],
21+
defaultPath: "scene.babylon",
22+
});
23+
24+
if (!filePath) {
25+
return;
26+
}
27+
28+
try {
29+
const scene = editor.layout.preview.scene;
30+
const data = await SceneSerializer.SerializeAsync(scene);
31+
32+
await writeJSON(filePath, data, JSON_CONFIG);
33+
34+
editor.layout.console.log(`Scene exported successfully to ${filePath}`);
35+
toast.success(`Scene exported successfully to ${filePath}`);
36+
} catch (e) {
37+
if (e instanceof Error) {
38+
editor.layout.console.error(`Error exporting scene: ${e.message}`);
39+
toast.error("Error exporting scene");
40+
}
41+
}
42+
}
43+
44+
/**
45+
* Exports a specific node and all its children to a .babylon file.
46+
* @param editor defines the reference to the editor used to get the scene.
47+
* @param node defines the node to export along with its descendants.
48+
*/
49+
export async function exportNode(editor: Editor, node: Node): Promise<void> {
50+
const filePath = saveSingleFileDialog({
51+
title: "Export Node",
52+
filters: [{ name: "Babylon Scene Files", extensions: ["babylon"] }],
53+
defaultPath: `${node.name}.babylon`,
54+
});
55+
56+
if (!filePath) {
57+
return;
58+
}
59+
60+
try {
61+
const scene = editor.layout.preview.scene;
62+
63+
// Get all nodes that should be included (selected node + descendants)
64+
const nodesToInclude = new Set<Node>();
65+
nodesToInclude.add(node);
66+
67+
// Add all descendants
68+
const descendants = node.getDescendants(false);
69+
descendants.forEach((descendant) => nodesToInclude.add(descendant));
70+
71+
// Store original doNotSerialize values
72+
const originalDoNotSerialize = new Map<Node, boolean>();
73+
74+
// Temporarily set doNotSerialize = true for all nodes except the ones we want to include
75+
scene.meshes.forEach((mesh) => {
76+
originalDoNotSerialize.set(mesh, mesh.doNotSerialize);
77+
mesh.doNotSerialize = !nodesToInclude.has(mesh);
78+
});
79+
80+
scene.lights.forEach((light) => {
81+
originalDoNotSerialize.set(light, light.doNotSerialize);
82+
light.doNotSerialize = !nodesToInclude.has(light);
83+
});
84+
85+
scene.cameras.forEach((camera) => {
86+
originalDoNotSerialize.set(camera, camera.doNotSerialize);
87+
camera.doNotSerialize = !nodesToInclude.has(camera);
88+
});
89+
90+
scene.transformNodes.forEach((transformNode) => {
91+
originalDoNotSerialize.set(transformNode, transformNode.doNotSerialize);
92+
transformNode.doNotSerialize = !nodesToInclude.has(transformNode);
93+
});
94+
95+
// Serialize the filtered scene
96+
const data = await SceneSerializer.SerializeAsync(scene);
97+
98+
// Restore original doNotSerialize values
99+
originalDoNotSerialize.forEach((value, node) => {
100+
node.doNotSerialize = value;
101+
});
102+
103+
await writeJSON(filePath, data, JSON_CONFIG);
104+
105+
editor.layout.console.log(`Node exported successfully to ${filePath}`);
106+
toast.success(`Node exported successfully to ${filePath}`);
107+
} catch (e) {
108+
if (e instanceof Error) {
109+
editor.layout.console.error(`Error exporting node: ${e.message}`);
110+
toast.error("Error exporting node");
111+
}
112+
}
113+
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { EditorInspectorSwitchField } from "../inspector/fields/switch";
4242

4343
import { Editor } from "../../main";
4444

45+
import { exportScene, exportNode } from "./export";
4546
import { removeNodes } from "./remove";
4647

4748
export interface IEditorGraphContextMenuProps extends PropsWithChildren {
@@ -71,10 +72,6 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
7172

7273
{!isScene(this.props.object) && !isSound(this.props.object) && (
7374
<>
74-
<ContextMenuItem onClick={() => this._cloneNode(this.props.object)}>Clone</ContextMenuItem>
75-
76-
<ContextMenuSeparator />
77-
7875
<ContextMenuItem onClick={() => this.props.editor.layout.graph.copySelectedNodes()}>
7976
Copy <ContextMenuShortcut>{platform() === "darwin" ? "⌘+C" : "CTRL+C"}</ContextMenuShortcut>
8077
</ContextMenuItem>
@@ -86,11 +83,26 @@ export class EditorGraphContextMenu extends Component<IEditorGraphContextMenuPro
8683
)}
8784

8885
<ContextMenuSeparator />
86+
87+
<ContextMenuItem onClick={() => this._cloneNode(this.props.object)}>Clone</ContextMenuItem>
88+
89+
{isNode(this.props.object) && !isScene(this.props.object) && (
90+
<ContextMenuItem onClick={() => exportNode(this.props.editor, this.props.object)}>Export Node (.babylon)</ContextMenuItem>
91+
)}
92+
93+
<ContextMenuSeparator />
8994
</>
9095
)}
9196

9297
{isSound(this.props.object) && <ContextMenuItem onClick={() => this._reloadSound()}>Reload</ContextMenuItem>}
9398

99+
{isScene(this.props.object) && (
100+
<>
101+
<ContextMenuItem onClick={() => exportScene(this.props.editor)}>Export Scene (.babylon)</ContextMenuItem>
102+
<ContextMenuSeparator />
103+
</>
104+
)}
105+
94106
{(isNode(this.props.object) || isScene(this.props.object)) && !isSceneLinkNode(this.props.object) && (
95107
<ContextMenuSub>
96108
<ContextMenuSubTrigger className="flex items-center gap-2">

0 commit comments

Comments
 (0)