MichaelRKessler commited on
Commit
32ac972
·
1 Parent(s): 1ccc4b3

updated to allow 3 files at once and load sample stls

Browse files
CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
app.py CHANGED
@@ -10,6 +10,8 @@ from stl_slicer import SliceStack, load_mesh, slice_stl_to_tiffs
10
 
11
 
12
  ViewerState = dict[str, Any]
 
 
13
 
14
 
15
  def _read_slice_preview(path: str) -> Image.Image:
@@ -47,16 +49,6 @@ def _format_summary(stack: SliceStack, source_name: str) -> str:
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(
@@ -78,102 +70,131 @@ def _slice_label(state: ViewerState, index: int) -> str:
78
  return f"Slice {index + 1} / {total} | z = {z_value:.4f} | {path}"
79
 
80
 
81
- def _render_selected_slice(state: ViewerState, index: int) -> tuple[str, Image.Image | None, str | None]:
82
  tiff_paths = state.get("tiff_paths", [])
83
  if not tiff_paths:
84
- return "No slice stack loaded yet.", None, None
85
 
86
  bounded_index = max(0, min(int(index), len(tiff_paths) - 1))
87
  selected_path = tiff_paths[bounded_index]
88
  return (
89
  _slice_label(state, bounded_index),
90
  _read_slice_preview(selected_path),
91
- selected_path,
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,
128
  pixel_size: float,
129
  progress: gr.Progress = gr.Progress(),
130
  ):
131
- if not stl_file:
132
- raise gr.Error("Upload an STL file before generating slices.")
133
-
134
- def report_progress(current: int, total: int) -> None:
135
- progress(current / total, desc=f"Rendering slice {current} of {total}")
136
-
137
- stack = slice_stl_to_tiffs(
138
- stl_file,
139
- layer_height=layer_height,
140
- pixel_size=pixel_size,
141
- progress_callback=report_progress,
142
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- state = _stack_to_state(stack)
145
- label, preview, selected_path = _render_selected_slice(state, 0)
146
- slider_update = gr.update(
147
- minimum=0,
148
- maximum=max(0, len(stack.tiff_paths) - 1),
149
- value=0,
150
- step=1,
151
- interactive=len(stack.tiff_paths) > 1,
152
- )
153
-
154
- return (
155
- _format_summary(stack, Path(stl_file).name),
156
- state,
157
- slider_update,
158
- label,
159
- preview,
160
- str(stack.zip_path),
161
- selected_path,
162
- )
163
 
164
 
165
- def jump_to_slice(state: ViewerState, index: float) -> tuple[str, Image.Image | None, str | None]:
166
  return _render_selected_slice(state, int(index))
167
 
168
 
169
- def shift_slice(state: ViewerState, index: float, delta: int) -> tuple[int, str, Image.Image | None, str | None]:
170
  tiff_paths = state.get("tiff_paths", [])
171
  if not tiff_paths:
172
- return 0, "No slice stack loaded yet.", None, None
173
 
174
  new_index = max(0, min(int(index) + delta, len(tiff_paths) - 1))
175
- label, preview, selected_path = _render_selected_slice(state, new_index)
176
- return new_index, label, preview, selected_path
177
 
178
 
179
  def build_demo() -> gr.Blocks:
@@ -181,113 +202,145 @@ def build_demo() -> gr.Blocks:
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
 
189
- state = gr.State(_empty_state())
190
-
191
  with gr.Row():
192
- with gr.Column(scale=1):
193
- stl_file = gr.File(
194
- label="STL File",
195
- file_types=[".stl"],
196
- type="filepath",
197
- )
198
- layer_height = gr.Number(
199
- label="Layer Height",
200
- value=0.1,
201
- minimum=0.0001,
202
- step=0.01,
203
- )
204
- pixel_size = gr.Number(
205
- label="Pixel Size",
206
- value=0.05,
207
- minimum=0.0001,
208
- step=0.01,
209
- )
210
- generate_button = gr.Button("Generate TIFF Stack", variant="primary")
211
- download_zip = gr.File(label="Download All TIFFs (ZIP)")
212
- current_tiff = gr.File(label="Current TIFF Slice")
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")
232
- next_button = gr.Button("Next Slice")
233
- slice_slider = gr.Slider(
234
- label="Slice Index",
235
- minimum=0,
236
- maximum=0,
237
- value=0,
238
- step=1,
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],
261
- outputs=[
262
- summary,
263
- state,
264
- slice_slider,
265
- slice_label,
266
- slice_preview,
267
- download_zip,
268
- current_tiff,
269
- ],
270
- )
271
-
272
- slice_slider.release(
273
- fn=jump_to_slice,
274
- inputs=[state, slice_slider],
275
- outputs=[slice_label, slice_preview, current_tiff],
276
- queue=False,
277
- )
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
- prev_button.click(
280
- fn=lambda state_value, index: shift_slice(state_value, index, -1),
281
- inputs=[state, slice_slider],
282
- outputs=[slice_slider, slice_label, slice_preview, current_tiff],
283
- queue=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  )
285
 
286
- next_button.click(
287
- fn=lambda state_value, index: shift_slice(state_value, index, 1),
288
- inputs=[state, slice_slider],
289
- outputs=[slice_slider, slice_label, slice_preview, current_tiff],
290
- queue=False,
291
  )
292
 
293
  return demo
 
10
 
11
 
12
  ViewerState = dict[str, Any]
13
+ SAMPLE_STL_FILENAMES = ("Hollow_Pyramid.stl", "balanced_die.stl", "halfsphere.stl")
14
+ SAMPLE_STL_DIR = Path(__file__).resolve().parent / "sample_stls"
15
 
16
 
17
  def _read_slice_preview(path: str) -> Image.Image:
 
49
  )
