MichaelRKessler commited on
Commit
4776a40
·
1 Parent(s): bedcfb7

Simplify STL viewer layout

Browse files
Files changed (2) hide show
  1. README.md +2 -1
  2. app.py +85 -4
README.md CHANGED
@@ -1,6 +1,6 @@
1
  # STL to TIFF Gradio App
2
 
3
- This project provides a Gradio app that takes an uploaded STL file, slices it along the Z axis, saves each slice as a TIFF image, and lets you browse the stack inside the UI.
4
 
5
  ## Run
6
 
@@ -14,6 +14,7 @@ Then open the local Gradio URL in your browser, upload an STL file, and generate
14
  ## What the app does
15
 
16
  - Uploads a single `.stl` file
 
17
  - Lets you choose layer height and XY pixel size
18
  - Produces one `.tif` image per slice
19
  - Lets you step through the slice stack in the browser
 
1
  # STL to TIFF Gradio App
2
 
3
+ This project provides a Gradio app that takes an uploaded STL file, shows an interactive 3D viewer, slices it along the Z axis, saves each slice as a TIFF image, and lets you browse the stack inside the UI.
4
 
5
  ## Run
6
 
 
14
  ## What the app does
15
 
16
  - Uploads a single `.stl` file
17
+ - Shows an interactive 3D viewer for rotating the model
18
  - Lets you choose layer height and XY pixel size
19
  - Produces one `.tif` image per slice
20
  - Lets you step through the slice stack in the browser
app.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any
6
  import gradio as gr
7
  from PIL import Image
8
 
9
- from stl_slicer import SliceStack, slice_stl_to_tiffs
10
 
11
 
12
  ViewerState = dict[str, Any]
@@ -21,6 +21,10 @@ def _empty_state() -> ViewerState:
21
  return {"tiff_paths": [], "z_values": []}
22
 
23
 
 
 
 
 
24
  def _stack_to_state(stack: SliceStack) -> ViewerState:
25
  return {
26
  "tiff_paths": [str(path) for path in stack.tiff_paths],
@@ -43,6 +47,30 @@ def _format_summary(stack: SliceStack, source_name: str) -> str:
43
  )
44
 
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  def _slice_label(state: ViewerState, index: int) -> str:
47
  path = Path(state["tiff_paths"][index]).name
48
  z_value = state["z_values"][index]
@@ -64,6 +92,36 @@ def _render_selected_slice(state: ViewerState, index: int) -> tuple[str, Image.I
64
  )
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def generate_stack(
68
  stl_file: str | None,
69
  layer_height: float,
@@ -123,8 +181,8 @@ def build_demo() -> gr.Blocks:
123
  gr.Markdown(
124
  """
125
  # STL to TIFF Slicer
126
- Upload an STL, choose a layer height and XY pixel size, and generate a TIFF stack.
127
- Use the slider or the previous and next buttons to browse through the resulting slices.
128
  """
129
  )
130
 
@@ -155,12 +213,19 @@ def build_demo() -> gr.Blocks:
155
 
156
  with gr.Column(scale=2):
157
  summary = gr.Markdown("Upload an STL file to begin.")
 
 
 
 
 
 
 
158
  slice_label = gr.Markdown("No slice stack loaded yet.")
159
  slice_preview = gr.Image(
160
  label="Slice Preview",
161
  type="pil",
162
  image_mode="L",
163
- height=520,
164
  )
165
  with gr.Row():
166
  prev_button = gr.Button("Previous Slice")
@@ -174,6 +239,22 @@ def build_demo() -> gr.Blocks:
174
  interactive=False,
175
  )
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  generate_button.click(
178
  fn=generate_stack,
179
  inputs=[stl_file, layer_height, pixel_size],
 
6
  import gradio as gr
7
  from PIL import Image
8
 
9
+ from stl_slicer import SliceStack, load_mesh, slice_stl_to_tiffs
10
 
11
 
12
  ViewerState = dict[str, Any]
 
21
  return {"tiff_paths": [], "z_values": []}
22
 
23
 
24
+ def _reset_slider() -> dict[str, Any]:
25
+ return gr.update(minimum=0, maximum=0, value=0, step=1, interactive=False)
26
+
27
+
28
  def _stack_to_state(stack: SliceStack) -> ViewerState:
29
  return {
30
  "tiff_paths": [str(path) for path in stack.tiff_paths],
 
47
  )
48
 
49
 
50
+ def _format_model_status(source_name: str) -> str:
51
+ return "\n".join(
52
+ [
53
+ "### Model Loaded",
54
+ f"- Source: `{source_name}`",
55
+ "- Rotate the model in the 3D viewer, then choose slice settings and generate the TIFF stack.",
56
+ ]
57
+ )
58
+
59
+
60
+ def _format_model_details(source_name: str, mesh) -> str:
61
+ extents = mesh.extents
62
+ return "\n".join(
63
+ [
64
+ "### Model Details",
65
+ f"- Source: `{source_name}`",
66
+ f"- Extents: `{extents[0]:.3f} x {extents[1]:.3f} x {extents[2]:.3f}`",
67
+ f"- Faces: `{len(mesh.faces)}`",
68
+ f"- Vertices: `{len(mesh.vertices)}`",
69
+ f"- Watertight: `{'yes' if mesh.is_watertight else 'no'}`",
70
+ ]
71
+ )
72
+
73
+
74
  def _slice_label(state: ViewerState, index: int) -> str:
75
  path = Path(state["tiff_paths"][index]).name
76
  z_value = state["z_values"][index]
 
92
  )
