bokula commited on
Commit
d6e17ea
·
verified ·
1 Parent(s): 5a5f252

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +173 -304
  2. requirements.txt +1 -1
app.py CHANGED
@@ -1,16 +1,8 @@
1
  """
2
- app.py — Fabrication Topologies: Pattern Ground Relief Generator
3
- Gradio web app per HuggingFace Spaces
4
-
5
- Deploy:
6
- 1. Crea un nuovo Space su huggingface.co/new-space
7
- 2. Scegli SDK: Gradio | Hardware: CPU Basic (gratuito)
8
- 3. Carica questi 3 file: app.py, relief_workshop.py, requirements.txt
9
- 4. Il deploy parte automaticamente in ~2 minuti
10
  """
11
 
12
- import io
13
- import struct
14
  import tempfile
15
  from pathlib import Path
16
 
@@ -19,134 +11,100 @@ import gradio as gr
19
  import numpy as np
20
  from PIL import Image
21
 
22
- # Importa le funzioni di processing da relief_workshop.py
23
  from relief_workshop import build_heightmap, heightmap_to_stl
24
 
25
 
26
  # ─────────────────────────────────────────────
27
- # CORE FUNCTION
28
  # ─────────────────────────────────────────────
29
 
30
- def generate_relief(
31
- image,
32
- tile_w: float,
33
- tile_h_mode: str,
34
- tile_h_custom: float,
35
- relief_mm: float,
36
- base_mm: float,
37
- invert: bool,
38
- quantize_levels: int,
39
- tileable: bool,
40
- blur: float,
41
- resolution: int,
42
- ):
43
  if image is None:
44
- return None, None, "⚠️ Carica un'immagine per iniziare."
45
-
46
  try:
47
- # Salva immagine in temp
48
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
49
- tmp_in = f.name
50
- Image.fromarray(image).save(tmp_in)
51
-
52
- # Calcola altezza tile
53
- img_h, img_w = image.shape[:2]
54
- if tile_h_mode == "Stessa larghezza (100×100)":
55
- tile_h = tile_w
56
- elif tile_h_mode == "Proporzioni originali":
57
- tile_h = tile_w * img_h / img_w
58
- else:
59
- tile_h = tile_h_custom
60
 
61
- # Build heightmap
62
- quantize = quantize_levels if quantize_levels >= 2 else 0
63
  height_map, px_w, px_h = build_heightmap(
64
- tmp_in,
65
- max_px=resolution,
66
- invert=invert,
67
- blur=blur,
68
- gamma=1.1,
69
- quantize=quantize,
70
- tileable=tileable,
71
- fade_px=12,
72
  )
 
73
 
74
- # Preview depth map
75
  preview = (height_map * 255).astype(np.uint8)
76
- preview_rgb = np.stack([preview, preview, preview], axis=-1)
77
-
78
- # Generate STL
79
- stl_bytes = heightmap_to_stl(
80
- height_map, px_w, px_h,
81
- tile_w_mm=tile_w,
82
- tile_h_mm=tile_h,
83
- relief_mm=relief_mm,
84
- base_mm=base_mm,
85
- )
86
-
87
- # Save STL to temp file
88
- with tempfile.NamedTemporaryFile(suffix=".stl", delete=False) as f:
89
- stl_path = f.name
90
- f.write(stl_bytes)
91
-
92
- size_kb = len(stl_bytes) / 1024
93
- n_tri = (len(stl_bytes) - 84) // 50
94
-
95
- info = f"""✓ Tile generata
96
-
97
- Dimensioni: {tile_w:.0f} × {tile_h:.0f} mm
98
- Altezza: base {base_mm:.1f} mm + rilievo {relief_mm:.1f} mm = {base_mm + relief_mm:.1f} mm totale
99
- Mesh: {px_w} × {px_h} px — {n_tri:,} triangoli
100
- File STL: {size_kb:.0f} KB
101
-
102
- Impostazioni slicer: layer 0.15 mm · infill 15% · no supports"""
103
-
104
- Path(tmp_in).unlink(missing_ok=True)
105
- return preview_rgb, stl_path, info
106
-
107
  except Exception as e:
108
- return None, None, f"❌ Errore: {str(e)}"
109
 
110
 
111
- def preview_only(
112
  image,
113
  tile_w, tile_h_mode, tile_h_custom,
114
  relief_mm, base_mm,
115
  invert, quantize_levels, tileable, blur, resolution,
116
  ):
