mirror of
https://github.com/gradio-app/gradio.git
synced 2025-03-19 12:00:39 +08:00
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:
parent
3d66e61d64
commit
79d8f9d891
8
.changeset/gold-pears-guess.md
Normal file
8
.changeset/gold-pears-guess.md
Normal 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
|
@ -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,
|
||||
|
@ -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": {
|
||||
|
@ -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)}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
2
pnpm-lock.yaml
generated
@ -1,4 +1,4 @@
|
||||
lockfileVersion: '6.0'
|
||||
lockfileVersion: '6.1'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user