Skip to content

Commit 945d7ae

Browse files
committed
fix: import of filenamify in export node and put feature as experimental
1 parent b482ee1 commit 945d7ae

File tree

4 files changed

+162
-86
lines changed

4 files changed

+162
-86
lines changed

editor/src/editor/dialogs/edit-preferences/edit-preferences.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ export class EditorEditPreferencesComponent extends Component<IEditorEditPrefere
221221

222222
trySetExperimentalFeaturesEnabledInLocalStorage(v);
223223

224-
this.props.editor.layout.graph.forceUpdate();
225-
this.props.editor.layout.assets.forceUpdate();
224+
this.props.editor.layout.graph.refresh();
225+
this.props.editor.layout.assets.refresh();
226226
this.props.editor.layout.preview.forceUpdate();
227227
this.props.editor.layout.inspector.forceUpdate();
228228
this.props.editor.layout.animations.forceUpdate();

editor/src/editor/layout/assets-browser.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import {
6161
ContextMenuSubContent,
6262
} from "../../ui/shadcn/ui/context-menu";
6363

64+
import { exportNodeToPath } from "./graph/export";
65+
6466
import { FileInspectorObject } from "./inspector/file";
6567

6668
import { AssetBrowserGUIItem } from "./assets-browser/items/gui-item";
@@ -893,8 +895,12 @@ export class EditorAssetsBrowser extends Component<IEditorAssetsBrowserProps, IE
893895

894896
private _handleDragOver(event: DragEvent<HTMLDivElement>): void {
895897
event.preventDefault();
898+
899+
const isGraphNode = event.dataTransfer.types.includes("graph/node");
900+
const isFiles = event.dataTransfer.types.length === 1 && event.dataTransfer.types[0] === "Files";
901+
896902
this.setState({
897-
dragAndDroppingFiles: event.dataTransfer.types.length === 1 && event.dataTransfer.types[0] === "Files",
903+
dragAndDroppingFiles: isFiles || isGraphNode,
898904
});
899905
}
900906

@@ -910,6 +916,17 @@ export class EditorAssetsBrowser extends Component<IEditorAssetsBrowserProps, IE
910916
return;
911917
}
912918

919+
// Nodes from graph?
920+
try {
921+
const data = JSON.parse(event.dataTransfer.getData("graph/node")) as string[];
922+
if (data?.length) {
923+
return this._handleDroppedNodesFromGraph(data);
924+
}
925+
} catch (e) {
926+
// Catch silently.
927+
}
928+
929+
// Those are files.
913930
let assetFileNotInAssetsFolder = false;
914931

915932
const filesToCopy: Record<string, string> = {};
@@ -963,6 +980,34 @@ export class EditorAssetsBrowser extends Component<IEditorAssetsBrowserProps, IE
963980
this.refresh();
964981
}
965982

