Repository: gradio-app/gradio
Pull request: Babylon update for model3D
## Description

Update Babylon.js dependency from 7.25.1 to 7.54.0. Use Babylon viewer as a ES module to reduce .js size.
No change to supported file formats (obj, gtlf,...)

Closes: #10846 

## Changes
- Replace Babylon.js core and loader with Babylon Viewer
- Use engine version 7.54.0 instead of 7.25.1
- Use viewer properties for right hand coordinate, autorotate,...
- Replace point cloud system with points rendering

## Improvements

Current Canvas3D size : 4.6Mb
Updated Canvas3D size: 1.2Mb
  
Viewer will try to use WebGPU when available for faster rendering and lower CPU usage

## Known limitations

Wireframe and points rendering use triangles and vertices color. It's not using a plain solid one (see screenshot). If this is a problem, I can update Babylon.js code to support a solid color and expose it as a property.

![image](https://github.com/user-attachments/assets/435d6f1f-238e-4fe9-a25c-54f4354b96ab)


Problem statement:
Model3D: Use Babylon Viewer ESM
model3D uses Babylon.js as UMD module. Update to ESM to allow tree-shaking and reduced size.
Also, Babylon.js new viewer can help make the code smaller. It can come with UI elements like loading bars, animation controls, etc.

Current Canvas3D size : 4.6Mb
Updated Canvas3D size: 1.2Mb

Hints from issue discussion:

Commit message: add changeset

Patch to review:
diff --git a/.changeset/old-lamps-wash.md b/.changeset/old-lamps-wash.md
new file mode 100644
index 0000000000..20e608abbc
--- /dev/null
+++ b/.changeset/old-lamps-wash.md
@@ -0,0 +1,6 @@
+---
+"@gradio/model3d": minor
+"gradio": minor
+---
+
+feat:Babylon update for model3D
diff --git a/js/model3D/package.json b/js/model3D/package.json
index 54adb18a62..0a4fdc01f2 100644
--- a/js/model3D/package.json
+++ b/js/model3D/package.json
@@ -15,8 +15,7 @@
 		"@gradio/utils": "workspace:^",
 		"@gradio/wasm": "workspace:^",
 		"@types/babylon": "^6.16.6",
-		"babylonjs": "^7.25.1",
-		"babylonjs-loaders": "^7.25.1",
+		"@babylonjs/viewer": "^7.54.0",
 		"dequal": "^2.0.2",
 		"gsplat": "^1.0.5"
 	},
diff --git a/js/model3D/shared/Canvas3D.svelte b/js/model3D/shared/Canvas3D.svelte
index 564e6dc9a3..5553b5ce3d 100644
--- a/js/model3D/shared/Canvas3D.svelte
+++ b/js/model3D/shared/Canvas3D.svelte
@@ -1,19 +1,9 @@
 <script lang="ts">
 	import { onMount } from "svelte";
-	import * as BABYLON from "babylonjs";
-	import * as BABYLON_LOADERS from "babylonjs-loaders";
+	import * as BABYLON_VIEWER from "@babylonjs/viewer";
 	import type { FileData } from "@gradio/client";
 	import { resolve_wasm_src } from "@gradio/wasm/svelte";
 
-	$: {
-		if (
-			BABYLON_LOADERS.OBJFileLoader != undefined &&
-			!BABYLON_LOADERS.OBJFileLoader.IMPORT_VERTEX_COLORS
-		) {
-			BABYLON_LOADERS.OBJFileLoader.IMPORT_VERTEX_COLORS = true;
-		}
-	}
-
 	export let value: FileData;
 	export let display_mode: "solid" | "point_cloud" | "wireframe";
 	export let clear_color: [number, number, number, number];
@@ -52,102 +42,80 @@
 
 	/* BabylonJS engine and scene management */
 	let canvas: HTMLCanvasElement;