117
- """Genera solo la depth map preview, più veloce."""
118
  if image is None:
119
- return None, "⚠️ Carica un'immagine per iniziare."
120
-
121
  try:
122
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
123
  tmp_in = f.name
124
  Image.fromarray(image).save(tmp_in)
125
 
126
- quantize = quantize_levels if quantize_levels >= 2 else 0
 
 
 
 
 
 
 
 
127
  height_map, px_w, px_h = build_heightmap(
128
- tmp_in,
129
- max_px=min(resolution, 300), # preview più veloce
130
- invert=invert,
131
- blur=blur,
132
- gamma=1.1,
133
- quantize=quantize,
134
- tileable=tileable,
135
- fade_px=12,
136
  )
 
137
 
 
138
  preview = (height_map * 255).astype(np.uint8)
139
- preview_rgb = np.stack([preview, preview, preview], axis=-1)
140
 
141
- Path(tmp_in).unlink(missing_ok=True)
 
 
 
 
 
 
 
142
 
143
- msg = f"Depth map: {px_w}×{px_h}px · Bianco = alto · Nero = basso"
144
- if invert:
145
- msg += " (invertita)"
146
- return preview_rgb, msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  except Exception as e:
149
- return None, f"❌ Errore: {str(e)}"
150
 
151
 
152
  # ─────────────────────────────────────────────
