Adds height and zoom_speed parameters to Model3D component, as well as a button to reset the camera position (#5373)

* add params in backend

* tweaks

* add changeset

* updates

* tweaks

* lint

* cleanup

* fix tests

* add reset button

* add changeset

* lint

* add changeset

* add changeset

* lint

* add changeset

* faster

* add changeset

* undo website

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Abubakar Abid 2023-08-31 12:24:22 -07:00 committed by GitHub
parent 3d66e61d64
commit 79d8f9d891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 142 additions and 48 deletions

View File

@ -0,0 +1,8 @@
---
"@gradio/app": minor
"@gradio/model3d": minor
"@gradio/upload": minor
"gradio": minor
---
feat:Adds `height` and `zoom_speed` parameters to `Model3D` component, as well as a button to reset the camera position

View File

@ -45,6 +45,8 @@ class Model3D(
None,
None,
),
zoom_speed: float = 1,
height: int | None = None,
label: str | None = None,
show_label: bool | None = None,
every: float | None = None,
@ -61,6 +63,8 @@ class Model3D(
value: path to (.obj, glb, or .gltf) file to show in model3D viewer. If callable, the function will be called whenever the app loads to set the initial value of the component.
clear_color: background color of scene, should be a tuple of 4 floats between 0 and 1 representing RGBA values.
camera_position: initial camera position of scene, provided as a tuple of `(alpha, beta, radius)`. Each value is optional. If provided, `alpha` and `beta` should be in degrees reflecting the angular position along the longitudinal and latitudinal axes, respectively. Radius corresponds to the distance from the center of the object to the camera.
zoom_speed: the speed of zooming in and out of the scene when the cursor wheel is rotated or when screen is pinched on a mobile device. Should be a positive float, increase this value to make zooming faster, decrease to make it slower. Affects the wheelPrecision property of the camera.
height: height of the model3D component, in pixels.
label: component name in interface.
show_label: if True, will display label.
every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
@ -73,6 +77,8 @@ class Model3D(
"""
self.clear_color = clear_color or [0, 0, 0, 0]
self.camera_position = camera_position
self.height = height
self.zoom_speed = zoom_speed
IOComponent.__init__(
self,
@ -94,6 +100,8 @@ class Model3D(
"clear_color": self.clear_color,
"value": self.value,
"camera_position": self.camera_position,
"height": self.height,
"zoom_speed": self.zoom_speed,
**IOComponent.get_config(self),
}
@ -111,6 +119,8 @@ class Model3D(
]
| None = None,
clear_color: tuple[float, float, float, float] | None = None,
height: int | None = None,
zoom_speed: float | None = None,
label: str | None = None,
show_label: bool | None = None,
container: bool | None = None,
@ -121,6 +131,8 @@ class Model3D(
updated_config = {
"camera_position": camera_position,
"clear_color": clear_color,
"height": height,
"zoom_speed": zoom_speed,
"label": label,
"show_label": show_label,
"container": container,

View File

@ -40,7 +40,8 @@
"or": "or",
"remove": "Remove",
"share": "Share",
"submit": "Submit"
"submit": "Submit",
"undo": "Undo"
},
"dataframe": {
"incorrect_format": "Incorrect format, only CSV and TSV files are supported",
@ -74,7 +75,6 @@
"remove_image": "Remove Image",
"select_brush_color": "Select brush color",
"start_drawing": "Start drawing",
"undo": "Undo",
"use_brush": "Use brush"
},
"label": {

View File

@ -26,6 +26,8 @@
change: typeof value;
clear: never;
}>;
export let zoom_speed = 1;
export let height: number | undefined = undefined;
// alpha, beta, radius
export let camera_position: [number | null, number | null, number | null] = [
@ -50,6 +52,7 @@
{container}
{scale}
{min_width}
{height}
>
<StatusTracker {...loading_status} />
@ -59,6 +62,7 @@
{clear_color}
value={_value}
{camera_position}
{zoom_speed}
on:change={({ detail }) => (value = detail)}
on:drag={({ detail }) => (dragging = detail)}
on:change={({ detail }) => gradio.dispatch("change", detail)}

View File

@ -4,12 +4,13 @@
import type { FileData } from "@gradio/upload";
import { BlockLabel } from "@gradio/atoms";
import { File } from "@gradio/icons";
import { add_new_model } from "../shared/utils";
import { add_new_model, reset_camera_position } from "../shared/utils";
export let value: null | FileData;
export let clear_color: [number, number, number, number] = [0, 0, 0, 0];
export let label = "";
export let show_label: boolean;
export let zoom_speed = 1;
// alpha, beta, radius
export let camera_position: [number | null, number | null, number | null] = [
@ -19,10 +20,25 @@
];
let mounted = false;
let canvas: HTMLCanvasElement;
let scene: BABYLON.Scene;
let engine: BABYLON.Engine;
function reset_scene(): void {
scene = add_new_model(
canvas,
scene,
engine,
value,
clear_color,
camera_position,
zoom_speed
);
}
onMount(() => {
if (value != null) {
add_new_model(canvas, scene, engine, value, clear_color, camera_position);
reset_scene();
}
mounted = true;
});
@ -33,19 +49,15 @@
name: undefined
});
$: canvas &&
mounted &&
data != null &&
is_file &&
add_new_model(canvas, scene, engine, value, clear_color, camera_position);
$: canvas && mounted && data != null && is_file && reset_scene();
async function handle_upload({
detail
}: CustomEvent<FileData>): Promise<void> {
value = detail;
await tick();
reset_scene();
dispatch("change", value);
add_new_model(canvas, scene, engine, value, clear_color, camera_position);
}
async function handle_clear(): Promise<void> {
@ -58,6 +70,10 @@
dispatch("clear");
}
async function handle_undo(): Promise<void> {
reset_camera_position(scene, camera_position, zoom_speed);
}
const dispatch = createEventDispatcher<{
change: FileData | null;
clear: undefined;
@ -71,10 +87,6 @@
BABYLON_LOADERS.OBJFileLoader.IMPORT_VERTEX_COLORS = true;
let canvas: HTMLCanvasElement;
let scene: BABYLON.Scene;
let engine: BABYLON.Engine;
$: dispatch("drag", dragging);
</script>
@ -86,7 +98,12 @@
</Upload>
{:else}
<div class="input-model">
<ModifyUpload on:clear={handle_clear} absolute />
<ModifyUpload
undoable
on:clear={handle_clear}
on:undo={handle_undo}
absolute
/>
<canvas bind:this={canvas} />
</div>
{/if}
@ -98,7 +115,7 @@
justify-content: center;
align-items: center;
width: var(--size-full);
height: var(--size-64);
height: var(--size-full);
}
canvas {

View File

@ -1,14 +1,38 @@
import type { FileData } from "@gradio/upload";
import * as BABYLON from "babylonjs";
const create_camera = (
scene: BABYLON.Scene,
camera_position: [number | null, number | null, number | null],
zoom_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];
}
// Disable panning. Adapted from: https://playground.babylonjs.com/#4U6TVQ#3
helperCamera.panningSensibility = 0;
helperCamera.attachControl(false, false, -1);
helperCamera.pinchToPanMaxDistance = 0;
helperCamera.wheelPrecision = 2500 / zoom_speed;
};
export const add_new_model = (
canvas: HTMLCanvasElement,
scene: BABYLON.Scene,
engine: BABYLON.Engine,
value: FileData | null,
clear_color: [number, number, number, number],
camera_position: [number | null, number | null, number | null]
): void => {
camera_position: [number | null, number | null, number | null],
zoom_speed: number
): BABYLON.Scene => {
if (scene && !scene.isDisposed && engine) {
scene.dispose();
engine.dispose();
@ -27,8 +51,7 @@ export const add_new_model = (
engine.resize();
});
if (!value) return;
if (!value) return scene;
let url: string;
if (value.is_file) {
url = value.data;
@ -38,31 +61,24 @@ export const add_new_model = (
let blob = new Blob([raw_content]);
url = URL.createObjectURL(blob);
}
BABYLON.SceneLoader.ShowLoadingScreen = false;
BABYLON.SceneLoader.Append(
url,
"",
scene,
() => {
// scene.createDefaultCamera(createArcRotateCamera, replace, attachCameraControls)
scene.createDefaultCamera(true, true, true);
// scene.activeCamera has to be an ArcRotateCamera if the call succeeds,
// we assume it does
var helperCamera = scene.activeCamera! as BABYLON.ArcRotateCamera;
if (camera_position[0] !== null) {
helperCamera.alpha = (Math.PI * camera_position[0]) / 180;
}
if (camera_position[1] !== null) {
helperCamera.beta = (Math.PI * camera_position[1]) / 180;
}
if (camera_position[2] !== null) {
helperCamera.radius = camera_position[2];
}
},
() => create_camera(scene, camera_position, zoom_speed),
undefined,
undefined,
"." + value.name.split(".")[1]
);
return scene;
};
export const reset_camera_position = (
scene: BABYLON.Scene,
camera_position: [number | null, number | null, number | null],
zoom_speed: number
): void => {
scene.removeCamera(scene.activeCamera!);
create_camera(scene, camera_position, zoom_speed);
};

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type { FileData } from "@gradio/upload";
import { BlockLabel, IconButton } from "@gradio/atoms";
import { File, Download } from "@gradio/icons";
import { add_new_model } from "../shared/utils";
import { File, Download, Undo } from "@gradio/icons";
import { add_new_model, reset_camera_position } from "../shared/utils";
import { _ } from "svelte-i18n";
import { onMount } from "svelte";
import * as BABYLON from "babylonjs";
@ -12,6 +12,7 @@
export let clear_color: [number, number, number, number] = [0, 0, 0, 0];
export let label = "";
export let show_label: boolean;
export let zoom_speed = 1;
// alpha, beta, radius
export let camera_position: [number | null, number | null, number | null] = [
@ -54,15 +55,28 @@
});
}
if (engine !== null) {
add_new_model(canvas, scene, engine, value, clear_color, camera_position);
scene = add_new_model(
canvas,
scene,
engine,
value,
clear_color,
camera_position,
zoom_speed
);
}
}
function handle_undo(): void {
reset_camera_position(scene, camera_position, zoom_speed);
}
</script>
<BlockLabel {show_label} Icon={File} label={label || $_("3D_model.3d_model")} />
{#if value}
<div class="model3D">
<div class="download">
<div class="buttons">
<IconButton Icon={Undo} label="Undo" on:click={() => handle_undo()} />
<a
href={value.data}
target={window.__is_colab__ ? "_blank" : null}
@ -89,9 +103,13 @@
object-fit: contain;
overflow: hidden;
}
.download {
.buttons {
display: flex;
position: absolute;
top: 6px;
right: 6px;
top: var(--size-2);
right: var(--size-2);
justify-content: flex-end;
gap: var(--spacing-sm);
z-index: var(--layer-5);
}
</style>

View File

@ -22,6 +22,8 @@
export let container = true;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let height: number | undefined = undefined;
export let zoom_speed = 1;
// alpha, beta, radius
export let camera_position: [number | null, number | null, number | null] = [
@ -46,6 +48,7 @@
{container}
{scale}
{min_width}
{height}
>
<StatusTracker {...loading_status} />
@ -56,6 +59,7 @@
{label}
{show_label}
{camera_position}
{zoom_speed}
/>
{:else}
<!-- Not ideal but some bugs to work out before we can

View File

@ -1,14 +1,19 @@
<script lang="ts">
import { IconButton } from "@gradio/atoms";
import { Edit, Clear } from "@gradio/icons";
import { Edit, Clear, Undo } from "@gradio/icons";
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
export let editable = false;
export let undoable = false;
export let absolute = true;
const dispatch = createEventDispatcher<{ edit: never; clear: never }>();
const dispatch = createEventDispatcher<{
edit: never;
clear: never;
undo: never;
}>();
</script>
<div
@ -23,6 +28,14 @@
/>
{/if}
{#if undoable}
<IconButton
Icon={Undo}
label={$_("common.undo")}
on:click={() => dispatch("undo")}
/>
{/if}
<IconButton
Icon={Clear}
label={$_("common.clear")}

2
pnpm-lock.yaml generated
View File

@ -1,4 +1,4 @@
lockfileVersion: '6.0'
lockfileVersion: '6.1'
settings:
autoInstallPeers: true

View File

@ -2150,6 +2150,8 @@ class TestModel3D:
"min_width": 160,
"scale": None,
"camera_position": (None, None, None),
"height": None,
"zoom_speed": 1,
} == component.get_config()
file = "test/test_files/Box.gltf"