Spaces:
Running
fixed layout to fit 3 across on laptop & fixed reset button in viewer
Browse filesRoot cause — In Gradio 6.10, the Model3D Undo (reset) button is wired to a Svelte-local state has_change_history in Index-DN7eG78L.js:330 that only flips to true inside handle_change, which fires only when the user uploads through Model3D's own upload widget. Since your app supplies the model value programmatically from the gr.File component, that handler never runs, so the button stays permanently disabled. The has_change_history flag is not exposed as a Python prop, so it can't be set from app.py.
Fix — In app.py:
Added APP_HEAD containing a small <script> with a MutationObserver that strips the disabled attribute off any .model3D button[aria-label="Undo"] whenever Svelte sets it. The button's onclick handler is still wired up — disabling only blocks the click — so once enabled, clicks reach handle_undo → canvas3d.reset_camera_position() → viewer.resetCamera().
Added a small CSS rule in APP_CSS so the button no longer renders in its grayed-out state.
Moved css and head from the gr.Blocks(...) constructor to demo.launch(...), since Gradio 6.0 moved those parameters and the old call site was already emitting a deprecation warning.
|
@@ -32,6 +32,55 @@ APP_CSS = """
|
|
| 32 |
.gradio-container .prose {
|
| 33 |
margin-bottom: 0.4rem !important;
|
| 34 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
|
| 37 |
|
|
@@ -202,7 +251,7 @@ def shift_slice(state: ViewerState, index: float, delta: int) -> tuple[int, str,
|
|
| 202 |
|
| 203 |
|
| 204 |
def build_demo() -> gr.Blocks:
|
| 205 |
-
with gr.Blocks(title="STL TIFF Slicer"
|
| 206 |
gr.Markdown(
|
| 207 |
"""
|
| 208 |
# STL to TIFF Slicer
|
|
@@ -226,7 +275,7 @@ def build_demo() -> gr.Blocks:
|
|
| 226 |
|
| 227 |
with gr.Row():
|
| 228 |
for i in range(3):
|
| 229 |
-
with gr.Column():
|
| 230 |
stl_file = gr.File(
|
| 231 |
label=f"STL File {i + 1}",
|
| 232 |
file_types=[".stl"],
|
|
@@ -350,4 +399,4 @@ demo = build_demo()
|
|
| 350 |
|
| 351 |
|
| 352 |
if __name__ == "__main__":
|
| 353 |
-
demo.launch(ssr_mode=False)
|
|
|
|
| 32 |
.gradio-container .prose {
|
| 33 |
margin-bottom: 0.4rem !important;
|
| 34 |
}
|
| 35 |
+
|
| 36 |
+
.model3D button[aria-label="Undo"] {
|
| 37 |
+
color: var(--block-label-text-color) !important;
|
| 38 |
+
cursor: pointer !important;
|
| 39 |
+
opacity: 1 !important;
|
| 40 |
+
}
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
# Gradio 6.10's gr.Model3D leaves the Undo (reset view) button permanently
|
| 44 |
+
# disabled when the value is supplied programmatically — its `has_change_history`
|
| 45 |
+
# state only flips on uploads through Model3D's own upload widget. This script
|
| 46 |
+
# strips the disabled attribute so clicks reach Svelte's handle_undo, which
|
| 47 |
+
# calls reset_camera_position on the underlying canvas.
|
| 48 |
+
APP_HEAD = """
|
| 49 |
+
<script>
|
| 50 |
+
(function () {
|
| 51 |
+
function enableUndoButtons(root) {
|
| 52 |
+
(root || document).querySelectorAll('.model3D button[aria-label="Undo"]').forEach(function (btn) {
|
| 53 |
+
if (btn.disabled) {
|
| 54 |
+
btn.disabled = false;
|
| 55 |
+
}
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
function start() {
|
| 59 |
+
enableUndoButtons();
|
| 60 |
+
var observer = new MutationObserver(function (mutations) {
|
| 61 |
+
for (var i = 0; i < mutations.length; i++) {
|
| 62 |
+
var m = mutations[i];
|
| 63 |
+
if (m.type === 'attributes' && m.target && m.target.matches && m.target.matches('.model3D button[aria-label="Undo"]')) {
|
| 64 |
+
if (m.target.disabled) m.target.disabled = false;
|
| 65 |
+
} else if (m.type === 'childList') {
|
| 66 |
+
enableUndoButtons(m.target);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
});
|
| 70 |
+
observer.observe(document.body, {
|
| 71 |
+
childList: true,
|
| 72 |
+
subtree: true,
|
| 73 |
+
attributes: true,
|
| 74 |
+
attributeFilter: ['disabled']
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
if (document.readyState === 'loading') {
|
| 78 |
+
document.addEventListener('DOMContentLoaded', start);
|
| 79 |
+
} else {
|
| 80 |
+
start();
|
| 81 |
+
}
|
| 82 |
+
})();
|
| 83 |
+
</script>
|
| 84 |
"""
|
| 85 |
|
| 86 |
|
|
|
|
| 251 |
|
| 252 |
|
| 253 |
def build_demo() -> gr.Blocks:
|
| 254 |
+
with gr.Blocks(title="STL TIFF Slicer") as demo:
|
| 255 |
gr.Markdown(
|
| 256 |
"""
|
| 257 |
# STL to TIFF Slicer
|
|
|
|
| 275 |
|
| 276 |
with gr.Row():
|
| 277 |
for i in range(3):
|
| 278 |
+
with gr.Column(min_width=250):
|
| 279 |
stl_file = gr.File(
|
| 280 |
label=f"STL File {i + 1}",
|
| 281 |
file_types=[".stl"],
|
|
|
|
| 399 |
|
| 400 |
|
| 401 |
if __name__ == "__main__":
|
| 402 |
+
demo.launch(ssr_mode=False, css=APP_CSS, head=APP_HEAD)
|