@@ -156,239 +114,150 @@ def preview_only(
156
  CSS = """
157
  @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Instrument+Serif:ital@0;1&display=swap');
158
 
159
- :root {
160
- --black: #0a0a0a;
161
- --white: #f2efe8;
162
- --yellow: #d4a912;
163
- --grey: #333;
164
- --mid: #888;
165
- }
166
 
167
- body, .gradio-container {
168
- background: var(--black) !important;
169
- color: var(--white) !important;
170
  font-family: 'DM Mono', monospace !important;
 
171
  }
172
 
173
- .gr-block, .gr-box, .gr-panel {
174
- background: #111 !important;
175
- border: 1px solid #222 !important;
176
- border-radius: 0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
 
179
  .gr-button {
180
- border-radius: 0 !important;
181
  font-family: 'DM Mono', monospace !important;
182
- letter-spacing: 0.05em !important;
183
  text-transform: uppercase !important;
184
- font-size: 0.75rem !important;
 
 
185
  }
186
 
187
- .gr-button.primary {
188
- background: var(--yellow) !important;
189
- color: var(--black) !important;
190
  border: none !important;
191
  }
192
 
193
- .gr-button.secondary {
194
  background: transparent !important;
195
- color: var(--mid) !important;
196
  border: 1px solid #333 !important;
197
  }
 
198
 
199
- .gr-button:hover {
200
- opacity: 0.85 !important;
201
- }
202
-
203
- label, .gr-label {
204
- font-family: 'DM Mono', monospace !important;
205
- font-size: 0.7rem !important;
206
- letter-spacing: 0.08em !important;
207
- color: var(--mid) !important;
208
- text-transform: uppercase !important;
209
- }
210
 
211
- .gr-textbox textarea, .gr-textbox input {
212
- background: #0d0d0d !important;
213
- border: 1px solid #222 !important;
214
- color: var(--white) !important;
215
- font-family: 'DM Mono', monospace !important;
216
- border-radius: 0 !important;
217
- }
218
 
219
- h1 {
220
- font-family: 'Instrument Serif', serif !important;
221
- font-size: 2.2rem !important;
222
- color: var(--white) !important;
223
- font-weight: 400 !important;
224
- letter-spacing: -0.02em !important;
225
- line-height: 1.1 !important;
226
- }
227
 
228
- h3 {
229
- font-family: 'DM Mono', monospace !important;
230
- font-size: 0.65rem !important;
231
- letter-spacing: 0.15em !important;
232
- color: var(--yellow) !important;
233
- text-transform: uppercase !important;
234
- margin-bottom: 0.5rem !important;
235
- border-bottom: 1px solid #222 !important;
236
- padding-bottom: 0.4rem !important;
237
- }
238
 
239
- .info-box {
240
- background: #111;
241
- border: 1px solid #222;
242
- padding: 1rem;
243
- font-size: 0.75rem;
244
- line-height: 1.7;
245
- color: var(--mid);
246
- white-space: pre-line;
247
- }
248
 
249
- .accent { color: var(--yellow); }
250
 
251
- /* Slider */
252
- .gr-slider input[type=range] {
253
- accent-color: var(--yellow) !important;
254
- }
255
 
256
- /* Checkbox */
257
- input[type=checkbox] {
258
- accent-color: var(--yellow) !important;
259
- }
260
  """
261
 
262
- HEADER = """
263
- # Pattern Ground
264
- ### Fabrication Topologies — Vectorealism × Dropcity
265
 
266
- Converti un'immagine in una tile di rilievo 3D pronta per la stampa.
267
- Regola i parametri, controlla la depth map, scarica l'STL.
268
- """
269
 
270
- INSTRUCTIONS = """### Come funziona
271
-
272
- **Depth map preview** — bianco = alto, nero = basso. Controlla prima di generare l'STL.
273
-
274
- **Quantize** — crea gradini netti invece di un rilievo continuo. 4–6 livelli funziona bene per pattern urbani (sanpietrini, griglia, muratura).
275
-
276
- **Tileable** — sfuma i bordi della tile così le piastrelle si affiancano senza giunti visibili nella griglia condivisa.
277
-
278
- **Invert** — inverte l'altezza: utile se il pattern ha sfondo bianco e motivo scuro.
279
-
280
- **Impostazioni slicer consigliate:** layer height 0.15 mm · infill 15% · no supports · ironing on top surface"""
281
-
282
- def build_ui():
283
- with gr.Blocks(css=CSS, title="Pattern Ground — Fabrication Topologies") as demo:
284
-
285
- gr.Markdown(HEADER)
286
-
287
- with gr.Row():
288
-
289
- # ── Colonna sinistra: input + parametri ──
290
- with gr.Column(scale=1):
291
-
292
- gr.Markdown("### Immagine")
293
- image_input = gr.Image(
294
- label="Carica immagine (PNG, JPG)",
295
- type="numpy",
296
- image_mode="RGB",
297
- )
298
-
299
- gr.Markdown("### Dimensioni tile")
300
- with gr.Row():
301
- tile_w = gr.Slider(
302
- 20, 200, value=100, step=5,
303
- label="Larghezza (mm)"
304
- )
305
- tile_h_mode = gr.Radio(
306
- ["Stessa larghezza (100×100)", "Proporzioni originali", "Personalizzata"],
307
- value="Stessa larghezza (100×100)",
308
- label="Altezza tile"
309
- )
310
- tile_h_custom = gr.Slider(
311
- 20, 200, value=100, step=5,
312
- label="Altezza personalizzata (mm)",
313
- visible=False
314
- )
315
-
316
- gr.Markdown("### Rilievo")
317
- with gr.Row():
318
- relief_mm = gr.Slider(1, 10, value=4, step=0.5, label="Altezza rilievo (mm)")
319
- base_mm = gr.Slider(1, 5, value=2, step=0.5, label="Spessore base (mm)")
320
-
321
- gr.Markdown("### Processing")
322
- quantize_levels = gr.Slider(
323
- 0, 12, value=0, step=1,
324
- label="Quantize — livelli di altezza (0 = continuo, 4–6 = gradini)"
325
- )
326
- blur = gr.Slider(
327
- 0, 3, value=0.5, step=0.1,
328
- label="Smoothing (0 = nessuno)"
329
- )
330
- resolution = gr.Slider(
331
- 100, 400, value=200, step=50,
332
- label="Risoluzione mesh (px per lato)"
333
- )
334
-
335
- gr.Markdown("### Opzioni")
336
- with gr.Row():
337
- invert = gr.Checkbox(label="Invert depth (scuro=alto)", value=False)
338
- tileable = gr.Checkbox(label="Tileable (bordi sfumati)", value=False)
339
-
340
- with gr.Row():
341
- preview_btn = gr.Button("Depth map preview", variant="secondary")
342
- generate_btn = gr.Button("Genera STL", variant="primary")
343
-
344
- # ── Colonna destra: output ─��
345
- with gr.Column(scale=1):
346
- gr.Markdown("### Depth map")
347
- depth_preview = gr.Image(
348
- label="Depth map — bianco = alto, nero = basso",
349
- type="numpy",
350
- interactive=False,
351
- )
352
-
353
- gr.Markdown("### Download")
354
- stl_output = gr.File(label="File STL")
355
-
356
- gr.Markdown("### Info")
357
- info_output = gr.Textbox(
358
- label="",
359
- lines=8,
360
- interactive=False,
361
- )
362
-
363
- gr.Markdown(INSTRUCTIONS)
364
-
365
- # ── Logica UI ──
366
- def toggle_custom_height(mode):
367
- return gr.update(visible=(mode == "Personalizzata"))
368
-
369
- tile_h_mode.change(toggle_custom_height, tile_h_mode, tile_h_custom)
370
-
371
- # Inputs condivisi
372
- inputs = [
373
- image_input, tile_w, tile_h_mode, tile_h_custom,
374
- relief_mm, base_mm, invert, quantize_levels, tileable, blur, resolution
375
- ]
376
-
377
- preview_btn.click(
378
- preview_only,
379
- inputs=inputs,
380
- outputs=[depth_preview, info_output],
381
- )
382
 
383
- generate_btn.click(
384
- generate_relief,
385
- inputs=inputs,
386
- outputs=[depth_preview, stl_output, info_output],
387
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
- return demo
 
390
 
391
 
392
  if __name__ == "__main__":
393
- app = build_ui()
394
- app.launch()
 
1
  """
2
+ app.py — Pattern Ground Relief Generator
3
+ Gradio 5.x compatibile Python 3.13 — HuggingFace Spaces
 
 
 
 
 
 
4
  """
5
 
 
 
6
  import tempfile
7
  from pathlib import Path
8
 
 
11
  import numpy as np
12
  from PIL import Image
13
 
 
14
  from relief_workshop import build_heightmap, heightmap_to_stl
15
 
16
 
17
  # ─────────────────────────────────────────────
18
+ # CORE FUNCTIONS
19
  # ─────────────────────────────────────────────
20
 
21
+ def preview_depthmap(image, invert, quantize_levels, tileable, blur):
 
 
 
 
 
 
 
 
 
 
 
 
22
  if image is None:
23
+ return None, "⚠️ Carica un'immagine."
 
24
  try:
 
25
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
26
+ tmp = f.name
27
+ Image.fromarray(image).save(tmp)
 
 
 
 
 
 
 
 
 
28
 
29
+ quantize = int(quantize_levels) if quantize_levels >= 2 else 0
 
30
  height_map, px_w, px_h = build_heightmap(
31
+ tmp, max_px=300,
32
+ invert=invert, blur=blur, gamma=1.1,
33
+ quantize=quantize, tileable=tileable, fade_px=12,
 
 
 
 
 
34
  )
35
+ Path(tmp).unlink(missing_ok=True)
36
 
 
37
  preview = (height_map * 255).astype(np.uint8)
38
+ info = f"Depth map {px_w}×{px_h}px — bianco=alto, nero=basso"
39
+ if invert:
40
+ info += " (invertita)"
41
+ return preview, info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  except Exception as e:
43
+ return None, f"❌ {e}"
44
 
45
 
46
+ def generate_stl(
47
  image,
48
  tile_w, tile_h_mode, tile_h_custom,
49
  relief_mm, base_mm,
50
  invert, quantize_levels, tileable, blur, resolution,
51
  ):
 
52
  if image is None:
53
+ return None, None, "⚠️ Carica un'immagine."
 
54
  try:
55
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
56
  tmp_in = f.name
57
  Image.fromarray(image).save(tmp_in)
58
 
59
+ img_h, img_w = image.shape[:2]
60
+ if tile_h_mode == "Quadrata (uguale alla larghezza)":
61
+ tile_h = tile_w
62
+ elif tile_h_mode == "Proporzioni originali":
63
+ tile_h = tile_w * img_h / img_w
64
+ else:
65
+ tile_h = tile_h_custom
66
+
67
+ quantize = int(quantize_levels) if quantize_levels >= 2 else 0
68
  height_map, px_w, px_h = build_heightmap(
69
+ tmp_in, max_px=int(resolution),
70
+ invert=invert, blur=blur, gamma=1.1,
71
+ quantize=quantize, tileable=tileable, fade_px=12,
 
 
 
 
 
72
  )
73
+ Path(tmp_in).unlink(missing_ok=True)
74
 
75
+ # Depth map preview
76
  preview = (height_map * 255).astype(np.uint8)
 
77
 
78
+ # Generate STL bytes
79
+ stl_bytes = heightmap_to_stl(
80
+ height_map, px_w, px_h,
81
+ tile_w_mm=float(tile_w),
82
+ tile_h_mm=float(tile_h),
83
+ relief_mm=float(relief_mm),
84
+ base_mm=float(base_mm),
85
+ )
86
 
87
+ # Write to named temp file (Gradio 5 needs a real path)
88
+ stl_tmp = tempfile.NamedTemporaryFile(
89
+ suffix=".stl", delete=False, prefix="tile_"
90
+ )
91
+ stl_tmp.write(stl_bytes)
92
+ stl_tmp.close()
93
+
94
+ n_tri = (len(stl_bytes) - 84) // 50
95
+ size_kb = len(stl_bytes) / 1024
96
+ info = (
97
+ f"✓ Tile generata\n\n"
98
+ f"Dimensioni: {tile_w:.0f} × {tile_h:.0f} mm\n"
99
+ f"Altezza: {base_mm:.1f} base + {relief_mm:.1f} rilievo = {base_mm+relief_mm:.1f} mm\n"
100
+ f"Mesh: {px_w}×{px_h}px — {n_tri:,} triangoli\n"
101
+ f"File: {size_kb:.0f} KB\n\n"
102
+ f"Slicer: layer 0.15 mm · infill 15% · no supports"
103
+ )
104
+ return preview, stl_tmp.name, info
105
 
106
  except Exception as e:
107
+ return None, None, f"❌ {e}"
108
 
109
 
110
  # ─────────────────────────────────────────────
 
114
  CSS = """
115
  @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Instrument+Serif:ital@0;1&display=swap');
116
 
117
+ body { background: #0a0a0a !important; }
 
 
 
 
 
 
118
 
119
+ .gradio-container {
120
+ background: #0a0a0a !important;
 
121
  font-family: 'DM Mono', monospace !important;
122
+ max-width: 1100px !important;
123
  }
124
 
125
+ h1 {
126
+ font-family: 'Instrument Serif', serif !important;
127
+ font-size: 2rem !important;
128
+ color: #f2efe8 !important;
129
+ font-weight: 400 !important;
130
+ letter-spacing: -0.02em !important;
131
+ }
132
+
133
+ h2 {
134
+ font-family: 'DM Mono', monospace !important;
135
+ font-size: 0.6rem !important;
136
+ letter-spacing: 0.2em !important;
137
+ color: #d4a912 !important;
138
+ text-transform: uppercase !important;
139
+ border-bottom: 1px solid #222 !important;
140
+ padding-bottom: 0.4rem !important;
141
+ margin-top: 1.2rem !important;
142
  }
143
 
144
  .gr-button {
 
145
  font-family: 'DM Mono', monospace !important;
 
146
  text-transform: uppercase !important;
147
+ letter-spacing: 0.08em !important;
148
+ font-size: 0.72rem !important;
149
+ border-radius: 0 !important;
150
  }
151
 
152
+ button.primary {
153
+ background: #d4a912 !important;
154
+ color: #0a0a0a !important;
155
  border: none !important;
156
  }
157
 
158
+ button.secondary {
159
  background: transparent !important;
160
+ color: #888 !important;
161
  border: 1px solid #333 !important;
162
  }
163
+ """
164
 
165
+ HEADER = """
166
+ # Pattern Ground
167
+ **Fabrication Topologies — Vectorealism × Dropcity · Design Week Milano 2026**
 
 
 
 
 
 
 
 
168
 
169
+ Converti un'immagine in una tile di rilievo 3D pronta per la stampa.
170
+ """
 
 
 
 
 
171
 
172
+ GUIDE = """
173
+ **Come funziona**
 
 
 
 
 
 
174
 
175
+ Ogni pixel dell'immagine diventa un'altezza: **bianco = alto, nero = basso** (o inverso con Invert).
176
+ Controlla sempre la depth map prima di generare l'STL.
 
 
 
 
 
 
 
 
177
 
178
+ **Quantize** — divide il rilievo in N livelli netti invece di un gradiente continuo.
179
+ Ottimo per pattern urbani: sanpietrini, griglia, muratura. Prova 4–6 livelli.
 
 
 
 
 
 
 
180
 
181
+ **Tileable** sfuma i bordi così le tile si affiancano senza giunti visibili nella griglia.
182
 
183
+ **Immagini che funzionano bene:** foto di pavimentazioni, mappe b/n di edifici, texture geometriche,
184
+ pattern a contrasto alto. Evita foto a colori complesse senza preprocessarle in bianco/nero.
 
 
185
 
186
+ **Slicer:** layer height 0.15 mm · infill 15% · no supports · ironing on top surface.
 
 
 
187
  """
188
 
 
 
 
189
 
190
+ def toggle_custom(mode):
191
+ return gr.update(visible=(mode == "Personalizzata"))
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ with gr.Blocks(css=CSS, title="Pattern Ground") as demo:
195
+ gr.Markdown(HEADER)
196
+
197
+ with gr.Row():
198
+ # ── Colonna sinistra ──
199
+ with gr.Column(scale=1):
200
+
201
+ gr.Markdown("## Immagine")
202
+ image_in = gr.Image(label="Carica immagine", type="numpy", image_mode="RGB")
203
+
204
+ gr.Markdown("## Dimensioni tile")
205
+ tile_w = gr.Slider(20, 200, value=100, step=5, label="Larghezza (mm)")
206
+ tile_h_mode = gr.Radio(
207
+ ["Quadrata (uguale alla larghezza)", "Proporzioni originali", "Personalizzata"],
208
+ value="Quadrata (uguale alla larghezza)",
209
+ label="Altezza",
210
+ )
211
+ tile_h_custom = gr.Slider(20, 200, value=100, step=5,
212
+ label="Altezza personalizzata (mm)", visible=False)
213
+
214
+ gr.Markdown("## Rilievo")
215
+ with gr.Row():
216
+ relief_mm = gr.Slider(1, 10, value=4, step=0.5, label="Altezza rilievo (mm)")
217
+ base_mm = gr.Slider(1, 5, value=2, step=0.5, label="Base (mm)")
218
+
219
+ gr.Markdown("## Processing")
220
+ quantize = gr.Slider(0, 12, value=0, step=1,
221
+ label="Quantize — livelli (0=continuo, 4–6=gradini)")
222
+ blur = gr.Slider(0, 3, value=0.5, step=0.1, label="Smoothing (0=nessuno)")
223
+ resolution = gr.Slider(100, 400, value=200, step=50,
224
+ label="Risoluzione mesh (px per lato)")
225
+
226
+ gr.Markdown("## Opzioni")
227
+ with gr.Row():
228
+ invert = gr.Checkbox(label="Invert (scuro=alto)", value=False)
229
+ tileable = gr.Checkbox(label="Tileable (bordi sfumati)", value=False)
230
+
231
+ with gr.Row():
232
+ preview_btn = gr.Button("Depth map preview", variant="secondary")
233
+ gen_btn = gr.Button("Genera STL", variant="primary")
234
+
235
+ # ── Colonna destra ──
236
+ with gr.Column(scale=1):
237
+ gr.Markdown("## Depth map")
238
+ depth_out = gr.Image(label="Bianco = alto · Nero = basso",
239
+ type="numpy", interactive=False)
240
+
241
+ gr.Markdown("## Download")
242
+ stl_out = gr.File(label="STL file")
243
+
244
+ gr.Markdown("## Info")
245
+ info_out = gr.Textbox(label="", lines=7, interactive=False)
246
+
247
+ gr.Markdown(GUIDE)
248
+
249
+ # ── Events ──
250
+ tile_h_mode.change(toggle_custom, tile_h_mode, tile_h_custom)
251
+
252
+ preview_inputs = [image_in, invert, quantize, tileable, blur]
253
+ gen_inputs = [
254
+ image_in, tile_w, tile_h_mode, tile_h_custom,
255
+ relief_mm, base_mm, invert, quantize, tileable, blur, resolution,
256
+ ]
257
 
258
+ preview_btn.click(preview_depthmap, preview_inputs, [depth_out, info_out])
259
+ gen_btn.click(generate_stl, gen_inputs, [depth_out, stl_out, info_out])
260
 
261
 
262
  if __name__ == "__main__":
263
+ demo.launch()
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio>=4.0
2
  numpy
3
  pillow
4
  opencv-python-headless
 
1
+ gradio>=5.29.1
2
  numpy
3
  pillow
4
  opencv-python-headless