Spaces:
Runtime error
Runtime error
| <script lang="ts"> | |
| import type { FileData } from "@gradio/client"; | |
| import { BlockLabel, IconButton } from "@gradio/atoms"; | |
| import { File, Download, Undo } from "@gradio/icons"; | |
| import { | |
| add_new_light, | |
| add_new_model, | |
| change_camera_position, | |
| change_light, | |
| change_show_axes, | |
| reset_camera_position, | |
| zoom, | |
| } from "./utils"; | |
| import { onMount } from "svelte"; | |
| import BABYLON from "babylonjs"; | |
| import BABYLON_LOADERS from "babylonjs-loaders"; | |
| import type { I18nFormatter } from "@gradio/utils"; | |
| import Plus from "../icons/Plus.svelte"; | |
| import Minus from "../icons/Minus.svelte"; | |
| export let value: FileData | null; | |
| export let clear_color: [number, number, number, number] = [0, 0, 0, 0]; | |
| export let label = ""; | |
| export let show_label: boolean; | |
| export let i18n: I18nFormatter; | |
| export let zoom_speed = 1; | |
| export let pan_speed = 1; | |
| export let show_axes = false; | |
| const light_list = ["HemiLight", "pointLight", "DirectionLight"]; | |
| export let lights: { type: string; position: [number, number, number] }[] = [ | |
| { | |
| type: "HemiLight", | |
| position: [0, 1, 0], | |
| }, | |
| ]; | |
| let visible_submenu: null | string = null; | |
| const label_dict = { | |
| 1: "X", | |
| 2: "Y", | |
| 3: "Z", | |
| }; | |
| // alpha, beta, radius | |
| export let camera_position: [number | null, number | null, number | null] = [ | |
| null, | |
| null, | |
| null, | |
| ]; | |
| $: { | |
| 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; | |
| onMount(() => { | |
| engine = new BABYLON.Engine(canvas, true); | |
| window.addEventListener("resize", () => { | |
| engine?.resize(); | |
| }); | |
| mounted = true; | |
| }); | |
| $: ({ path } = value || { | |
| path: undefined, | |
| }); | |
| $: canvas && mounted && path && 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, | |
| value, | |
| clear_color, | |
| camera_position, | |
| zoom_speed, | |
| pan_speed | |
| ); | |
| } | |
| } | |
| function handle_undo(): void { | |
| reset_camera_position(scene, camera_position, zoom_speed, pan_speed); | |
| } | |
| $: if (scene) | |
| reset_camera_position(scene, camera_position, zoom_speed, pan_speed); | |
| </script> | |
| <BlockLabel | |
| {show_label} | |
| Icon={File} | |
| label={label || i18n("3D_model.3d_model")} | |
| /> | |
| {#if value} | |
| <div class="model3D"> | |
| <div class="buttons"> | |
| <IconButton | |
| Icon={Plus} | |
| label={"Zoom In"} | |
| on:click={() => zoom("in", scene, zoom_speed)} | |
| /> | |
| <IconButton | |
| Icon={Minus} | |
| label={"Zoom Out"} | |
| on:click={() => zoom("out", scene, zoom_speed)} | |
| /> | |
| <IconButton Icon={Undo} label="Undo" on:click={() => handle_undo()} /> | |
| <a | |
| href={value.path} | |
| target={window.__is_colab__ ? "_blank" : null} | |
| download={window.__is_colab__ ? null : value.orig_name || value.path} | |
| > | |
| <IconButton Icon={Download} label={i18n("common.download")} /> | |
| </a> | |
| </div> | |
| <canvas bind:this={canvas} /> | |
| <div class="light-menu"> | |
| <span class="input-container"> | |
| <div class="input-wrapper-axes"> | |
| <label for="axes">Show Axes</label> | |
| <input | |
| type="checkbox" | |
| bind:checked={show_axes} | |
| on:change={() => { | |
| if (show_axes) { | |
| change_show_axes(scene, show_axes); | |
| } else { | |
| dispose(); | |
| } | |
| }} | |
| /> | |
| </div> | |
| </span> | |
| <button | |
| class="input-title" | |
| aria-label="Toggle open camera submenu" | |
| on:click={() => { | |
| if (visible_submenu === "camera") { | |
| visible_submenu = null; | |
| } else { | |
| visible_submenu = "camera"; | |
| } | |
| }} | |
| ><span | |
| class="caret" | |
| style:transform={visible_submenu === "camera" | |
| ? "rotateZ(180deg)" | |
| : "rotateZ(90deg)"} | |
| /> Camera Position</button | |
| > | |
| {#if visible_submenu === "camera"} | |
| <span class="input-wrapper"> | |
| {#each camera_position as camera_value, index} | |
| <span class="input-container"> | |
| <label for="position">Axis {label_dict[index + 1]} </label> | |
| <input | |
| type="number" | |
| class={`position-${index}`} | |
| min="0" | |
| max="999" | |
| step="5" | |
| placeholder="0" | |
| bind:value={camera_value} | |
| on:change={() => | |
| change_camera_position(scene, camera_position, zoom_speed)} | |
| /> | |
| </span> | |
| {/each} | |
| </span> | |
| {/if} | |
| <button | |
| aria-label="Toggle open light submenu" | |
| class="input-title" | |
| on:click={() => { | |
| if (visible_submenu === "light") { | |
| visible_submenu = null; | |
| } else { | |
| visible_submenu = "light"; | |
| } | |
| }} | |
| ><span | |
| class="caret" | |
| style:transform={visible_submenu === "light" | |
| ? "rotateZ(180deg)" | |
| : "rotateZ(90deg)"} | |
| /> Light Position</button | |
| > | |
| {#if visible_submenu === "light"} | |
| {#each lights as light, index} | |
| <span> | |
| <label for="light">Light Type</label> | |
| <select | |
| class="light-dropdown" | |
| name="light" | |
| bind:value={light.type} | |
| on:change={(e) => { | |
| change_light( | |
| scene, | |
| e.currentTarget.value, | |
| light.position, | |
| index | |
| ); | |
| }} | |
| > | |
| {#each light_list as light_type} | |
| <option value={light_type}>{light_type}</option> | |
| {/each} | |
| </select> | |
| </span> | |
| <span class="input-wrapper"> | |
| {#each light.position as light_value, index} | |
| <span class="input-container"> | |
| <label for="light">Axis {label_dict[index + 1]} </label> | |
| <input | |
| type="number" | |
| class={`light-${index}`} | |
| min="0" | |
| max="5" | |
| step="0.1" | |
| placeholder="0" | |
| bind:value={light_value} | |
| on:change={() => | |
| change_light(scene, light.type, light.position, index)} | |
| /> | |
| </span> | |
| {/each} | |
| </span> | |
| <div class="separator" /> | |
| {/each} | |
| <button | |
| class="add" | |
| disabled={lights.length >= 5} | |
| on:click={() => { | |
| if (lights.length < 5) { | |
| lights = [...lights, { type: "HemiLight", position: [0, 1, 0] }]; | |
| add_new_light(scene, [0, 1, 0]); | |
| } | |
| }}>Add Light</button | |
| > | |
| {/if} | |
| </div> | |
| </div> | |
| {/if} | |
| <style> | |
| .separator { | |
| border-bottom: 1px solid var(--border-color-primary); | |
| margin: 10px 0; | |
| width: 100%; | |
| } | |
| .add { | |
| border: var(--button-border-width) solid | |
| var(--button-secondary-border-color); | |
| background: var(--button-secondary-background-fill); | |
| color: var(--button-secondary-text-color); | |
| margin: 5px 0; | |
| } | |
| .add:disabled { | |
| background-color: var(--button-secondary-background-fill-disabled); | |
| cursor: not-allowed; | |
| opacity: 0.5; | |
| filter: grayscale(30%); | |
| } | |
| .input-container { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .light-dropdown { | |
| all: unset; | |
| border: var(--button-border-width) solid | |
| var(--button-secondary-border-color); | |
| background: var(--button-secondary-background-fill); | |
| color: var(--button-secondary-text-color); | |
| margin: 5px 0; | |
| width: min-content; | |
| height: min-content; | |
| font-size: var(--text-md); | |
| padding: 5px; | |
| position: relative; | |
| text-align: left; | |
| } | |
| .input-wrapper { | |
| display: flex; | |
| flex-direction: row; | |
| } | |
| .caret { | |
| display: inline-flex; | |
| border-left: solid 5px transparent; | |
| border-right: solid 5px transparent; | |
| border-bottom: solid 5px #fff; | |
| transform: rotateZ(90deg); | |
| top: -2px; | |
| position: relative; | |
| } | |
| .input-wrapper input[type="number"] { | |
| width: min-content; | |
| height: min-content; | |
| display: inline-block; | |
| background: var(--input-background-fill); | |
| border: 1px solid var(--input-border-color); | |
| padding: 0 5px; | |
| color: var(--body-text-color); | |
| margin: 0 2px; | |
| } | |
| .input-title { | |
| color: white; | |
| margin: 5px 0; | |
| text-align: right; | |
| } | |
| .input-wrapper-axes { | |
| display: flex; | |
| flex-direction: row; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .input-wrapper-axes input[type="checkbox"] { | |
| background-color: var(--checkbox-background-color); | |
| } | |
| input[type="checkbox"]:checked { | |
| background-color: var(--checkbox-background-color); | |
| border: 1px solid var(--checkbox-border-color-selected); | |
| } | |
| .light-menu { | |
| display: flex; | |
| flex-direction: column; | |
| position: absolute; | |
| text-align: right; | |
| bottom: 0; | |
| right: 0; | |
| margin: 10px; | |
| min-width: 200px; | |
| color: black; | |
| background-color: rgba(11, 15, 25, 0.6); | |
| border: 1px solid var(--border-color-primary); | |
| border-radius: var(--table-radius); | |
| padding: 15px; | |
| max-height: 90%; | |
| overflow-y: scroll; | |
| } | |
| .light-menu label { | |
| font-size: 12px; | |
| margin-right: 5px; | |
| color: white; | |
| font: var(--font); | |
| width: 100%; | |
| padding: 5px; | |
| } | |
| .model3D { | |
| display: flex; | |
| position: relative; | |
| width: var(--size-full); | |
| height: var(--size-full); | |
| } | |
| canvas { | |
| width: var(--size-full); | |
| height: var(--size-full); | |
| object-fit: contain; | |
| overflow: hidden; | |
| } | |
| .buttons { | |
| display: flex; | |
| position: absolute; | |
| top: var(--size-2); | |
| right: var(--size-2); | |
| justify-content: flex-end; | |
| gap: var(--spacing-sm); | |
| z-index: var(--layer-5); | |
| } | |
| </style> | |