MichaelRKessler commited on
Commit
059beb6
·
2 Parent(s): 1ccc4b33d1f0a3

Merge branch 'Three_files'

Browse files
CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
app.py CHANGED
@@ -10,6 +10,29 @@ 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:
@@ -32,31 +55,6 @@ def _stack_to_state(stack: SliceStack) -> ViewerState:
32
  }
33
 
34
 
35
- def _format_summary(stack: SliceStack, source_name: str) -> str:
36
- (x_min, y_min, z_min), (x_max, y_max, z_max) = stack.bounds
37
- return "\n".join(
38
- [
39
- "### Slice Stack Ready",
40
- f"- Source: `{source_name}`",
41
- f"- TIFF count: `{len(stack.tiff_paths)}`",
42
- f"- Image size: `{stack.image_size[0]} x {stack.image_size[1]}` pixels",
43
- f"- Layer height: `{stack.layer_height}`",
44
- f"- Pixel size: `{stack.pixel_size}`",
45
- f"- Bounds: `x={x_min:.3f}..{x_max:.3f}`, `y={y_min:.3f}..{y_max:.3f}`, `z={z_min:.3f}..{z_max:.3f}`",
46
- ]
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,216 +76,271 @@ 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:
180
- with gr.Blocks(title="STL TIFF Slicer") as demo:
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
+ APP_CSS = """
16
+ .gradio-container {
17
+ font-size: 90%;
18
+ padding-top: 0.5rem !important;
19
+ padding-bottom: 0.5rem !important;
20
+ }
21
+
22
+ .gradio-container .gr-row {
23
+ gap: 0.5rem !important;
24
+ }
25
+
26
+ .gradio-container .gr-form,
27
+ .gradio-container .gr-box,
28
+ .gradio-container .block {
29
+ padding: 0.4rem !important;
30
+ }
31
+
32
+ .gradio-container .prose {
33
+ margin-bottom: 0.4rem !important;
34
+ }
35
+ """
36
 
37
 
38
  def _read_slice_preview(path: str) -> Image.Image:
 
55
  }
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def _format_model_details(source_name: str, mesh) -> str:
59
  extents = mesh.extents
60
  return "\n".join(
 
76
  return f"Slice {index + 1} / {total} | z = {z_value:.4f} | {path}"
77
 
78
 
79
+ def _render_selected_slice(state: ViewerState, index: int) -> tuple[str, Image.Image | None]:
80
  tiff_paths = state.get("tiff_paths", [])
81
  if not tiff_paths:
82
+ return "No slice stack loaded yet.", None
83
 
84
  bounded_index = max(0, min(int(index), len(tiff_paths) - 1))
85
  selected_path = tiff_paths[bounded_index]
86
  return (
87
  _slice_label(state, bounded_index),
88
  _read_slice_preview(selected_path),
 
89
  )
90
 
91
 
92
+ def load_single_model(stl_file: str | None) -> tuple[str | None, str]:
93
  if not stl_file:
94
+ return None, "No model loaded."
 
 
 
 
 
 
 
 
 
 
 
95
  mesh = load_mesh(stl_file)
96
+ return stl_file, _format_model_details(Path(stl_file).name, mesh)
97
+
98
+
99
+ def preload_sample_models() -> tuple:
100
+ outputs: list[Any] = []
101
+
102
+ for filename in SAMPLE_STL_FILENAMES:
103
+ stl_path = SAMPLE_STL_DIR / filename
104
+ if not stl_path.exists():
105
+ outputs.extend([
106
+ None,
107
+ None,
108
+ f"Sample file not found: {stl_path}",
109
+ ])
110
+ continue
111
+
112
+ try:
113
+ mesh = load_mesh(stl_path)
114
+ except Exception as exc:
115
+ outputs.extend([
116
+ str(stl_path),
117
+ None,
118
+ f"Failed to load sample model: {stl_path.name} ({exc})",
119
+ ])
120
+ continue
121
+
122
+ outputs.extend([
123
+ str(stl_path),
124
+ str(stl_path),
125
+ _format_model_details(stl_path.name, mesh),
126
+ ])
127
+
128
+ return tuple(outputs)
129
+
130
+
131
+ def generate_all_stacks(
132
+ stl1: str | None,
133
+ stl2: str | None,
134
+ stl3: str | None,
135
  layer_height: float,
136
  pixel_size: float,
137
  progress: gr.Progress = gr.Progress(),
138
  ):
139
+ files = [stl1, stl2, stl3]
140
+ valid_count = max(1, sum(1 for f in files if f))
141
+ results: list = []
142
+ completed = 0
143
+
144
+ for stl_file in files:
145
+ if not stl_file:
146
+ results.extend([
147
+ _empty_state(),
148
+ _reset_slider(),
149
+ "No slice stack loaded yet.",
150
+ None,
151
+ None,
152
+ ])
153
+ continue
154
+
155
+ slot_offset = completed
156
+
157
+ def report_progress(cur: int, tot: int, offset: int = slot_offset) -> None:
158
+ progress(
159
+ (offset + cur / tot) / valid_count,
160
+ desc=f"Slicing object {offset + 1} of {valid_count}\u2026",
161
+ )
162
+
163
+ stack = slice_stl_to_tiffs(
164
+ stl_file,
165
+ layer_height=layer_height,
166
+ pixel_size=pixel_size,
167
+ progress_callback=report_progress,
168
+ )
169
+ state = _stack_to_state(stack)
170
+ label, preview = _render_selected_slice(state, 0)
171
+ slider = gr.update(
172
+ minimum=0,
173
+ maximum=max(0, len(stack.tiff_paths) - 1),
174
+ value=0,
175
+ step=1,
176
+ interactive=len(stack.tiff_paths) > 1,
177
+ )
178
+ results.extend([
179
+ state,
180
+ slider,
181
+ label,
182
+ preview,
183
+ str(stack.zip_path),
184
+ ])
185
+ completed += 1
186
 
187
+ return tuple(results)
 
 
 
 
 
 
 
 
188
 
189
 
190
+ def jump_to_slice(state: ViewerState, index: float) -> tuple[str, Image.Image | None]:
191
  return _render_selected_slice(state, int(index))
192
 
193
 
194
+ def shift_slice(state: ViewerState, index: float, delta: int) -> tuple[int, str, Image.Image | None]:
195
  tiff_paths = state.get("tiff_paths", [])
196
  if not tiff_paths:
197
+ return 0, "No slice stack loaded yet.", None
198
 
199
  new_index = max(0, min(int(index) + delta, len(tiff_paths) - 1))
200
+ label, preview = _render_selected_slice(state, new_index)
201
+ return new_index, label, preview
202
 
203
 
204
  def build_demo() -> gr.Blocks:
205
+ with gr.Blocks(title="STL TIFF Slicer", css=APP_CSS) as demo:
206
  gr.Markdown(
207
  """
208
  # STL to TIFF Slicer
209
+ Upload up to three STL files, choose a shared layer height and XY pixel size, then generate TIFF stacks for all uploaded models.
 
210
  """
211
  )
212
 
 
 
213
  with gr.Row():
214
+ load_samples_button = gr.Button(
215
+ "Load Sample STLs",
216
+ variant="secondary",
217
+ size="sm",
218
+ min_width=140,
219
+ scale=0,
220
+ )
221
+
222
+ # --- Upload + 3D viewer row ---
223
+ stl_files: list[gr.File] = []
224
+ model_viewers: list[gr.Model3D] = []
225
+ model_details_list: list[gr.Markdown] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"],
233
+ type="filepath",
234
+ )
235
+ model_viewer = gr.Model3D(
236
+ label=f"3D Viewer {i + 1}",
237
+ display_mode="solid",
238
+ clear_color=(0.94, 0.95, 0.97, 1.0),
239
+ height=270,
240
+ )
241
+ model_details = gr.Markdown(f"No model {i + 1} loaded.")
242
+ stl_files.append(stl_file)
243
+ model_viewers.append(model_viewer)
244
+ model_details_list.append(model_details)
245
+
246
+ # --- Shared slicing controls ---
247
+ with gr.Row():
248
+ layer_height = gr.Number(label="Layer Height", value=0.1, minimum=0.0001, step=0.01)
249
+ pixel_size = gr.Number(label="Pixel Size", value=0.05, minimum=0.0001, step=0.01)
250
+ generate_button = gr.Button("Generate TIFF Stacks", variant="primary")
251
 