983+
private async _handleDroppedNodesFromGraph(nodeIds: string[]): Promise<void> {
984+
if (!this.state.browsedPath || !this.props.editor.state.enableExperimentalFeatures) {
985+
return;
986+
}
987+
988+
if (!this.isAssetsFolder()) {
989+
showAlert("Warning", <div>You can only export nodes to at least in the root "assets" folder.</div>, true);
990+
return;
991+
}
992+
993+
for (const nodeId of nodeIds) {
994+
const node = this.props.editor.layout.preview.scene.getNodeById(nodeId);
995+
if (node) {
996+
const scenePath = join(this.state.browsedPath, `${filenamify(node.name)}.babylon`);
997+
if (await pathExists(scenePath)) {
998+
const overwrite = await showConfirm("Overwrite Scene?", `A scene named "${node.name}" already exists at this location. Do you want to overwrite it?`);
999+
if (!overwrite) {
1000+
continue;
1001+
}
1002+
}
1003+
1004+
await exportNodeToPath(this.props.editor, node, join(this.state.browsedPath, `${filenamify(node.name)}.babylon`));
1005+
}
1006+
}
1007+
1008+
return this.refresh();
1009+
}
1010+
9661011
private async _handleCreateDirectory(): Promise<void> {
9671012
if (!this.state.browsedPath) {
9681013
return;
Lines changed: 107 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { Node, SceneSerializer } from "babylonjs";
2-
3-
import { saveSingleFileDialog } from "../../../tools/dialog";
1+
import filenamify from "filenamify";
42
import { writeJSON } from "fs-extra";
3+
54
import { toast } from "sonner";
65

6+
import { Node, SceneSerializer, Scene, Material } from "babylonjs";
7+
8+
import { isAbstractMesh } from "../../../tools/guards/nodes";
9+
import { saveSingleFileDialog } from "../../../tools/dialog";
10+
import { isMultiMaterial } from "../../../tools/guards/material";
11+
712
import { Editor } from "../../main";
8-
import filenamify from "filenamify/filenamify";
913

10-
const JSON_CONFIG = {
11-
spaces: 4,
12-
encoding: "utf8",
13-
};
1414
/**
1515
* Exports the entire scene to a .babylon file.
1616
* @param editor defines the reference to the editor used to get the scene.
@@ -28,9 +28,11 @@ export async function exportScene(editor: Editor): Promise<void> {
2828

2929
try {
3030
const scene = editor.layout.preview.scene;
31+
32+
SceneSerializer.ClearCache();
3133
const data = await SceneSerializer.SerializeAsync(scene);
3234

33-
await writeJSON(filePath, data, JSON_CONFIG);
35+
await writeJSON(filePath, data);
3436

3537
editor.layout.console.log(`Scene exported successfully to ${filePath}`);
3638
toast.success(`Scene exported successfully to ${filePath}`);
@@ -54,10 +56,17 @@ export async function exportNode(editor: Editor, node: Node): Promise<void> {
5456
defaultPath: `${filenamify(node.name)}.babylon`,
5557
});
5658

57-
if (!filePath) {
58-
return;
59+
if (filePath) {
60+
return exportNodeToPath(editor, node, filePath);
5961
}
62+
}
6063

64+
/**
65+
* Exports a specific node and all its children to a .babylon file.
66+
* @param editor defines the reference to the editor used to get the scene.
67+
* @param node defines the node to export along with its descendants.
68+
*/
69+
export async function exportNodeToPath(editor: Editor, node: Node, filePath: string): Promise<void> {
6170
try {
6271
const scene = editor.layout.preview.scene;
6372

@@ -70,26 +79,29 @@ export async function exportNode(editor: Editor, node: Node): Promise<void> {
7079
descendants.forEach((descendant) => nodesToInclude.add(descendant));
7180

7281
// Store original doNotSerialize values
73-
const originalDoNotSerialize = new Map<Node, boolean>();
82+
const originalNodesDoNotSerialize = new Map<Node, boolean>();
83+
const originalMaterialsDoNotSerialize = new Map<Material, boolean>();
7484

75-
exportMeshes(scene, nodesToInclude, originalDoNotSerialize);
76-
exportLights(scene, nodesToInclude, originalDoNotSerialize);
77-
exportCameras(scene, nodesToInclude, originalDoNotSerialize);
78-
exportTransformNodes(scene, nodesToInclude, originalDoNotSerialize);
85+
exportMeshes(scene, nodesToInclude, originalNodesDoNotSerialize);
86+
exportLights(scene, nodesToInclude, originalNodesDoNotSerialize);
87+
exportCameras(scene, nodesToInclude, originalNodesDoNotSerialize);
88+
exportTransformNodes(scene, nodesToInclude, originalNodesDoNotSerialize);
89+
exportMaterials(scene, nodesToInclude, originalMaterialsDoNotSerialize);
7990

8091
const originalParticleSystems = exportParticleSystems(scene, nodesToInclude);
8192
const originalSoundTracks = exportSounds(scene, nodesToInclude);
8293

8394
// Serialize the filtered scene
95+
// SceneSerializer.ClearCache();
8496
const data = await SceneSerializer.SerializeAsync(scene);
8597

8698
// Restore original scene state
87-
restoreSceneState(scene, originalDoNotSerialize, {
99+
restoreSceneState(scene, originalNodesDoNotSerialize, originalMaterialsDoNotSerialize, {
100+
originalSoundTracks,
88101
originalParticleSystems,
89-
originalSoundTracks
90102
});
91103

92-
await writeJSON(filePath, data, JSON_CONFIG);
104+
await writeJSON(filePath, data);
93105

94106
editor.layout.console.log(`Node exported successfully to ${filePath}`);
95107
toast.success(`Node exported successfully to ${filePath}`);
@@ -108,21 +120,24 @@ export async function exportNode(editor: Editor, node: Node): Promise<void> {
108120
* @param exportData Data needed for restoration
109121
*/
110122
function restoreSceneState(
111-
scene: any,
123+
scene: Scene,
112124
originalDoNotSerialize: Map<Node, boolean>,
125+
originalMaterialsDoNotSerialize: Map<Material, boolean>,
113126
exportData: { originalParticleSystems: any[]; originalSoundTracks: any[] }
114127
): void {
115128
// Restore original doNotSerialize values
116129
originalDoNotSerialize.forEach((value, node) => {
117130
node.doNotSerialize = value;
118131
});
119-
132+
133+
originalMaterialsDoNotSerialize.forEach((value, material) => {
134+
material.doNotSerialize = value;
135+
});
136+
120137
// Restore particle systems
121138
scene.particleSystems.length = 0;
122-
exportData.originalParticleSystems.forEach((ps: any) =>
123-
scene.particleSystems.push(ps)
124-
);
125-
139+
exportData.originalParticleSystems.forEach((ps: any) => scene.particleSystems.push(ps));
140+
126141
// Restore soundtracks
127142
scene.soundTracks = exportData.originalSoundTracks;
128143
}
@@ -133,29 +148,50 @@ function restoreSceneState(
133148
* @param nodesToInclude Set of nodes to include in the export
134149
* @param originalDoNotSerialize Map to store original serialization state
135150
*/
136-
function exportMeshes(
137-
scene: any,
138-
nodesToInclude: Set<Node>,
139-
originalDoNotSerialize: Map<Node, boolean>
140-
): void {
141-
scene.meshes.forEach((mesh: Node) => {
151+
function exportMeshes(scene: Scene, nodesToInclude: Set<Node>, originalDoNotSerialize: Map<Node, boolean>): void {
152+
scene.meshes.forEach((mesh) => {
142153
originalDoNotSerialize.set(mesh, mesh.doNotSerialize);
143154
mesh.doNotSerialize = !nodesToInclude.has(mesh);
144155
});
145156
}
146157

158+
function exportMaterials(scene: Scene, nodesToInclude: Set<Node>, originalMaterialsDoNotSerialize: Map<Material, boolean>): void {
159+
scene.materials.forEach((material) => {
160+
let foundUsedMaterial = false;
161+
162+
for (const node of nodesToInclude) {
163+
if (!isAbstractMesh(node) || !node.material) {
164+
continue;
165+
}
166+
167+
if (node.material === material) {
168+
foundUsedMaterial = true;
169+
break;
170+
}
171+
172+
if (isMultiMaterial(node.material)) {
173+
if (node.material.subMaterials.includes(material)) {
174+
foundUsedMaterial = true;
175+
break;
176+
}
177+
}
178+
}
179+
180+
if (!foundUsedMaterial) {
181+
originalMaterialsDoNotSerialize.set(material, material.doNotSerialize);
182+
material.doNotSerialize = true;
183+
}
184+
});
185+
}
186+
147187
/**
148188
* Configures light nodes for export.
149189
* @param scene The scene containing the lights
150190
* @param nodesToInclude Set of nodes to include in the export
151191
* @param originalDoNotSerialize Map to store original serialization state
152192
*/
153-
function exportLights(
154-
scene: any,
155-
nodesToInclude: Set<Node>,
156-
originalDoNotSerialize: Map<Node, boolean>
157-
): void {
158-
scene.lights.forEach((light: Node) => {
193+
function exportLights(scene: Scene, nodesToInclude: Set<Node>, originalDoNotSerialize: Map<Node, boolean>): void {
194+
scene.lights.forEach((light) => {
159195
originalDoNotSerialize.set(light, light.doNotSerialize);
160196
light.doNotSerialize = !nodesToInclude.has(light);
161197
});
@@ -167,12 +203,8 @@ function exportLights(
167203
* @param nodesToInclude Set of nodes to include in the export
168204
* @param originalDoNotSerialize Map to store original serialization state
169205
*/
170-
function exportCameras(
171-
scene: any,
172-
nodesToInclude: Set<Node>,
173-
originalDoNotSerialize: Map<Node, boolean>
174-
): void {
175-
scene.cameras.forEach((camera: Node) => {
206+
function exportCameras(scene: Scene, nodesToInclude: Set<Node>, originalDoNotSerialize: Map<Node, boolean>): void {
207+
scene.cameras.forEach((camera) => {
176208
originalDoNotSerialize.set(camera, camera.doNotSerialize);
177209
camera.doNotSerialize = !nodesToInclude.has(camera);
178210
});
@@ -184,12 +216,8 @@ function exportCameras(
184216
* @param nodesToInclude Set of nodes to include in the export
185217
* @param originalDoNotSerialize Map to store original serialization state
186218
*/
187-
function exportTransformNodes(
188-
scene: any,
189-
nodesToInclude: Set<Node>,
190-
originalDoNotSerialize: Map<Node, boolean>
191-
): void {
192-
scene.transformNodes.forEach((transformNode: Node) => {
219+
function exportTransformNodes(scene: Scene, nodesToInclude: Set<Node>, originalDoNotSerialize: Map<Node, boolean>): void {
220+
scene.transformNodes.forEach((transformNode) => {
193221
originalDoNotSerialize.set(transformNode, transformNode.doNotSerialize);
194222
transformNode.doNotSerialize = !nodesToInclude.has(transformNode);
195223
});
@@ -201,20 +229,22 @@ function exportTransformNodes(
201229
* @param nodesToInclude Set of nodes to include in the export
202230
* @returns The original array of particle systems (for restoration)
203231
*/
204-
function exportParticleSystems(scene: any, nodesToInclude: Set<Node>): Array<any> {
232+
function exportParticleSystems(scene: Scene, nodesToInclude: Set<Node>): Array<any> {
205233
// Save original particle systems
206234
const originalParticleSystems = scene.particleSystems.slice();
207-
235+
208236
// Filter particle systems to only include those attached to our nodes
209237
const particlesToKeep = originalParticleSystems.filter((ps: any) => {
210238
const emitter = ps.emitter;
211239
return emitter && nodesToInclude.has(emitter as Node);
212240
});
213-
241+
214242
// Replace the scene's particle systems with only those we want to include
215243
scene.particleSystems.length = 0;
216-
particlesToKeep.forEach((ps: any) => scene.particleSystems.push(ps));
217-
244+
particlesToKeep.forEach((ps: any) => {
245+
scene.particleSystems.push(ps);
246+
});
247+
218248
return originalParticleSystems;
219249
}
220250

@@ -224,37 +254,39 @@ function exportParticleSystems(scene: any, nodesToInclude: Set<Node>): Array<any
224254
* @param nodesToInclude Set of nodes to include in the export
225255
* @returns The original array of soundtracks (for restoration)
226256
*/
227-
function exportSounds(scene: any, nodesToInclude: Set<Node>): Array<any> {
257+
function exportSounds(scene: Scene, nodesToInclude: Set<Node>): Array<any> {
228258
// Handle sounds - filter out sounds not attached to our nodes
229259
let originalSoundTracks: any[] = [];
230-
260+
231261
if (scene.soundTracks) {
232262
// Store original soundtracks to restore later
233263
originalSoundTracks = scene.soundTracks.slice();
234-
264+
235265
// Filter each soundtrack to only include sounds attached to our nodes
236-
const filteredSoundTracks = scene.soundTracks.map((soundtrack: any) => {
237-
// Create a new sound collection with only the sounds attached to our nodes
238-
const filteredSoundCollection = soundtrack.soundCollection.filter((sound: any) => {
239-
if (sound.spatialSound && sound.metadata && sound.metadata.connectedMeshName) {
240-
// Check if the connected mesh name matches any of our nodes
241-
for (const meshNode of nodesToInclude) {
242-
if (meshNode.name === sound.metadata.connectedMeshName) {
243-
return true;
266+
const filteredSoundTracks = scene.soundTracks
267+
.map((soundtrack: any) => {
268+
// Create a new sound collection with only the sounds attached to our nodes
269+
const filteredSoundCollection = soundtrack.soundCollection.filter((sound: any) => {
270+
if (sound.spatialSound && sound.metadata && sound.metadata.connectedMeshName) {
271+
// Check if the connected mesh name matches any of our nodes
272+
for (const meshNode of nodesToInclude) {
273+
if (meshNode.name === sound.metadata.connectedMeshName) {
274+
return true;
275+
}
244276
}
245277
}
246-
}
247-
return false;
248-
});
249-
250-
// Replace the original sound collection with our filtered one
251-
soundtrack.soundCollection = filteredSoundCollection;
252-
return soundtrack;
253-
}).filter((st: any) => st.soundCollection.length > 0); // Remove empty soundtracks
254-
278+
return false;
279+
});
280+
281+
// Replace the original sound collection with our filtered one
282+
soundtrack.soundCollection = filteredSoundCollection;
283+
return soundtrack;
284+
})
285+
.filter((st: any) => st.soundCollection.length > 0); // Remove empty soundtracks
286+
255287
// Replace scene soundtracks with our filtered ones
256288
scene.soundTracks = filteredSoundTracks;
257289
}
258-
290+
259291
return originalSoundTracks;
260292
}

0 commit comments

Comments
 (0)