Skip to content

Commit

Permalink
Create <Canvas3D /> and use it from <Model3D /> and <Model3DUpload />
Browse files Browse the repository at this point in the history
  • Loading branch information
whitphx committed Feb 6, 2024
1 parent f0ec7b2 commit 8ccffff
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 271 deletions.
148 changes: 148 additions & 0 deletions js/model3D/shared/Canvas3D.svelte
@@ -0,0 +1,148 @@
<script lang="ts">
import { onMount } from "svelte";
import * as BABYLON from "babylonjs";
import * as BABYLON_LOADERS from "babylonjs-loaders";
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 clear_color: [number, number, number, number];
export let camera_position: [number | null, number | null, number | null];
export let zoom_speed: number;
export let pan_speed: number;
$: url = value.url;
/* URL resolution for the Wasm mode. */
export let resolved_url: typeof url = undefined; // Exposed to be bound to the download link in the parent component.
// The prop can be updated before the Promise from `resolve_wasm_src` is resolved.
// In such a case, the resolved url for the old `url` has to be discarded,
// This variable `latest_url` is used to pick up only the value resolved for the latest `url`.
let latest_url: typeof url;
$: {
// In normal (non-Wasm) Gradio, the original `url` should be used immediately
// without waiting for `resolve_wasm_src()` to resolve.
// If it waits, a blank element is displayed until the async task finishes
// and it leads to undesirable flickering.
// So set `resolved_url` immediately above, and update it with the resolved values below later.
resolved_url = url;
if (url) {
latest_url = url;
const resolving_url = url;
resolve_wasm_src(url).then((resolved) => {
if (latest_url === resolving_url) {
resolved_url = resolved ?? undefined;
} else {
resolved && URL.revokeObjectURL(resolved);
}
});
}
}
/* BabylonJS engine and scene management */
let canvas: HTMLCanvasElement;
let scene: BABYLON.Scene;
let engine: BABYLON.Engine;
onMount(() => {
// Initialize BabylonJS engine and scene
engine = new BABYLON.Engine(canvas, true);
scene = new BABYLON.Scene(engine);
scene.createDefaultCameraOrLight();
scene.clearColor = scene.clearColor = new BABYLON.Color4(...clear_color);
engine.runRenderLoop(() => {
scene.render();
});
function onWindowResize(): void {
engine.resize();
}
window.addEventListener("resize", onWindowResize);
load_model(resolved_url);
return () => {
scene.dispose();
engine.dispose();
window.removeEventListener("resize", onWindowResize);
};
});
$: 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();
});
// Load the new model
if (url) {
BABYLON.SceneLoader.ShowLoadingScreen = false;
BABYLON.SceneLoader.Append(
url,
"",
scene,
() => create_camera(scene, camera_position, zoom_speed, pan_speed),
undefined,
undefined,
"." + value.path.split(".").pop()
);
}
}
}
function create_camera(
scene: BABYLON.Scene,
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;
if (camera_position[0] !== null) {
helperCamera.alpha = BABYLON.Tools.ToRadians(camera_position[0]);
}
if (camera_position[1] !== null) {
helperCamera.beta = BABYLON.Tools.ToRadians(camera_position[1]);
}
if (camera_position[2] !== null) {
helperCamera.radius = camera_position[2];
}
helperCamera.lowerRadiusLimit = 0.1;
const updateCameraSensibility = (): void => {
helperCamera.wheelPrecision = 250 / (helperCamera.radius * zoom_speed);
helperCamera.panningSensibility =
(10000 * pan_speed) / helperCamera.radius;
};
updateCameraSensibility();
helperCamera.attachControl(true);
helperCamera.onAfterCheckInputsObservable.add(updateCameraSensibility);
}
export function reset_camera_position(
camera_position: [number | null, number | null, number | null],
zoom_speed: number,
pan_speed: number
): void {
if (scene) {
scene.removeCamera(scene.activeCamera!);
create_camera(scene, camera_position, zoom_speed, pan_speed);
}
}
</script>