93
 
94
 
95
+ def load_model_assets(stl_file: str | None):
96
+ if not stl_file:
97
+ return (
98
+ "Upload an STL file to begin.",
99
+ _empty_state(),
100
+ _reset_slider(),
101
+ "No slice stack loaded yet.",
102
+ None,
103
+ None,
104
+ None,
105
+ None,
106
+ "No model loaded yet.",
107
+ )
108
+
109
+ mesh = load_mesh(stl_file)
110
+ source_name = Path(stl_file).name
111
+
112
+ return (
113
+ _format_model_status(source_name),
114
+ _empty_state(),
115
+ _reset_slider(),
116
+ "No slice stack loaded yet.",
117
+ None,
118
+ None,
119
+ None,
120
+ stl_file,
121
+ _format_model_details(source_name, mesh),
122
+ )
123
+
124
+
125
  def generate_stack(
126
  stl_file: str | None,
127
  layer_height: float,
 
181
  gr.Markdown(
182
  """
183
  # STL to TIFF Slicer
184
+ Upload an STL to inspect it in a rotatable 3D viewer.
185
+ Then choose a layer height and XY pixel size, generate the TIFF stack, and browse the slices below.
186
  """
187
  )
188
 
 
213
 
214
  with gr.Column(scale=2):
215
  summary = gr.Markdown("Upload an STL file to begin.")
216
+ model_details = gr.Markdown("No model loaded yet.")
217
+ model_viewer = gr.Model3D(
218
+ label="Interactive 3D Viewer",
219
+ display_mode="solid",
220
+ clear_color=(0.94, 0.95, 0.97, 1.0),
221
+ height=360,
222
+ )
223
  slice_label = gr.Markdown("No slice stack loaded yet.")
224
  slice_preview = gr.Image(
225
  label="Slice Preview",
226
  type="pil",
227
  image_mode="L",
228
+ height=420,
229
  )
230
  with gr.Row():
231
  prev_button = gr.Button("Previous Slice")
 
239
  interactive=False,
240
  )
241
 
242
+ stl_file.change(
243
+ fn=load_model_assets,
244
+ inputs=stl_file,
245
+ outputs=[
246
+ summary,
247
+ state,
248
+ slice_slider,
249
+ slice_label,
250
+ slice_preview,
251
+ download_zip,
252
+ current_tiff,
253
+ model_viewer,
254
+ model_details,
255
+ ],
256
+ )
257
+
258
  generate_button.click(
259
  fn=generate_stack,
260
  inputs=[stl_file, layer_height, pixel_size],