50
 
51
 
 
 
 
 
 
 
 
 
 
 
52
  def _format_model_details(source_name: str, mesh) -> str:
53
  extents = mesh.extents
54
  return "\n".join(
 
70
  return f"Slice {index + 1} / {total} | z = {z_value:.4f} | {path}"
71
 
72
 
73
+ def _render_selected_slice(state: ViewerState, index: int) -> tuple[str, Image.Image | None]:
74
  tiff_paths = state.get("tiff_paths", [])
75
  if not tiff_paths:
76
+ return "No slice stack loaded yet.", None
77
 
78
  bounded_index = max(0, min(int(index), len(tiff_paths) - 1))
79
  selected_path = tiff_paths[bounded_index]
80
  return (
81
  _slice_label(state, bounded_index),
82
  _read_slice_preview(selected_path),
 
83
  )
84
 
85
 
86
+ def load_single_model(stl_file: str | None) -> tuple[str | None, str]:
87
  if not stl_file:
88
+ return None, "No model loaded."
 
 
 
 
 
 
 
 
 
 
 
89
  mesh = load_mesh(stl_file)
90
+ return stl_file, _format_model_details(Path(stl_file).name, mesh)
91
+
92
+
93
+ def preload_sample_models() -> tuple:
94
+ outputs: list[Any] = []
95
+
96
+ for filename in SAMPLE_STL_FILENAMES:
97
+ stl_path = SAMPLE_STL_DIR / filename
98
+ if not stl_path.exists():
99
+ outputs.extend([
100
+ None,
101
+ None,
102
+ f"Sample file not found: {stl_path}",
103
+ ])
104
+ continue
105
+
106
+ try:
107
+ mesh = load_mesh(stl_path)
108
+ except Exception as exc:
109
+ outputs.extend([
110
+ str(stl_path),
111
+ None,
112
+ f"Failed to load sample model: {stl_path.name} ({exc})",
113
+ ])
114
+ continue
115
+
116
+ outputs.extend([
117
+ str(stl_path),
118
+ str(stl_path),
119
+ _format_model_details(stl_path.name, mesh),
120
+ ])
121
+
122
+ return tuple(outputs)
123
+
124
+
125
+ def generate_all_stacks(
126
+ stl1: str | None,
127
+ stl2: str | None,
128
+ stl3: str | None,
129
  layer_height: float,
130
  pixel_size: float,
131
  progress: gr.Progress = gr.Progress(),
132
  ):
133
+ files = [stl1, stl2, stl3]
134
+ valid_count = max(1, sum(1 for f in files if f))
135
+ results: list = []
136
+ completed = 0
137
+
138
+ for stl_file in files:
139
+ if not stl_file:
140
+ results.extend([
141
+ "No file uploaded.",
142
+ _empty_state(),
143
+ _reset_slider(),
144
+ "No slice stack loaded yet.",
145
+ None,
146
+ None,
147
+ ])
148
+ continue
149
+
150
+ slot_offset = completed
151
+
152
+ def report_progress(cur: int, tot: int, offset: int = slot_offset) -> None:
153
+ progress(
154
+ (offset + cur / tot) / valid_count,
155
+ desc=f"Slicing object {offset + 1} of {valid_count}\u2026",
156
+ )
157
+
158
+ stack = slice_stl_to_tiffs(
159
+ stl_file,
160
+ layer_height=layer_height,
161
+ pixel_size=pixel_size,
162
+ progress_callback=report_progress,
163
+ )
164
+ state = _stack_to_state(stack)
165
+ label, preview = _render_selected_slice(state, 0)
166
+ slider = gr.update(
167
+ minimum=0,
168
+ maximum=max(0, len(stack.tiff_paths) - 1),
169
+ value=0,
170
+ step=1,
171
+ interactive=len(stack.tiff_paths) > 1,
172
+ )
173
+ results.extend([
174
+ _format_summary(stack, Path(stl_file).name),
175
+ state,
176
+ slider,
177
+ label,
178
+ preview,
179
+ str(stack.zip_path),
180
+ ])
181
+ completed += 1
182
 
183
+ return tuple(results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
 
186
+ def jump_to_slice(state: ViewerState, index: float) -> tuple[str, Image.Image | None]:
187
  return _render_selected_slice(state, int(index))
188
 
189
 
190
+ def shift_slice(state: ViewerState, index: float, delta: int) -> tuple[int, str, Image.Image | None]:
191
  tiff_paths = state.get("tiff_paths", [])
192
  if not tiff_paths:
193
+ return 0, "No slice stack loaded yet.", None
194
 
195
  new_index = max(0, min(int(index) + delta, len(tiff_paths) - 1))
196
+ label, preview = _render_selected_slice(state, new_index)
197
+ return new_index, label, preview
198
 
199
 
200
  def build_demo() -> gr.Blocks:
 
202
  gr.Markdown(
203
  """
204
  # STL to TIFF Slicer
205
+ Upload up to three STL files, choose a shared layer height and XY pixel size, then generate TIFF stacks for all uploaded models.
 
206
  """
207
  )
208
 
 
 
209
  with gr.Row():
210
+ load_samples_button = gr.Button(
211
+ "Load Sample STLs",
212
+ variant="secondary",
213
+ size="sm",
214
+ min_width=140,
215
+ scale=0,
216
+ )
217
+
218
+ # --- Upload + 3D viewer row ---
219
+ stl_files: list[gr.File] = []
220
+ model_viewers: list[gr.Model3D] = []
221
+ model_details_list: list[gr.Markdown] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
+ with gr.Row():
224
+ for i in range(3):
225
+ with gr.Column():
226
+ stl_file = gr.File(
227
+ label=f"STL File {i + 1}",
228
+ file_types=[".stl"],
229
+ type="filepath",
230
+ )
231
+ model_viewer = gr.Model3D(
232
+ label=f"3D Viewer {i + 1}",
233
+ display_mode="solid",
234
+ clear_color=(0.94, 0.95, 0.97, 1.0),
235
+ height=300,
236
+ )
237
+ model_details = gr.Markdown(f"No model {i + 1} loaded.")
238
+ stl_files.append(stl_file)
239
+ model_viewers.append(model_viewer)
240
+ model_details_list.append(model_details)
241
+
242
+ # --- Shared slicing controls ---
243
+ with gr.Row():
244
+ layer_height = gr.Number(label="Layer Height", value=0.1, minimum=0.0001, step=0.01)
245
+ pixel_size = gr.Number(label="Pixel Size", value=0.05, minimum=0.0001, step=0.01)
246
+ generate_button = gr.Button("Generate TIFF Stacks", variant="primary")
247
+
248
+ # --- Per-object slice browsers ---
249
+ states: list[gr.State] = []
250
+ sliders: list[gr.Slider] = []
251
+ summaries: list[gr.Markdown] = []
252
+ slice_labels: list[gr.Markdown] = []
253
+ slice_previews: list[gr.Image] = []
254
+ download_zips: list[gr.File] = []
255
 
256
+ with gr.Row():
257
+ for i in range(3):
258
+ with gr.Column():
259
+ summary = gr.Markdown(f"Upload STL {i + 1} to begin.")
260
+ slice_label = gr.Markdown("No slice stack loaded yet.")
261
+ slice_preview = gr.Image(
262
+ label=f"Slice Preview {i + 1}",
263
+ type="pil",
264
+ image_mode="L",
265
+ height=360,
266
+ )
267
+ with gr.Row():
268
+ prev_button = gr.Button("\u25c4 Prev")
269
+ next_button = gr.Button("Next \u25ba")
270
+ slice_slider = gr.Slider(
271
+ label="Slice Index",
272
+ minimum=0,
273
+ maximum=0,
274
+ value=0,
275
+ step=1,
276
+ interactive=False,
277
+ )
278
+ download_zip = gr.File(label=f"Download TIFF ZIP {i + 1}")
279
+ state = gr.State(_empty_state())
280
+
281
+ summaries.append(summary)
282
+ slice_labels.append(slice_label)
283
+ slice_previews.append(slice_preview)
284
+ sliders.append(slice_slider)
285
+ download_zips.append(download_zip)
286
+ states.append(state)
287
+
288
+ slice_slider.release(
289
+ fn=jump_to_slice,
290
+ inputs=[state, slice_slider],
291
+ outputs=[slice_label, slice_preview],
292
+ queue=False,
293
+ )
294
+ prev_button.click(
295
+ fn=lambda sv, idx: shift_slice(sv, idx, -1),
296
+ inputs=[state, slice_slider],
297
+ outputs=[slice_slider, slice_label, slice_preview],
298
+ queue=False,
299
+ )
300
+ next_button.click(
301
+ fn=lambda sv, idx: shift_slice(sv, idx, 1),
302
+ inputs=[state, slice_slider],
303
+ outputs=[slice_slider, slice_label, slice_preview],
304
+ queue=False,
305
+ )
306
+
307
+ # --- File upload handlers ---
308
+ for i in range(3):
309
+ stl_files[i].change(
310
+ fn=load_single_model,
311
+ inputs=stl_files[i],
312
+ outputs=[model_viewers[i], model_details_list[i]],
313
+ )
314
+
315
+ # --- Generate button ---
316
+ generate_outputs: list = []
317
+ for i in range(3):
318
+ generate_outputs.extend([
319
+ summaries[i],
320
+ states[i],
321
+ sliders[i],
322
+ slice_labels[i],
323
+ slice_previews[i],
324
+ download_zips[i],
325
+ ])
326
+
327
+ preload_outputs: list = []
328
+ for i in range(3):
329
+ preload_outputs.extend([
330
+ stl_files[i],
331
+ model_viewers[i],
332
+ model_details_list[i],
333
+ ])
334
+
335
+ load_samples_button.click(
336
+ fn=preload_sample_models,
337
+ outputs=preload_outputs,
338
  )
339
 
340
+ generate_button.click(
341
+ fn=generate_all_stacks,
342
+ inputs=[stl_files[0], stl_files[1], stl_files[2], layer_height, pixel_size],
343
+ outputs=generate_outputs,
 
344
  )
345
 
346
  return demo
sample_stls/Hollow_Pyramid.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bcc1a8c2588f789eb5c809c3fe1455e717cda8f611294c9714f206ee9c61eeb3
3
+ size 13157
sample_stls/balanced_die.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74d339fa958f1ac83b21368830a82480bf566a3a77df16f84beb44bcd1b382e3
3
+ size 108184
sample_stls/halfsphere.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5116f0474b5c562a6558af12c854abee33c631e15d4928670dbe46ec4c47547a
3
+ size 64884