-	let scene: BABYLON.Scene;
-	let engine: BABYLON.Engine;
-	let point_cloud_system: BABYLON.PointsCloudSystem | null = null;
+	let viewer: BABYLON_VIEWER.Viewer;
+	let viewerDetails: Readonly<BABYLON_VIEWER.ViewerDetails>;
 	let mounted = false;
 
 	onMount(() => {
-		// Initialize BabylonJS engine and scene
-		engine = new BABYLON.Engine(canvas, true);
-		scene = new BABYLON.Scene(engine);
-
-		scene.createDefaultCameraOrLight();
-		scene.useRightHandedSystem = true;
-		scene.clearColor = scene.clearColor = new BABYLON.Color4(...clear_color);
-
-		engine.runRenderLoop(() => {
-			scene.render();
+		// Initialize BabylonJS viewer
+		BABYLON_VIEWER.createViewerForCanvas(canvas, {
+			clearColor: clear_color,
+			useRightHandedSystem: true,
+			cameraAutoOrbit: { enabled: false },
+			onInitialized: (details: Readonly<BABYLON_VIEWER.ViewerDetails>) => {
+				viewerDetails = details;
+			}
+		}).then((promiseViewer: BABYLON_VIEWER.Viewer) => {
+			viewer = promiseViewer;
+			mounted = true;
 		});
 
-		function onWindowResize(): void {
-			engine.resize();
-		}
-		window.addEventListener("resize", onWindowResize);
-
-		mounted = true;
-
 		return () => {
-			scene.dispose();
-			engine.dispose();
-			window.removeEventListener("resize", onWindowResize);
+			viewer.dispose();
 		};
 	});
 
 	$: mounted && load_model(resolved_url);
 
-	function load_model(url: string | undefined): void {
-		if (scene) {
-			// Dispose of the previous model before loading a new one
-			scene.meshes.forEach((mesh) => {
-				mesh.dispose();
-			});
-
-			if (point_cloud_system) {
-				point_cloud_system.dispose();
-				point_cloud_system = null;
-			}
+	function setRenderingMode(pointsCloud: boolean, wireframe: boolean): void {
+		viewerDetails.scene.forcePointsCloud = pointsCloud;
+		viewerDetails.scene.forceWireframe = wireframe;
+	}
 
-			// Load the new model
-			if (url) {
-				BABYLON.SceneLoader.ShowLoadingScreen = false;
-				BABYLON.SceneLoader.Append(
-					url,
-					"",
-					scene,
-					() => {
-						if (display_mode === "point_cloud") {
-							create_point_cloud(scene);
-						} else if (display_mode === "wireframe") {
-							create_wireframe(scene);
-						} else {
-							create_camera(scene, camera_position, zoom_speed, pan_speed);
+	function load_model(url: string | undefined): void {
+		if (viewer) {
+			viewer
+				.loadModel(url!, {
+					pluginOptions: {
+						obj: {
+							importVertexColors: true
 						}
-					},
-					undefined,
-					undefined,
-					"." + value.path.split(".").pop()
-				);
-			}
+					}
+				})
+				.then(() => {
+					if (display_mode === "point_cloud") {
+						setRenderingMode(true, false);
+					} else if (display_mode === "wireframe") {
+						setRenderingMode(false, true);
+					} else {
+						update_camera(camera_position, zoom_speed, pan_speed);
+					}
+				});
 		}
 	}
 
-	function create_camera(
-		scene: BABYLON.Scene,
+	function update_camera(
 		camera_position: [number | null, number | null, number | null],
 		zoom_speed: number,
 		pan_speed: number
 	): void {
-		scene.createDefaultCamera(true, true, true);
-		var helperCamera = scene.activeCamera! as BABYLON.ArcRotateCamera;
+		const camera = viewerDetails.camera;
 		if (camera_position[0] !== null) {
-			helperCamera.alpha = BABYLON.Tools.ToRadians(camera_position[0]);
+			camera.alpha = BABYLON.Tools.ToRadians(camera_position[0]);
 		}
 		if (camera_position[1] !== null) {
-			helperCamera.beta = BABYLON.Tools.ToRadians(camera_position[1]);
+			camera.beta = BABYLON.Tools.ToRadians(camera_position[1]);
 		}
 		if (camera_position[2] !== null) {
-			helperCamera.radius = camera_position[2];
+			camera.radius = camera_position[2];
 		}
-		helperCamera.lowerRadiusLimit = 0.1;
+		camera.lowerRadiusLimit = 0.1;
 		const updateCameraSensibility = (): void => {
-			helperCamera.wheelPrecision = 250 / (helperCamera.radius * zoom_speed);
-			helperCamera.panningSensibility =
-				(10000 * pan_speed) / helperCamera.radius;
+			camera.wheelPrecision = 250 / (camera.radius * zoom_speed);
+			camera.panningSensibility = (10000 * pan_speed) / camera.radius;
 		};
 		updateCameraSensibility();
-		helperCamera.attachControl(true);
-		helperCamera.onAfterCheckInputsObservable.add(updateCameraSensibility);
+		camera.onAfterCheckInputsObservable.add(updateCameraSensibility);
 	}
 
 	export function reset_camera_position(
@@ -155,73 +123,10 @@
 		zoom_speed: number,
 		pan_speed: number
 	): void {
-		if (scene) {
-			scene.removeCamera(scene.activeCamera!);
-			create_camera(scene, camera_position, zoom_speed, pan_speed);
+		if (viewerDetails) {
+			update_camera(camera_position, zoom_speed, pan_speed);
 		}
 	}
-
-	function create_point_cloud(scene: BABYLON.Scene): void {
-		const meshes = scene.meshes;
-		const pointPositions: BABYLON.Vector3[] = [];
-
-		meshes.forEach((mesh) => {
-			if (mesh instanceof BABYLON.Mesh) {
-				const positions = mesh.getVerticesData(
-					BABYLON.VertexBuffer.PositionKind
-				);
-				if (positions) {
-					for (let i = 0; i < positions.length; i += 3) {
-						pointPositions.push(
-							new BABYLON.Vector3(
-								positions[i],
-								positions[i + 1],
-								positions[i + 2]
-							)
-						);
-					}
-				}
-				mesh.setEnabled(false);
-			}
-		});
-
-		point_cloud_system = new BABYLON.PointsCloudSystem(
-			"point_cloud_system",
-			1,
-			scene
-		);
-
-		point_cloud_system.addPoints(
-			pointPositions.length,
-			(particle: BABYLON.CloudPoint, i: number) => {
-				particle.position = pointPositions[i];
-				particle.color = new BABYLON.Color4(
-					Math.random(),
-					Math.random(),
-					Math.random(),
-					1.0
-				);
-			}
-		);
-
-		point_cloud_system.buildMeshAsync().then((mesh) => {
-			mesh.alwaysSelectAsActiveMesh = true;
-			create_camera(scene, camera_position, zoom_speed, pan_speed);
-		});
-	}
-
-	function create_wireframe(scene: BABYLON.Scene): void {
-		scene.meshes.forEach((mesh) => {
-			if (mesh instanceof BABYLON.Mesh) {
-				mesh.material = new BABYLON.StandardMaterial(
-					"wireframeMaterial",
-					scene
-				);
-				mesh.material.wireframe = true;
-			}
-			create_camera(scene, camera_position, zoom_speed, pan_speed);
-		});
-	}
 </script>
 
 <canvas bind:this={canvas}></canvas>
diff --git a/package.json b/package.json
index 94c3f38856..83b8233f2c 100644
--- a/package.json
+++ b/package.json
@@ -55,8 +55,9 @@
 		"@typescript-eslint/eslint-plugin": "^7.8.0",
 		"@typescript-eslint/parser": "^7.8.0",
 		"autoprefixer": "^10.4.19",
-		"babylonjs": "^7.25.1",
-		"babylonjs-loaders": "^7.25.1",
+		"babylonjs": "^7.42.0",
+		"babylonjs-loaders": "^7.42.0",
+		"babylonjs-viewer": "^7.42.0",
 		"esbuild": "^0.21.0",
 		"eslint": "^9.1.1",
 		"eslint-plugin-svelte": "^2.38.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f13ef615b..0b647944b6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -83,11 +83,14 @@ importers:
         specifier: ^10.4.19
         version: 10.4.19(postcss@8.4.38)
       babylonjs:
-        specifier: ^7.25.1
-        version: 7.25.2
+        specifier: ^7.42.0
+        version: 7.50.0
       babylonjs-loaders:
-        specifier: ^7.25.1
-        version: 7.25.2
+        specifier: ^7.42.0
+        version: 7.50.0
+      babylonjs-viewer:
+        specifier: ^7.42.0
+        version: 7.42.0
       esbuild:
         specifier: ^0.21.0
         version: 0.21.0
@@ -1786,6 +1789,9 @@ importers:
 
   js/model3D:
     dependencies:
+      '@babylonjs/viewer':
+        specifier: ^7.54.0
+        version: 7.54.0(@babylonjs/core@7.54.0)(@babylonjs/loaders@7.52.2(@babylonjs/core@7.54.0)(babylonjs-gltf2interface@7.50.0))
       '@gradio/atoms':
         specifier: workspace:^
         version: link:../atoms
@@ -1810,12 +1816,6 @@ importers:
       '@types/babylon':
         specifier: ^6.16.6
         version: 6.16.6
-      babylonjs:
-        specifier: ^7.25.1
-        version: 7.25.2
-      babylonjs-loaders:
-        specifier: ^7.25.1
-        version: 7.25.2
       dequal:
         specifier: ^2.0.2
         version: 2.0.3
@@ -2649,6 +2649,21 @@ packages:
     resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==}
     engines: {node: '>=6.9.0'}
 
+  '@babylonjs/core@7.54.0':
+    resolution: {integrity: sha512-K9NvHkmJD42/x1X8ZrFbLfwR/j3S4wdrpWrPDVNjS7+kDb4KKgqRDnmXHnigAPh5ddxJT+6DbtLyRmlzdQUZug==}
+
+  '@babylonjs/loaders@7.52.2':
+    resolution: {integrity: sha512-5NknIGF6X7rBP5aai+HS5wj2BdF2me8rn/6Ftix9O/3z7/riGWYL4yaVKHnwOQzzHLFu9KhaOMmn0liNQmnEEA==}
+    peerDependencies:
+      '@babylonjs/core': ^7.0.0
+      babylonjs-gltf2interface: ^7.0.0
+
+  '@babylonjs/viewer@7.54.0':
+    resolution: {integrity: sha512-S+X6oCXemDJ8a7WGxN8O2wh87PRcWvc3Iqv65a7kA5m3o7jsKi8Zc7I55Z0mOP0jWYhoUxGW0eNb5YPb/OIjmA==}
+    peerDependencies:
+      '@babylonjs/core': ^7.23.1
+      '@babylonjs/loaders': ^7.23.1
+
   '@bundled-es-modules/cookie@2.0.0':
     resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==}
 
@@ -3603,6 +3618,12 @@ packages:
   '@lezer/python@1.1.9':
     resolution: {integrity: sha512-8Ua3p8NdICXR6qWvRCnCx5CI1B0DklZGNtRLwOrIS/OHecHIugRHZyr0NsaaQO2H2Nn34EPlRtltXIirLsry5Q==}
 
+  '@lit-labs/ssr-dom-shim@1.3.0':
+    resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==}
+
+  '@lit/reactive-element@2.0.4':
+    resolution: {integrity: sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==}
+
   '@manypkg/find-root@1.1.0':
     resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
 
@@ -4956,14 +4977,17 @@ packages:
     resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==}
     engines: {node: '>= 10.0.0'}
 
-  babylonjs-gltf2interface@7.25.2:
-    resolution: {integrity: sha512-Vzwp0FmgTdxXH1RsPW8jRReSTkhTcrpulE6uJk4Kd4UTsRi1lpER31LpeysEYrjFf5SS+vc5gx4li8/TAUAWNw==}
+  babylonjs-gltf2interface@7.50.0:
+    resolution: {integrity: sha512-ogaOBkUT6DrVYh/VWl6nlnmKjVK3Ml9rJgj3C53bauC96vPGR89egW8D7YshbPOF16Y4NaCggAL2G8mHr1aaMw==}
+
+  babylonjs-loaders@7.50.0:
+    resolution: {integrity: sha512-yg95UDy4dCqit+NYF6ity0tAH/e0zSfbtCndKbIW8HBXFZ4uQensVxvlE6MfGbZoAr3et2E6TUIekY9vuJyrzg==}
 
-  babylonjs-loaders@7.25.2:
-    resolution: {integrity: sha512-FMZ5G2x8QgGowLRMxVifVuMNFnY4aaQuUIwqxrgcrKigaHKx1uNuk1+A4+vCG7i43TJJnTr4++QJF7nn6JdU0g==}
+  babylonjs-viewer@7.42.0:
+    resolution: {integrity: sha512-0CW1EtW877pb2HrW/iQBnS8YeYe12aTj8qMvLqMTWnFAqppmRy3aufnUmx9Li1udUB2sNPOnu5hYTbpHRxIfcQ==}
 
-  babylonjs@7.25.2:
-    resolution: {integrity: sha512-hucoQfYHrBtgq7niZEHwSDcvcESDiJ74AYY42KSDA43bGI+lq8UkW2wgs6euOFrigR4RxSORHWm0nkNrXCn41Q==}
+  babylonjs@7.50.0:
+    resolution: {integrity: sha512-0tmo5IJ/GQuukjXcYLaqtzUmW3s8p9A5QsH253wQCyHq2vjlcCQEgzquAc32ApFIldKAVg/2eFmnoP7C6eRDJw==}
 
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -6976,6 +7000,15 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
+  lit-element@4.1.1:
+    resolution: {integrity: sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==}
+
+  lit-html@3.2.1:
+    resolution: {integrity: sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==}
+
+  lit@3.2.1:
+    resolution: {integrity: sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==}
+
   load-yaml-file@0.2.0:
     resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
     engines: {node: '>=6'}
@@ -9401,6 +9434,19 @@ snapshots:
       to-fast-properties: 2.0.0
     optional: true
 
+  '@babylonjs/core@7.54.0': {}
+
+  '@babylonjs/loaders@7.52.2(@babylonjs/core@7.54.0)(babylonjs-gltf2interface@7.50.0)':
+    dependencies:
+      '@babylonjs/core': 7.54.0
+      babylonjs-gltf2interface: 7.50.0
+
+  '@babylonjs/viewer@7.54.0(@babylonjs/core@7.54.0)(@babylonjs/loaders@7.52.2(@babylonjs/core@7.54.0)(babylonjs-gltf2interface@7.50.0))':
+    dependencies:
+      '@babylonjs/core': 7.54.0
+      '@babylonjs/loaders': 7.52.2(@babylonjs/core@7.54.0)(babylonjs-gltf2interface@7.50.0)
+      lit: 3.2.1
+
   '@bundled-es-modules/cookie@2.0.0':
     dependencies:
       cookie: 0.5.0
@@ -10301,6 +10347,12 @@ snapshots:
       '@lezer/highlight': 1.2.1
       '@lezer/lr': 1.3.14
 
+  '@lit-labs/ssr-dom-shim@1.3.0': {}
+
+  '@lit/reactive-element@2.0.4':
+    dependencies:
+      '@lit-labs/ssr-dom-shim': 1.3.0
+
   '@manypkg/find-root@1.1.0':
     dependencies:
       '@babel/runtime': 7.24.5
@@ -11841,14 +11893,18 @@ snapshots:
     dependencies:
       '@babel/types': 7.23.3
 
-  babylonjs-gltf2interface@7.25.2: {}
+  babylonjs-gltf2interface@7.50.0: {}
+
+  babylonjs-loaders@7.50.0:
+    dependencies:
+      babylonjs: 7.50.0
+      babylonjs-gltf2interface: 7.50.0
 
-  babylonjs-loaders@7.25.2:
+  babylonjs-viewer@7.42.0:
     dependencies:
-      babylonjs: 7.25.2
-      babylonjs-gltf2interface: 7.25.2
+      babylonjs-gltf2interface: 7.50.0
 
-  babylonjs@7.25.2: {}
+  babylonjs@7.50.0: {}
 
   balanced-match@1.0.2: {}
 
@@ -14086,6 +14142,22 @@ snapshots:
 
   lines-and-columns@1.2.4: {}
 
+  lit-element@4.1.1:
+    dependencies:
+      '@lit-labs/ssr-dom-shim': 1.3.0
+      '@lit/reactive-element': 2.0.4
+      lit-html: 3.2.1
+
+  lit-html@3.2.1:
+    dependencies:
+      '@types/trusted-types': 2.0.6
+
+  lit@3.2.1:
+    dependencies:
+      '@lit/reactive-element': 2.0.4
+      lit-element: 4.1.1
+      lit-html: 3.2.1
+
   load-yaml-file@0.2.0:
     dependencies:
       graceful-fs: 4.2.11


Code review comments:
diff_hunk: @@ -52,176 +42,91 @@
 
 	/* BabylonJS engine and scene management */
 	let canvas: HTMLCanvasElement;
-	let scene: BABYLON.Scene;
-	let engine: BABYLON.Engine;
-	let point_cloud_system: BABYLON.PointsCloudSystem | null = null;
+	let viewer: BABYLON_VIEWER.Viewer;
+	let viewerDetails: Readonly<BABYLON_VIEWER.ViewerDetails>;
 	let mounted = false;
 
 	onMount(() => {
-		// Initialize BabylonJS engine and scene
-		engine = new BABYLON.Engine(canvas, true);
-		scene = new BABYLON.Scene(engine);
-
-		scene.createDefaultCameraOrLight();
-		scene.useRightHandedSystem = true;
-		scene.clearColor = scene.clearColor = new BABYLON.Color4(...clear_color);
-
-		engine.runRenderLoop(() => {
-			scene.render();
+		// Initialize BabylonJS viewer
+		BABYLON_VIEWER.createViewerForCanvas(canvas, {
+			clearColor: clear_color,
+			useRightHandedSystem: true,
+			cameraAutoOrbit: { enabled: false },
+			onInitialized: (details: Readonly<BABYLON_VIEWER.ViewerDetails>) => {
+				viewerDetails = details;
+			}
+		}).then((promiseViewer: BABYLON_VIEWER.Viewer) => {
+			viewer = promiseViewer;
+			mounted = true;
 		});
 
-		function onWindowResize(): void {
-			engine.resize();
-		}
-		window.addEventListener("resize", onWindowResize);
-
-		mounted = true;
-
 		return () => {
-			scene.dispose();
-			engine.dispose();
-			window.removeEventListener("resize", onWindowResize);
+			viewer.dispose();
 		};
 	});
 
 	$: mounted && load_model(resolved_url);
 
-	function load_model(url: string | undefined): void {
-		if (scene) {
-			// Dispose of the previous model before loading a new one
-			scene.meshes.forEach((mesh) => {
-				mesh.dispose();
-			});
-
-			if (point_cloud_system) {
-				point_cloud_system.dispose();
-				point_cloud_system = null;
-			}
+	function setRenderingMode(pointsCloud: boolean, wireframe: boolean): void {
+		viewerDetails.scene.forcePointsCloud = pointsCloud;
+		viewerDetails.scene.forceWireframe = wireframe;
+	}
 
-			// Load the new model
-			if (url) {
-				BABYLON.SceneLoader.ShowLoadingScreen = false;
-				BABYLON.SceneLoader.Append(
-					url,
-					"",
-					scene,
-					() => {
-						if (display_mode === "point_cloud") {
-							create_point_cloud(scene);
-						} else if (display_mode === "wireframe") {
-							create_wireframe(scene);
-						} else {
-							create_camera(scene, camera_position, zoom_speed, pan_speed);
+	function load_model(url: string | undefined): void {
+		if (viewer) {
+			viewer
+				.loadModel(url!, {
+					pluginOptions: {
+						obj: {
+							importVertexColors: true
 						}
-					},
-					undefined,
-					undefined,
-					"." + value.path.split(".").pop()
-				);
-			}
+					}
+				})
+				.then(() => {
+					if (display_mode === "point_cloud") {
+						setRenderingMode(true, false);
+					} else if (display_mode === "wireframe") {
+						setRenderingMode(false, true);
+					} else {
+						update_camera(camera_position, zoom_speed, pan_speed);
+					}
+				});
 		}
 	}
 
-	function create_camera(
-		scene: BABYLON.Scene,
+	function update_camera(
 		camera_position: [number | null, number | null, number | null],
 		zoom_speed: number,
 		pan_speed: number
 	): void {
-		scene.createDefaultCamera(true, true, true);
-		var helperCamera = scene.activeCamera! as BABYLON.ArcRotateCamera;
+		const camera = viewerDetails.camera;
 		if (camera_position[0] !== null) {
-			helperCamera.alpha = BABYLON.Tools.ToRadians(camera_position[0]);
+			camera.alpha = BABYLON.Tools.ToRadians(camera_position[0]);
path: js/model3D/shared/Canvas3D.svelte
text: @user3:
Isn't `BABYLON` gone now (this was the previous reference to the UMD module)? Maybe this is also why the camera reset didn't work for @user1 as mentioned here: https://github.com/gradio-app/gradio/pull/10847#issuecomment-2744635324

Seems like instead we should `import { Tools } from "@user2/core/Misc/tools"`. Alternatively, you could just change this to:
```suggestion
			camera.alpha = (camera_position[0] * Math.PI) / 180;
```
Then you don't need to pull in the whole `Tools` class just for this, and keep the bundle size smaller.

@author:
fixed

diff_hunk: @@ -52,176 +42,91 @@
 
 	/* BabylonJS engine and scene management */
 	let canvas: HTMLCanvasElement;
-	let scene: BABYLON.Scene;
-	let engine: BABYLON.Engine;
-	let point_cloud_system: BABYLON.PointsCloudSystem | null = null;
+	let viewer: BABYLON_VIEWER.Viewer;
+	let viewerDetails: Readonly<BABYLON_VIEWER.ViewerDetails>;
 	let mounted = false;
 
 	onMount(() => {
-		// Initialize BabylonJS engine and scene
-		engine = new BABYLON.Engine(canvas, true);
-		scene = new BABYLON.Scene(engine);
-
-		scene.createDefaultCameraOrLight();
-		scene.useRightHandedSystem = true;
-		scene.clearColor = scene.clearColor = new BABYLON.Color4(...clear_color);
-
-		engine.runRenderLoop(() => {
-			scene.render();
+		// Initialize BabylonJS viewer
+		BABYLON_VIEWER.createViewerForCanvas(canvas, {
+			clearColor: clear_color,
+			useRightHandedSystem: true,
+			cameraAutoOrbit: { enabled: false },
+			onInitialized: (details: Readonly<BABYLON_VIEWER.ViewerDetails>) => {
+				viewerDetails = details;
+			}
+		}).then((promiseViewer: BABYLON_VIEWER.Viewer) => {
+			viewer = promiseViewer;
+			mounted = true;
 		});
 
-		function onWindowResize(): void {
-			engine.resize();
-		}
-		window.addEventListener("resize", onWindowResize);
-
-		mounted = true;
-
 		return () => {
-			scene.dispose();
-			engine.dispose();
-			window.removeEventListener("resize", onWindowResize);
+			viewer.dispose();
 		};
 	});
 
 	$: mounted && load_model(resolved_url);
 
-	function load_model(url: string | undefined): void {
-		if (scene) {
-			// Dispose of the previous model before loading a new one
-			scene.meshes.forEach((mesh) => {
-				mesh.dispose();
-			});
-
-			if (point_cloud_system) {
-				point_cloud_system.dispose();
-				point_cloud_system = null;
-			}
+	function setRenderingMode(pointsCloud: boolean, wireframe: boolean): void {
+		viewerDetails.scene.forcePointsCloud = pointsCloud;
+		viewerDetails.scene.forceWireframe = wireframe;
+	}
 
-			// Load the new model
-			if (url) {
-				BABYLON.SceneLoader.ShowLoadingScreen = false;
-				BABYLON.SceneLoader.Append(
-					url,
-					"",
-					scene,
-					() => {
-						if (display_mode === "point_cloud") {
-							create_point_cloud(scene);
-						} else if (display_mode === "wireframe") {
-							create_wireframe(scene);
-						} else {
-							create_camera(scene, camera_position, zoom_speed, pan_speed);
+	function load_model(url: string | undefined): void {
+		if (viewer) {
+			viewer
+				.loadModel(url!, {
path: js/model3D/shared/Canvas3D.svelte
text: @user1:
Instead of a non null assertion, if url is undefined you could call resetModel.

@author:
fixed