252
+ # --- Per-object slice browsers ---
253
+ states: list[gr.State] = []
254
+ sliders: list[gr.Slider] = []
255
+ slice_labels: list[gr.Markdown] = []
256
+ slice_previews: list[gr.Image] = []
257
+ download_zips: list[gr.File] = []
258
 
259
+ with gr.Row():
260
+ for i in range(3):
261
+ with gr.Column():
262
+ slice_label = gr.Markdown("No slice stack loaded yet.")
263
+ slice_preview = gr.Image(
264
+ label=f"Slice Preview {i + 1}",
265
+ type="pil",
266
+ image_mode="L",
267
+ height=324,
268
+ )
269
+ with gr.Row():
270
+ prev_button = gr.Button("\u25c4 Prev")
271
+ next_button = gr.Button("Next \u25ba")
272
+ slice_slider = gr.Slider(
273
+ label="Slice Index",
274
+ minimum=0,
275
+ maximum=0,
276
+ value=0,
277
+ step=1,
278
+ interactive=False,
279
+ )
280
+ download_zip = gr.File(label=f"Download TIFF ZIP {i + 1}")
281
+ state = gr.State(_empty_state())
282
+
283
+ slice_labels.append(slice_label)
284
+ slice_previews.append(slice_preview)
285
+ sliders.append(slice_slider)
286
+ download_zips.append(download_zip)
287
+ states.append(state)
288
+
289
+ slice_slider.release(
290
+ fn=jump_to_slice,
291
+ inputs=[state, slice_slider],
292
+ outputs=[slice_label, slice_preview],
293
+ queue=False,
294
+ )
295
+ prev_button.click(
296
+ fn=lambda sv, idx: shift_slice(sv, idx, -1),
297
+ inputs=[state, slice_slider],
298
+ outputs=[slice_slider, slice_label, slice_preview],
299
+ queue=False,
300
+ )
301
+ next_button.click(
302
+ fn=lambda sv, idx: shift_slice(sv, idx, 1),
303
+ inputs=[state, slice_slider],
304
+ outputs=[slice_slider, slice_label, slice_preview],
305
+ queue=False,
306
+ )
307
+
308
+ # --- File upload handlers ---
309
+ for i in range(3):
310
+ stl_files[i].change(
311
+ fn=load_single_model,
312
+ inputs=stl_files[i],
313
+ outputs=[model_viewers[i], model_details_list[i]],
314
+ )
315
+
316
+ # --- Generate button ---
317
+ generate_outputs: list = []
318
+ for i in range(3):
319
+ generate_outputs.extend([
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