<canvas bind:this={canvas}></canvas>
121 changes: 21 additions & 100 deletions js/model3D/shared/Model3D.svelte
Expand Up @@ -2,12 +2,8 @@
import type { FileData } from "@gradio/client";
import { BlockLabel, IconButton } from "@gradio/atoms";
import { File, Download, Undo } from "@gradio/icons";
import { add_new_model, reset_camera_position } from "./utils";
import { onMount } from "svelte";
import * as BABYLON from "babylonjs";
import * as BABYLON_LOADERS from "babylonjs-loaders";
import Canvas3D from "./Canvas3D.svelte";
import type { I18nFormatter } from "@gradio/utils";
import { resolve_wasm_src } from "@gradio/wasm/svelte";
import { dequal } from "dequal";
export let value: FileData | null;
Expand All @@ -26,101 +22,20 @@
let current_settings = { camera_position, zoom_speed, pan_speed };
$: {
if (
BABYLON_LOADERS.OBJFileLoader != undefined &&
!BABYLON_LOADERS.OBJFileLoader.IMPORT_VERTEX_COLORS
) {
BABYLON_LOADERS.OBJFileLoader.IMPORT_VERTEX_COLORS = true;
}
}
let canvas: HTMLCanvasElement;
let scene: BABYLON.Scene;
let engine: BABYLON.Engine | null;
let mounted = false;
let resolved_value: typeof value;
/* URL resolution for the Wasm mode. */
// The `value` prop can be updated before the Promise from `resolve_wasm_src` is resolved.
// In such a case, the resolved value for the old `value` has to be discarded,
// This variable `latest_value` is used to pick up only the value resolved for the latest `value` prop.
let latest_value: typeof value;
$: {
// In normal (non-Wasm) Gradio, the original `value` should be used immediately
// without waiting for `resolve_wasm_src()` to resolve.
// If it waits, a blank element is displayed until the async task finishes
// and it leads to undesirable flickering.
// So set `resolved_value` immediately above, and update it with the resolved values below later.
resolved_value = value;
if (value?.url) {
latest_value = value;
const resolving_value = value;
resolve_wasm_src(value.url).then((resolved_url) => {
if (latest_value === resolving_value) {
resolved_value = {
...resolving_value,
url: resolved_url ?? undefined
};
} else {
resolved_url && URL.revokeObjectURL(resolved_url);
}
});
}
}
onMount(() => {
engine = new BABYLON.Engine(canvas, true);
window.addEventListener("resize", () => {
engine?.resize();
});
mounted = true;
});
$: ({ path } = resolved_value || {
path: undefined
});
$: canvas && mounted && path && resolved_value && dispose();
function dispose(): void {
if (scene && !scene.isDisposed) {
scene.dispose();
engine?.stopRenderLoop();
engine?.dispose();
engine = null;
engine = new BABYLON.Engine(canvas, true);
window.addEventListener("resize", () => {
engine?.resize();
});
}
if (engine !== null) {
scene = add_new_model(
canvas,
scene,
engine,
resolved_value,
clear_color,
camera_position,
zoom_speed,
pan_speed
);
}
}
let canvas3d: Canvas3D;
let resolved_url: string | undefined;
function handle_undo(): void {
reset_camera_position(scene, camera_position, zoom_speed, pan_speed);
canvas3d.reset_camera_position(camera_position, zoom_speed, pan_speed);
}
$: {
if (
scene &&
(!dequal(current_settings.camera_position, camera_position) ||
current_settings.zoom_speed !== zoom_speed ||
current_settings.pan_speed !== pan_speed)
!dequal(current_settings.camera_position, camera_position) ||
current_settings.zoom_speed !== zoom_speed ||
current_settings.pan_speed !== pan_speed
) {
reset_camera_position(scene, camera_position, zoom_speed, pan_speed);
canvas3d.reset_camera_position(camera_position, zoom_speed, pan_speed);
current_settings = { camera_position, zoom_speed, pan_speed };
}
}
Expand All @@ -131,22 +46,28 @@
Icon={File}
label={label || i18n("3D_model.3d_model")}
/>
{#if resolved_value}
{#if value}
<div class="model3D">
<div class="buttons">
<IconButton Icon={Undo} label="Undo" on:click={() => handle_undo()} />
<a
href={resolved_value.url}
href={resolved_url}
target={window.__is_colab__ ? "_blank" : null}
download={window.__is_colab__
? null
: resolved_value.orig_name || resolved_value.path}
download={window.__is_colab__ ? null : value.orig_name || value.path}
>
<IconButton Icon={Download} label={i18n("common.download")} />
</a>
</div>

<canvas bind:this={canvas} />
<Canvas3D
bind:this={canvas3d}
bind:resolved_url
{value}
{clear_color}
{camera_position}
{zoom_speed}
{pan_speed}
/>
</div>
{/if}

Expand All @@ -157,7 +78,7 @@
width: var(--size-full);
height: var(--size-full);
}
canvas {
.model3D :global(canvas) {
width: var(--size-full);
height: var(--size-full);
object-fit: contain;
Expand Down

0 comments on commit 8ccffff

Please sign in to comment.