Jangai commited on
Commit
b8da200
·
verified ·
1 Parent(s): 8bdf61e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +368 -179
app.py CHANGED
@@ -4,7 +4,7 @@ from pathlib import Path
4
  import numpy as np
5
  import pandas as pd
6
 
7
- # Plotly opzionale (se non installato, l'app parte comunque)
8
  try:
9
  import plotly.graph_objects as go
10
  PLOTLY_AVAILABLE = True
@@ -15,132 +15,216 @@ except Exception:
15
  import gradio as gr
16
 
17
  # ------------ OpenCascade (pythonocc-core) ------------
18
- from OCC.Core.IGESControl import IGESControl_Reader
19
- from OCC.Core.STEPControl import STEPControl_Reader, STEPControl_AsIs
20
- from OCC.Core.IFSelect import IFSelect_ReturnStatus
21
- # ProgressRange potrebbe non servire su alcune build: gestiamo in try/except
22
  try:
23
- from OCC.Core.Message import Message_ProgressRange
24
- HAS_PROGRESS = True
25
- except Exception:
26
- Message_ProgressRange = None
27
- HAS_PROGRESS = False
28
-
29
- from OCC.Core.TopoDS import TopoDS_Shape
30
- from OCC.Core.Bnd import Bnd_Box
31
- from OCC.Core.BRepBndLib import brepbndlib
32
- from OCC.Core.GProp import GProp_GProps
33
- from OCC.Core.BRepGProp import brepgprop
34
- from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Sewing
35
- from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
36
- from OCC.Core.BRep import BRep_Tool
37
- from OCC.Extend.TopologyUtils import TopologyExplorer
38
- from OCC.Core.TopLoc import TopLoc_Location
39
- from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
40
- from OCC.Core.StlAPI import StlAPI_Writer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
 
43
  # =============== Helpers geometrici ===============
44
- def _bbox(shape: TopoDS_Shape):
 
 
 
 
45
  bbox = Bnd_Box()
46
- # brepbndlib.Add è la forma moderna; il wrapper accetta (shape, bbox)
47
  brepbndlib.Add(shape, bbox)
48
  return bbox.Get() # xmin, ymin, zmin, xmax, ymax, zmax
49
 
50
- def _area(shape: TopoDS_Shape):
 
 
 
 
51
  gp = GProp_GProps()
52
  brepgprop.SurfaceProperties(shape, gp)
53
  return gp.Mass()
54
 
55
- def _volume(shape: TopoDS_Shape):
 
 
 
 
56
  gp = GProp_GProps()
57
  brepgprop.VolumeProperties(shape, gp)
58
  return gp.Mass()
59
 
60
- def _try_sew(shape: TopoDS_Shape, tol=1e-3):
 
 
 
 
61
  sew = BRepBuilderAPI_Sewing(tol, True, True, True, False)
62
- sew.Add(shape); sew.Perform()
 
63
  return sew.SewedShape()
64
 
65
- def _count_sub(shape: TopoDS_Shape, kind):
66
- # conta facce o spigoli
 
 
 
67
  cnt = 0
68
  topo = TopologyExplorer(shape)
69
  if kind == TopAbs_FACE:
70
- for _ in topo.faces(): cnt += 1
 
71
  elif kind == TopAbs_EDGE:
72
- for _ in topo.edges(): cnt += 1
 
73
  return cnt
74
 
75
  def _fmt(x):
76
- if x is None: return ""
 
 
77
  try:
78
  if isinstance(x, (int,)) or (isinstance(x, float) and abs(x) >= 1000):
79
  return f"{x:,.0f}".replace(",", " ")
80
- if isinstance(x, float): return f"{x:.4f}"
 
81
  except Exception:
82
  pass
83
  return str(x)
84
 
85
- # =============== Lettura CAD (IGES/STEP/ZIP) ===============
86
- def _save_uploaded_to_tmp(uploaded_file) -> str:
87
- """Salva l'upload Gradio (ComponentFile) in /tmp e ritorna il path."""
88
  if uploaded_file is None:
89
  return None
90
- data = uploaded_file.read()
91
- name = Path(uploaded_file.name).name
92
- dst = Path(tempfile.gettempdir()) / name
93
- with open(dst, "wb") as f:
94
- f.write(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  return str(dst)
96
 
97
- def _maybe_unzip_and_pick(path_in_tmp: str) -> str:
 
98
  p = Path(path_in_tmp)
99
  if p.suffix.lower() != ".zip":
100
  return str(p)
 
101
  out_dir = Path(tempfile.gettempdir()) / "cad_zip"
102
  out_dir.mkdir(parents=True, exist_ok=True)
103
- # pulizia
 
104
  for c in out_dir.iterdir():
105
  try:
106
- if c.is_file(): c.unlink()
 
107
  except Exception:
108
  pass
109
- with zipfile.ZipFile(str(p), "r") as zf:
110
- zf.extractall(str(out_dir))
111
- for ext in (".stp",".step",".igs",".iges",".STP",".STEP",".IGS",".IGES"):
 
 
 
 
 
 
 
112
  cand = list(out_dir.rglob(f"*{ext}"))
113
  if cand:
114
  return str(cand[0])
 
115
  return None
116
 
117
- def _read_step_with_fallback(tmp_path: str) -> TopoDS_Shape:
 
 
 
 
118
  sreader = STEPControl_Reader()
119
  status = sreader.ReadFile(tmp_path)
120
  if int(status) != int(IFSelect_ReturnStatus.IFSelect_RetDone):
121
  return None
122
- # API nuova (con ProgressRange)
 
123
  try:
124
  nroots = sreader.NbRootsForTransfer()
125
- for i in range(1, nroots+1):
126
  if HAS_PROGRESS:
127
  sreader.TransferRoot(i, STEPControl_AsIs, Message_ProgressRange())
128
  else:
129
  raise TypeError("force fallback")
130
  except Exception:
131
- # API classica / fallback
132
  try:
133
  nroots = sreader.NbRootsForTransfer()
134
- for i in range(1, nroots+1):
135
  sreader.TransferRoot(i, STEPControl_AsIs)
136
  except Exception:
137
  sreader.TransferRoots()
 
138
  shape = sreader.OneShape()
139
  return None if shape.IsNull() else shape
140
 
141
- def _read_shape_any(tmp_path: str) -> TopoDS_Shape:
 
 
 
 
142
  ext = Path(tmp_path).suffix.lower()
143
- if ext in [".igs",".iges"]:
 
144
  rdr = IGESControl_Reader()
145
  st = rdr.ReadFile(tmp_path)
146
  if int(st) != int(IFSelect_ReturnStatus.IFSelect_RetDone):
@@ -151,22 +235,26 @@ def _read_shape_any(tmp_path: str) -> TopoDS_Shape:
151
  rdr.TransferAll()
152
  shp = rdr.OneShape()
153
  return None if shp.IsNull() else shp
154
- if ext in [".stp",".step"]:
 
155
  return _read_step_with_fallback(tmp_path)
 
156
  return None
157
 
158
- # =============== Triangolazione -> Plotly 3D ===============
159
- def shape_to_plotly_mesh(shape: TopoDS_Shape, deflection=0.8):
160
- if not PLOTLY_AVAILABLE:
 
161
  return None
162
- # Triangola
 
163
  BRepMesh_IncrementalMesh(shape, deflection, True, 0.5, True)
164
  vertices = []
165
  faces_i, faces_j, faces_k = [], [], []
166
  vmap = {}
167
 
168
  def add_vertex(p):
169
- key = (round(p.X(),6), round(p.Y(),6), round(p.Z(),6))
170
  idx = vmap.get(key)
171
  if idx is None:
172
  idx = len(vertices)
@@ -180,55 +268,73 @@ def shape_to_plotly_mesh(shape: TopoDS_Shape, deflection=0.8):
180
  tri = BRep_Tool.Triangulation(face, loc)
181
  if tri is None:
182
  continue
 
183
  trsf = loc.Transformation()
184
-
 
185
  has_nodes_attr = hasattr(tri, "Nodes") and callable(getattr(tri, "Nodes"))
186
- has_tris_attr = hasattr(tri, "Triangles") and callable(getattr(tri, "Triangles"))
187
 
188
  if has_nodes_attr and has_tris_attr:
189
  nodes = tri.Nodes()
190
- tris = tri.Triangles()
191
- for t in range(1, tri.NbTriangles()+1):
192
  n1, n2, n3 = tris.Value(t).Get()
193
  p1 = nodes.Value(n1).Transformed(trsf)
194
  p2 = nodes.Value(n2).Transformed(trsf)
195
  p3 = nodes.Value(n3).Transformed(trsf)
196
- i1 = add_vertex(p1); i2 = add_vertex(p2); i3 = add_vertex(p3)
197
- faces_i.append(i1); faces_j.append(i2); faces_k.append(i3)
 
 
 
 
198
  else:
199
- # API alternativa
200
- for t in range(1, tri.NbTriangles()+1):
201
  tri_t = tri.Triangle(t)
202
  n1, n2, n3 = tri_t.Get()
203
  p1 = tri.Node(n1).Transformed(trsf)
204
  p2 = tri.Node(n2).Transformed(trsf)
205
  p3 = tri.Node(n3).Transformed(trsf)
206
- i1 = add_vertex(p1); i2 = add_vertex(p2); i3 = add_vertex(p3)
207
- faces_i.append(i1); faces_j.append(i2); faces_k.append(i3)
 
 
 
 
208
 
209
  if len(vertices) == 0 or len(faces_i) == 0:
210
- return None # triangolazione vuota
211
 
212
  verts = np.array(vertices, dtype=float)
213
  fig = go.Figure(data=[
214
  go.Mesh3d(
215
- x=verts[:,0], y=verts[:,1], z=verts[:,2],
216
  i=faces_i, j=faces_j, k=faces_k,
217
  flatshading=True, opacity=1.0, showscale=False
218
  )
219
  ])
220
- fig.update_layout(scene_aspectmode="data", margin=dict(l=0, r=0, t=30, b=0),
221
- title="Anteprima 3D")
222
- fig.update_scenes(xaxis_visible=False, yaxis_visible=False, zaxis_visible=False)
 
 
 
 
 
 
 
223
  return fig
224
 
225
- # =============== Scoring complessità ===============
226
  def score_complessita(dimx, dimy, dimz, area, volume,
227
  n_faces=None, n_edges=None,
228
  aspect_ratio=None, sv_ratio=None,
229
  solidity=None, edge_density=None,
230
  weights=None):
231
- # weights: dict con chiavi specifiche
 
232
  w = {
233
  "w_long": 0.25, "w_area": 0.15, "w_ar": 0.15,
234
  "w_sv": 0.10, "w_sol": 0.10, "w_feat": 0.20, "w_edns": 0.05
@@ -239,34 +345,41 @@ def score_complessita(dimx, dimy, dimz, area, volume,
239
  longest = max(dimx, dimy, dimz)
240
  s_long = min(longest / 600.0, 2.0)
241
  s_area = min(area / 1.5e6, 2.0)
242
- s_ar = min((aspect_ratio or 1.0) / 3.0, 2.0)
243
- s_sv = min((sv_ratio or 0.0) / 0.02, 2.0)
244
- s_sol = 2.0 - min((solidity or 1.0), 2.0)
245
- s_feat = min(((n_faces or 0)/200.0)+((n_edges or 0)/1000.0), 2.0)
246
  s_edns = min((edge_density or 0.0) / 0.005, 2.0)
247
 
248
- score = (w["w_long"]*s_long + w["w_area"]*s_area + w["w_ar"]*s_ar +
249
- w["w_sv"]*s_sv + w["w_sol"]*s_sol + w["w_feat"]*s_feat +
250
- w["w_edns"]*s_edns)
251
  return float(score)
252
 
253
- def classe_da_score(score: float, th_easy=0.6, th_medium=1.2):
254
- if score < th_easy: return "facile"
255
- if score < th_medium: return "medio"
 
 
 
256
  return "difficile"
257
 
258
- # =============== Card HTML ===============
259
  def render_card_html(file_name, volume, area, dimx, dimy, dimz, classe=None, score=None):
260
- def f(x): return _fmt(x)
 
 
 
261
  badge = f"{classe.upper()} — score {score:.2f}" if (classe and score is not None) else "—"
262
  color = {"facile": "#22c55e", "medio": "#f59e0b", "difficile": "#ef4444"}.get((classe or ""), "#0ea5e9")
 
263
  html = f"""
264
  <div style="font-family: ui-sans-serif,system-ui; max-width: 980px; border-radius:16px; padding:20px 24px;
265
  background: linear-gradient(135deg,#0f172a 0%,#0b132b 45%,#1b2a49 100%); color:#e5e7eb;
266
  box-shadow:0 10px 30px rgba(0,0,0,.35);">
267
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
268
- <div style="font-size:18px;opacity:.9;">Analisi CAD (IGES/STEP)</div>
269
- <div style="font-size:12px;opacity:.6;">Unità come nel modello</div>
270
  </div>
271
  <div style="font-size:22px;font-weight:700;margin-bottom:16px;">{file_name}</div>
272
  <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;">
@@ -283,7 +396,7 @@ def render_card_html(file_name, volume, area, dimx, dimy, dimz, classe=None, sco
283
  <div style="font-size:18px;font-weight:700;margin-top:4px;">{f(dimx)} × {f(dimy)} × {f(dimz)}</div>
284
  </div>
285
  <div style="background:#0b1220;border:1px solid #1f2937;border-radius:14px;padding:16px;">
286
- <div style="font-size:12px;color:#9ca3af;">Complessità</div>
287
  <div style="font-size:18px;font-weight:800;margin-top:4px;color:{color};">{badge}</div>
288
  </div>
289
  </div>
@@ -291,71 +404,113 @@ def render_card_html(file_name, volume, area, dimx, dimy, dimz, classe=None, sco
291
  """
292
  return html
293
 
294
- # =============== Pipeline principale ===============
295
  def analyze(file_obj, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy, th_medium):
 
296
  logs = []
 
297
  def log(x):
298
  ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
299
  line = f"[{ts}] {x}"
300
  logs.append(line)
 
301
 
302
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  if file_obj is None:
304
- return "Seleziona un file", None, None, pd.DataFrame(), "\n".join(logs), None, None
 
305
 
306
- # 1) salva temp
307
  tmp_path = _save_uploaded_to_tmp(file_obj)
308
- log(f"Upload salvato in: {tmp_path}")
309
- # 2) zip?
 
 
 
 
 
 
310
  picked = _maybe_unzip_and_pick(tmp_path)
311
  if picked is None:
312
- log("ZIP senza CAD supportati (.stp/.igs).")
313
- return "File ZIP senza STEP/IGES", None, None, pd.DataFrame(), "\n".join(logs), None, None
 
 
314
  if picked != tmp_path:
315
- log(f"ZIP estrattoselezionato: {picked}")
316
  else:
317
- log(f"File selezionato: {picked}")
318
 
319
- # 3) leggi CAD
320
  shape = _read_shape_any(picked)
321
  if (shape is None) or shape.IsNull():
322
- log("Lettura CAD fallita (shape nulla).")
323
- return "Errore lettura CAD", None, None, pd.DataFrame(), "\n".join(logs), None, None
 
324
 
325
- # 4) metriche
326
  xmin, ymin, zmin, xmax, ymax, zmax = _bbox(shape)
327
  dimx, dimy, dimz = (xmax - xmin), (ymax - ymin), (zmax - zmin)
328
- area = _area(shape); log(f"Area: {area}")
329
- volume = _volume(shape); log(f"Volume: {volume}")
 
 
330
 
 
331
  if volume == 0.0:
332
- log("Volume=0 → sewing...")
333
  sewn = _try_sew(shape, tol=1e-3)
334
  if not sewn.IsNull():
335
  v2 = _volume(sewn)
336
- log(f"Volume dopo sewing: {v2}")
337
- if v2 > 0: shape, volume = sewn, v2
338
-
339
- n_faces = _count_sub(shape, TopAbs_FACE); log(f"#Facce: {n_faces}")
340
- n_edges = _count_sub(shape, TopAbs_EDGE); log(f"#Spigoli: {n_edges}")
341
- bbox_vol = (dimx * dimy * dimz) if all(v>0 for v in (dimx, dimy, dimz)) else 0.0
 
 
 
 
342
  sv_ratio = (area / volume) if volume and volume > 0 else None
343
  solidity = (volume / bbox_vol) if bbox_vol and volume and volume > 0 else None
344
  aspect_ratio = max(dimx, dimy, dimz) / max(1e-9, min(dimx, dimy, dimz))
345
  edge_density = (n_edges / area) if area and area > 0 else None
346
 
347
- # 5) score & classe
348
  weights = dict(
349
  w_long=w_long, w_area=w_area, w_ar=w_ar,
350
  w_sv=w_sv, w_sol=w_sol, w_feat=w_feat, w_edns=w_edns
351
  )
352
  score = score_complessita(dimx, dimy, dimz, area, volume,
353
- n_faces, n_edges, aspect_ratio, sv_ratio, solidity, edge_density,
354
- weights=weights)
355
  classe = classe_da_score(score, th_easy=th_easy, th_medium=th_medium)
356
- log(f"Score: {score:.3f} | Classe: {classe}")
357
 
358
- # 6) DataFrame
359
  df = pd.DataFrame([{
360
  "File": Path(picked).name,
361
  "Volume (u^3)": float(volume),
@@ -372,37 +527,36 @@ def analyze(file_obj, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy
372
  "Classe complessità": classe
373
  }])
374
 
375
- # 7) Export CSV/XLSX (file scaricabili)
376
  tmpdir = Path(tempfile.gettempdir())
377
- csv_path = tmpdir / "cad_report.csv"
378
- xlsx_path = tmpdir / "cad_report.xlsx"
379
  df.to_csv(csv_path, index=False)
 
 
 
380
  out_xlsx = None
381
  try:
382
- # se openpyxl non c'è, non bloccare
383
- import openpyxl # noqa
384
  df.to_excel(xlsx_path, index=False)
385
  out_xlsx = str(xlsx_path)
386
  log(f"Export: {csv_path}, {xlsx_path}")
387
  except Exception as e:
388
- log(f"Excel non creato (openpyxl mancante o errore): {e}")
389
- out_csv = str(csv_path)
390
 
391
- # 8) Render 3D
392
  fig = shape_to_plotly_mesh(shape, deflection=0.8)
393
  if fig is None and PLOTLY_AVAILABLE:
394
- # triangolazione vuotaprova STL per debug
395
  try:
396
  stl_path = tmpdir / "mesh_fallback.stl"
397
  StlAPI_Writer().Write(shape, str(stl_path))
398
- log(f"Triangolazione vuota → STL salvato: {stl_path}")
399
  except Exception as ee:
400
- log(f"Fallback STL fallito: {ee}")
401
 
402
- # 9) Card HTML
403
  card = render_card_html(Path(picked).name, volume, area, dimx, dimy, dimz, classe, score)
404
 
405
- # Ritorni
406
  return (
407
  f"{classe} (score {score:.2f})",
408
  card,
@@ -415,58 +569,93 @@ def analyze(file_obj, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy
415
 
416
  except Exception:
417
  logs.append("EXCEPTION:\n" + traceback.format_exc())
418
- return "Errore in analisi", None, None, pd.DataFrame(), "\n".join(logs), None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
-
421
- # =============== Interfaccia Gradio ===============
422
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
423
- gr.Markdown("# CAD Complexity Demo (IGES/STEP)")
424
- gr.Markdown("Carica un file **.stp/.step/.igs/.iges** (oppure uno **.zip** che lo contiene), "
425
- "regola i parametri (opzionale) e clicca **Analizza**.")
426
-
427
- with gr.Row():
428
- file_in = gr.File(label="File CAD (STEP/IGES o ZIP)", file_types=[".stp",".step",".igs",".iges",".zip"])
429
-
430
- with gr.Accordion("Parametri complessità (default consigliati)", open=False):
431
- gr.Markdown("**Pesi (somma ≈ 1):**")
432
- with gr.Row():
433
- w_long = gr.Slider(0, 1, value=0.25, step=0.01, label="Peso Ingombro max (w_long)")
434
- w_area = gr.Slider(0, 1, value=0.15, step=0.01, label="Peso Area (w_area)")
435
- w_ar = gr.Slider(0, 1, value=0.15, step=0.01, label="Peso Aspect Ratio (w_ar)")
436
  with gr.Row():
437
- w_sv = gr.Slider(0, 1, value=0.10, step=0.01, label="Peso Surface/Volume (w_sv)")
438
- w_sol = gr.Slider(0, 1, value=0.10, step=0.01, label="Peso Solidity (w_sol)")
439
- w_feat = gr.Slider(0, 1, value=0.20, step=0.01, label="Peso Feature count (w_feat)")
440
- with gr.Row():
441
- w_edns = gr.Slider(0, 1, value=0.05, step=0.01, label="Peso Densità spigoli (w_edns)")
442
- gr.Markdown("**Soglie classe:**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  with gr.Row():
444
- th_easy = gr.Slider(0.0, 2.0, value=0.60, step=0.01, label="Soglia FACILE (<)")
445
- th_medium = gr.Slider(0.0, 2.5, value=1.20, step=0.01, label="Soglia MEDIO (<; altrimenti DIFFICILE)")
446
 
447
- btn = gr.Button("Analizza", variant="primary")
 
 
 
 
 
 
448
 
449
- with gr.Row():
450
- complexity_out = gr.Textbox(label="Complessità", interactive=False)
451
- card_out = gr.HTML(label="Card")
452
 
453
- mesh_out = gr.Plot(
454
- label="Anteprima 3D (Plotly)" if PLOTLY_AVAILABLE else "Anteprima 3D (disabilitata: Plotly non installato)",
455
- height=520
456
- )
457
- df_out = gr.Dataframe(label="Parametri di complessità", row_count=1, wrap=True)
458
- logs_out = gr.Textbox(label="Log", lines=12)
459
 
460
- with gr.Row():
461
- csv_out = gr.File(label="Scarica CSV", visible=True)
462
- xlsx_out = gr.File(label="Scarica XLSX", visible=True)
463
 
464
- btn.click(
465
- analyze,
466
- inputs=[file_in, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy, th_medium],
467
- outputs=[complexity_out, card_out, mesh_out, df_out, logs_out, csv_out, xlsx_out]
468
- )
469
 
 
470
  if __name__ == "__main__":
471
- # In locale puoi fare demo.launch(); su Spaces non serve cambiare
472
- demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import numpy as np
5
  import pandas as pd
6
 
7
+ # Plotly optional (if not installed, app still starts)
8
  try:
9
  import plotly.graph_objects as go
10
  PLOTLY_AVAILABLE = True
 
15
  import gradio as gr
16
 
17
  # ------------ OpenCascade (pythonocc-core) ------------
 
 
 
 
18
  try:
19
+ from OCC.Core.IGESControl import IGESControl_Reader
20
+ from OCC.Core.STEPControl import STEPControl_Reader, STEPControl_AsIs
21
+ from OCC.Core.IFSelect import IFSelect_ReturnStatus
22
+ # ProgressRange might not be available on some builds
23
+ try:
24
+ from OCC.Core.Message import Message_ProgressRange
25
+ HAS_PROGRESS = True
26
+ except Exception:
27
+ Message_ProgressRange = None
28
+ HAS_PROGRESS = False
29
+
30
+ from OCC.Core.TopoDS import TopoDS_Shape
31
+ from OCC.Core.Bnd import Bnd_Box
32
+ from OCC.Core.BRepBndLib import brepbndlib
33
+ from OCC.Core.GProp import GProp_GProps
34
+ from OCC.Core.BRepGProp import brepgprop
35
+ from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Sewing
36
+ from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
37
+ from OCC.Core.BRep import BRep_Tool
38
+ from OCC.Extend.TopologyUtils import TopologyExplorer
39
+ from OCC.Core.TopLoc import TopLoc_Location
40
+ from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
41
+ from OCC.Core.StlAPI import StlAPI_Writer
42
+
43
+ OCC_AVAILABLE = True
44
+ except ImportError as e:
45
+ print(f"OpenCascade not available: {e}")
46
+ OCC_AVAILABLE = False
47
+ # Define dummy classes to prevent errors
48
+ class TopoDS_Shape:
49
+ def IsNull(self): return True
50
+ class TopAbs_FACE: pass
51
+ class TopAbs_EDGE: pass
52
 
53
 
54
  # =============== Helpers geometrici ===============
55
+ def _bbox(shape):
56
+ """Calculate bounding box of shape"""
57
+ if not OCC_AVAILABLE:
58
+ return (0, 0, 0, 100, 100, 100)
59
+
60
  bbox = Bnd_Box()
 
61
  brepbndlib.Add(shape, bbox)
62
  return bbox.Get() # xmin, ymin, zmin, xmax, ymax, zmax
63
 
64
+ def _area(shape):
65
+ """Calculate surface area of shape"""
66
+ if not OCC_AVAILABLE:
67
+ return 1000.0
68
+
69
  gp = GProp_GProps()
70
  brepgprop.SurfaceProperties(shape, gp)
71
  return gp.Mass()
72
 
73
+ def _volume(shape):
74
+ """Calculate volume of shape"""
75
+ if not OCC_AVAILABLE:
76
+ return 100.0
77
+
78
  gp = GProp_GProps()
79
  brepgprop.VolumeProperties(shape, gp)
80
  return gp.Mass()
81
 
82
+ def _try_sew(shape, tol=1e-3):
83
+ """Try to sew shape faces together"""
84
+ if not OCC_AVAILABLE:
85
+ return shape
86
+
87
  sew = BRepBuilderAPI_Sewing(tol, True, True, True, False)
88
+ sew.Add(shape)
89
+ sew.Perform()
90
  return sew.SewedShape()
91
 
92
+ def _count_sub(shape, kind):
93
+ """Count faces or edges"""
94
+ if not OCC_AVAILABLE:
95
+ return 50 if kind == TopAbs_FACE else 200
96
+
97
  cnt = 0
98
  topo = TopologyExplorer(shape)
99
  if kind == TopAbs_FACE:
100
+ for _ in topo.faces():
101
+ cnt += 1
102
  elif kind == TopAbs_EDGE:
103
+ for _ in topo.edges():
104
+ cnt += 1
105
  return cnt
106
 
107
  def _fmt(x):
108
+ """Format numbers for display"""
109
+ if x is None:
110
+ return "—"
111
  try:
112
  if isinstance(x, (int,)) or (isinstance(x, float) and abs(x) >= 1000):
113
  return f"{x:,.0f}".replace(",", " ")
114
+ if isinstance(x, float):
115
+ return f"{x:.4f}"
116
  except Exception:
117
  pass
118
  return str(x)
119
 
120
+ # =============== File handling ===============
121
+ def _save_uploaded_to_tmp(uploaded_file):
122
+ """Save Gradio upload to temp file and return path"""
123
  if uploaded_file is None:
124
  return None
125
+
126
+ # Handle different Gradio file object types
127
+ if hasattr(uploaded_file, 'name'):
128
+ file_name = Path(uploaded_file.name).name
129
+ file_path = uploaded_file.name
130
+ else:
131
+ file_name = "uploaded_file"
132
+ file_path = uploaded_file
133
+
134
+ # Copy to temp directory with proper name
135
+ dst = Path(tempfile.gettempdir()) / file_name
136
+
137
+ try:
138
+ if hasattr(uploaded_file, 'name') and os.path.exists(uploaded_file.name):
139
+ # File is already saved by Gradio
140
+ import shutil
141
+ shutil.copy2(uploaded_file.name, dst)
142
+ else:
143
+ # Read file content
144
+ if hasattr(uploaded_file, 'read'):
145
+ data = uploaded_file.read()
146
+ else:
147
+ with open(uploaded_file, 'rb') as f:
148
+ data = f.read()
149
+
150
+ with open(dst, "wb") as f:
151
+ f.write(data)
152
+ except Exception as e:
153
+ print(f"Error saving file: {e}")
154
+ return None
155
+
156
  return str(dst)
157
 
158
+ def _maybe_unzip_and_pick(path_in_tmp):
159
+ """Extract ZIP and find CAD file"""
160
  p = Path(path_in_tmp)
161
  if p.suffix.lower() != ".zip":
162
  return str(p)
163
+
164
  out_dir = Path(tempfile.gettempdir()) / "cad_zip"
165
  out_dir.mkdir(parents=True, exist_ok=True)
166
+
167
+ # Clean up existing files
168
  for c in out_dir.iterdir():
169
  try:
170
+ if c.is_file():
171
+ c.unlink()
172
  except Exception:
173
  pass
174
+
175
+ try:
176
+ with zipfile.ZipFile(str(p), "r") as zf:
177
+ zf.extractall(str(out_dir))
178
+ except Exception as e:
179
+ print(f"Error extracting ZIP: {e}")
180
+ return None
181
+
182
+ # Look for CAD files
183
+ for ext in (".stp", ".step", ".igs", ".iges", ".STP", ".STEP", ".IGS", ".IGES"):
184
  cand = list(out_dir.rglob(f"*{ext}"))
185
  if cand:
186
  return str(cand[0])
187
+
188
  return None
189
 
190
+ def _read_step_with_fallback(tmp_path):
191
+ """Read STEP file with fallback for different API versions"""
192
+ if not OCC_AVAILABLE:
193
+ return None
194
+
195
  sreader = STEPControl_Reader()
196
  status = sreader.ReadFile(tmp_path)
197
  if int(status) != int(IFSelect_ReturnStatus.IFSelect_RetDone):
198
  return None
199
+
200
+ # Try new API first (with ProgressRange)
201
  try:
202
  nroots = sreader.NbRootsForTransfer()
203
+ for i in range(1, nroots + 1):
204
  if HAS_PROGRESS:
205
  sreader.TransferRoot(i, STEPControl_AsIs, Message_ProgressRange())
206
  else:
207
  raise TypeError("force fallback")
208
  except Exception:
209
+ # Fallback to classic API
210
  try:
211
  nroots = sreader.NbRootsForTransfer()
212
+ for i in range(1, nroots + 1):
213
  sreader.TransferRoot(i, STEPControl_AsIs)
214
  except Exception:
215
  sreader.TransferRoots()
216
+
217
  shape = sreader.OneShape()
218
  return None if shape.IsNull() else shape
219
 
220
+ def _read_shape_any(tmp_path):
221
+ """Read any supported CAD file format"""
222
+ if not OCC_AVAILABLE:
223
+ return None
224
+
225
  ext = Path(tmp_path).suffix.lower()
226
+
227
+ if ext in [".igs", ".iges"]:
228
  rdr = IGESControl_Reader()
229
  st = rdr.ReadFile(tmp_path)
230
  if int(st) != int(IFSelect_ReturnStatus.IFSelect_RetDone):
 
235
  rdr.TransferAll()
236
  shp = rdr.OneShape()
237
  return None if shp.IsNull() else shp
238
+
239
+ if ext in [".stp", ".step"]:
240
  return _read_step_with_fallback(tmp_path)
241
+
242
  return None
243
 
244
+ # =============== 3D Visualization ===============
245
+ def shape_to_plotly_mesh(shape, deflection=0.8):
246
+ """Convert CAD shape to Plotly 3D mesh"""
247
+ if not PLOTLY_AVAILABLE or not OCC_AVAILABLE:
248
  return None
249
+
250
+ # Triangulate the shape
251
  BRepMesh_IncrementalMesh(shape, deflection, True, 0.5, True)
252
  vertices = []
253
  faces_i, faces_j, faces_k = [], [], []
254
  vmap = {}
255
 
256
  def add_vertex(p):
257
+ key = (round(p.X(), 6), round(p.Y(), 6), round(p.Z(), 6))
258
  idx = vmap.get(key)
259
  if idx is None:
260
  idx = len(vertices)
 
268
  tri = BRep_Tool.Triangulation(face, loc)
269
  if tri is None:
270
  continue
271
+
272
  trsf = loc.Transformation()
273
+
274
+ # Handle different API versions
275
  has_nodes_attr = hasattr(tri, "Nodes") and callable(getattr(tri, "Nodes"))
276
+ has_tris_attr = hasattr(tri, "Triangles") and callable(getattr(tri, "Triangles"))
277
 
278
  if has_nodes_attr and has_tris_attr:
279
  nodes = tri.Nodes()
280
+ tris = tri.Triangles()
281
+ for t in range(1, tri.NbTriangles() + 1):
282
  n1, n2, n3 = tris.Value(t).Get()
283
  p1 = nodes.Value(n1).Transformed(trsf)
284
  p2 = nodes.Value(n2).Transformed(trsf)
285
  p3 = nodes.Value(n3).Transformed(trsf)
286
+ i1 = add_vertex(p1)
287
+ i2 = add_vertex(p2)
288
+ i3 = add_vertex(p3)
289
+ faces_i.append(i1)
290
+ faces_j.append(i2)
291
+ faces_k.append(i3)
292
  else:
293
+ # Alternative API
294
+ for t in range(1, tri.NbTriangles() + 1):
295
  tri_t = tri.Triangle(t)
296
  n1, n2, n3 = tri_t.Get()
297
  p1 = tri.Node(n1).Transformed(trsf)
298
  p2 = tri.Node(n2).Transformed(trsf)
299
  p3 = tri.Node(n3).Transformed(trsf)
300
+ i1 = add_vertex(p1)
301
+ i2 = add_vertex(p2)
302
+ i3 = add_vertex(p3)
303
+ faces_i.append(i1)
304
+ faces_j.append(i2)
305
+ faces_k.append(i3)
306
 
307
  if len(vertices) == 0 or len(faces_i) == 0:
308
+ return None # Empty triangulation
309
 
310
  verts = np.array(vertices, dtype=float)
311
  fig = go.Figure(data=[
312
  go.Mesh3d(
313
+ x=verts[:, 0], y=verts[:, 1], z=verts[:, 2],
314
  i=faces_i, j=faces_j, k=faces_k,
315
  flatshading=True, opacity=1.0, showscale=False
316
  )
317
  ])
318
+ fig.update_layout(
319
+ scene_aspectmode="data",
320
+ margin=dict(l=0, r=0, t=30, b=0),
321
+ title="3D Preview"
322
+ )
323
+ fig.update_scenes(
324
+ xaxis_visible=False,
325
+ yaxis_visible=False,
326
+ zaxis_visible=False
327
+ )
328
  return fig
329
 
330
+ # =============== Complexity Scoring ===============
331
  def score_complessita(dimx, dimy, dimz, area, volume,
332
  n_faces=None, n_edges=None,
333
  aspect_ratio=None, sv_ratio=None,
334
  solidity=None, edge_density=None,
335
  weights=None):
336
+ """Calculate complexity score based on various geometric parameters"""
337
+ # Default weights
338
  w = {
339
  "w_long": 0.25, "w_area": 0.15, "w_ar": 0.15,
340
  "w_sv": 0.10, "w_sol": 0.10, "w_feat": 0.20, "w_edns": 0.05
 
345
  longest = max(dimx, dimy, dimz)
346
  s_long = min(longest / 600.0, 2.0)
347
  s_area = min(area / 1.5e6, 2.0)
348
+ s_ar = min((aspect_ratio or 1.0) / 3.0, 2.0)
349
+ s_sv = min((sv_ratio or 0.0) / 0.02, 2.0)
350
+ s_sol = 2.0 - min((solidity or 1.0), 2.0)
351
+ s_feat = min(((n_faces or 0) / 200.0) + ((n_edges or 0) / 1000.0), 2.0)
352
  s_edns = min((edge_density or 0.0) / 0.005, 2.0)
353
 
354
+ score = (w["w_long"] * s_long + w["w_area"] * s_area + w["w_ar"] * s_ar +
355
+ w["w_sv"] * s_sv + w["w_sol"] * s_sol + w["w_feat"] * s_feat +
356
+ w["w_edns"] * s_edns)
357
  return float(score)
358
 
359
+ def classe_da_score(score, th_easy=0.6, th_medium=1.2):
360
+ """Classify complexity based on score"""
361
+ if score < th_easy:
362
+ return "facile"
363
+ if score < th_medium:
364
+ return "medio"
365
  return "difficile"
366
 
367
+ # =============== HTML Card Rendering ===============
368
  def render_card_html(file_name, volume, area, dimx, dimy, dimz, classe=None, score=None):
369
+ """Render results as HTML card"""
370
+ def f(x):
371
+ return _fmt(x)
372
+
373
  badge = f"{classe.upper()} — score {score:.2f}" if (classe and score is not None) else "—"
374
  color = {"facile": "#22c55e", "medio": "#f59e0b", "difficile": "#ef4444"}.get((classe or ""), "#0ea5e9")
375
+
376
  html = f"""
377
  <div style="font-family: ui-sans-serif,system-ui; max-width: 980px; border-radius:16px; padding:20px 24px;
378
  background: linear-gradient(135deg,#0f172a 0%,#0b132b 45%,#1b2a49 100%); color:#e5e7eb;
379
  box-shadow:0 10px 30px rgba(0,0,0,.35);">
380
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
381
+ <div style="font-size:18px;opacity:.9;">CAD Analysis (IGES/STEP)</div>
382
+ <div style="font-size:12px;opacity:.6;">Units as in model</div>
383
  </div>
384
  <div style="font-size:22px;font-weight:700;margin-bottom:16px;">{file_name}</div>
385
  <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;">
 
396
  <div style="font-size:18px;font-weight:700;margin-top:4px;">{f(dimx)} × {f(dimy)} × {f(dimz)}</div>
397
  </div>
398
  <div style="background:#0b1220;border:1px solid #1f2937;border-radius:14px;padding:16px;">
399
+ <div style="font-size:12px;color:#9ca3af;">Complexity</div>
400
  <div style="font-size:18px;font-weight:800;margin-top:4px;color:{color};">{badge}</div>
401
  </div>
402
  </div>
 
404
  """
405
  return html
406
 
407
+ # =============== Main Analysis Pipeline ===============
408
  def analyze(file_obj, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy, th_medium):
409
+ """Main analysis function"""
410
  logs = []
411
+
412
  def log(x):
413
  ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
414
  line = f"[{ts}] {x}"
415
  logs.append(line)
416
+ print(line) # Also print to console for debugging
417
 
418
  try:
419
+ if not OCC_AVAILABLE:
420
+ log("OpenCascade (pythonocc-core) not available - demo mode")
421
+ # Return demo data
422
+ demo_df = pd.DataFrame([{
423
+ "File": "demo_model.stp",
424
+ "Volume (u^3)": 1000.0,
425
+ "Area (u^2)": 2000.0,
426
+ "Dim X": 100.0, "Dim Y": 50.0, "Dim Z": 20.0,
427
+ "BBox xmin": 0.0, "BBox ymin": 0.0, "BBox zmin": 0.0,
428
+ "BBox xmax": 100.0, "BBox ymax": 50.0, "BBox zmax": 20.0,
429
+ "# Facce": 50, "# Spigoli": 200,
430
+ "Aspect ratio": 5.0,
431
+ "Surface/Volume": 2.0,
432
+ "Solidity (Vol/BBoxVol)": 1.0,
433
+ "Densità spigoli (edges/area)": 0.1,
434
+ "Score complessità": 0.8,
435
+ "Classe complessità": "medio"
436
+ }])
437
+ card = render_card_html("demo_model.stp", 1000, 2000, 100, 50, 20, "medio", 0.8)
438
+ return ("medio (score 0.80)", card, None, demo_df,
439
+ "\n".join(logs) + "\nDemo mode - OpenCascade not available", None, None)
440
+
441
  if file_obj is None:
442
+ return ("Select a file", None, None, pd.DataFrame(),
443
+ "\n".join(logs), None, None)
444
 
445
+ # 1) Save temp file
446
  tmp_path = _save_uploaded_to_tmp(file_obj)
447
+ if tmp_path is None:
448
+ log("Failed to save uploaded file")
449
+ return ("File save error", None, None, pd.DataFrame(),
450
+ "\n".join(logs), None, None)
451
+
452
+ log(f"Upload saved to: {tmp_path}")
453
+
454
+ # 2) Handle ZIP files
455
  picked = _maybe_unzip_and_pick(tmp_path)
456
  if picked is None:
457
+ log("ZIP without supported CAD files (.stp/.igs)")
458
+ return ("ZIP without STEP/IGES", None, None, pd.DataFrame(),
459
+ "\n".join(logs), None, None)
460
+
461
  if picked != tmp_path:
462
+ log(f"ZIP extractedselected: {picked}")
463
  else:
464
+ log(f"File selected: {picked}")
465
 
466
+ # 3) Read CAD file
467
  shape = _read_shape_any(picked)
468
  if (shape is None) or shape.IsNull():
469
+ log("CAD reading failed (null shape)")
470
+ return ("CAD reading error", None, None, pd.DataFrame(),
471
+ "\n".join(logs), None, None)
472
 
473
+ # 4) Calculate metrics
474
  xmin, ymin, zmin, xmax, ymax, zmax = _bbox(shape)
475
  dimx, dimy, dimz = (xmax - xmin), (ymax - ymin), (zmax - zmin)
476
+ area = _area(shape)
477
+ log(f"Area: {area}")
478
+ volume = _volume(shape)
479
+ log(f"Volume: {volume}")
480
 
481
+ # Try sewing if volume is zero
482
  if volume == 0.0:
483
+ log("Volume=0 → trying sewing...")
484
  sewn = _try_sew(shape, tol=1e-3)
485
  if not sewn.IsNull():
486
  v2 = _volume(sewn)
487
+ log(f"Volume after sewing: {v2}")
488
+ if v2 > 0:
489
+ shape, volume = sewn, v2
490
+
491
+ n_faces = _count_sub(shape, TopAbs_FACE)
492
+ log(f"#Faces: {n_faces}")
493
+ n_edges = _count_sub(shape, TopAbs_EDGE)
494
+ log(f"#Edges: {n_edges}")
495
+
496
+ bbox_vol = (dimx * dimy * dimz) if all(v > 0 for v in (dimx, dimy, dimz)) else 0.0
497
  sv_ratio = (area / volume) if volume and volume > 0 else None
498
  solidity = (volume / bbox_vol) if bbox_vol and volume and volume > 0 else None
499
  aspect_ratio = max(dimx, dimy, dimz) / max(1e-9, min(dimx, dimy, dimz))
500
  edge_density = (n_edges / area) if area and area > 0 else None
501
 
502
+ # 5) Calculate complexity score and class
503
  weights = dict(
504
  w_long=w_long, w_area=w_area, w_ar=w_ar,
505
  w_sv=w_sv, w_sol=w_sol, w_feat=w_feat, w_edns=w_edns
506
  )
507
  score = score_complessita(dimx, dimy, dimz, area, volume,
508
+ n_faces, n_edges, aspect_ratio, sv_ratio,
509
+ solidity, edge_density, weights=weights)
510
  classe = classe_da_score(score, th_easy=th_easy, th_medium=th_medium)
511
+ log(f"Score: {score:.3f} | Class: {classe}")
512
 
513
+ # 6) Create DataFrame
514
  df = pd.DataFrame([{
515
  "File": Path(picked).name,
516
  "Volume (u^3)": float(volume),
 
527
  "Classe complessità": classe
528
  }])
529
 
530
+ # 7) Export CSV/XLSX (downloadable files)
531
  tmpdir = Path(tempfile.gettempdir())
532
+ csv_path = tmpdir / "cad_report.csv"
 
533
  df.to_csv(csv_path, index=False)
534
+ out_csv = str(csv_path)
535
+
536
+ xlsx_path = tmpdir / "cad_report.xlsx"
537
  out_xlsx = None
538
  try:
 
 
539
  df.to_excel(xlsx_path, index=False)
540
  out_xlsx = str(xlsx_path)
541
  log(f"Export: {csv_path}, {xlsx_path}")
542
  except Exception as e:
543
+ log(f"Excel not created (openpyxl missing or error): {e}")
 
544
 
545
+ # 8) Generate 3D visualization
546
  fig = shape_to_plotly_mesh(shape, deflection=0.8)
547
  if fig is None and PLOTLY_AVAILABLE:
548
+ # Empty triangulationtry STL for debug
549
  try:
550
  stl_path = tmpdir / "mesh_fallback.stl"
551
  StlAPI_Writer().Write(shape, str(stl_path))
552
+ log(f"Empty triangulation → STL saved: {stl_path}")
553
  except Exception as ee:
554
+ log(f"STL fallback failed: {ee}")
555
 
556
+ # 9) Generate HTML card
557
  card = render_card_html(Path(picked).name, volume, area, dimx, dimy, dimz, classe, score)
558
 
559
+ # Return results
560
  return (
561
  f"{classe} (score {score:.2f})",
562
  card,
 
569
 
570
  except Exception:
571
  logs.append("EXCEPTION:\n" + traceback.format_exc())
572
+ return ("Analysis error", None, None, pd.DataFrame(),
573
+ "\n".join(logs), None, None)
574
+
575
+
576
+ # =============== Gradio Interface ===============
577
+ def create_interface():
578
+ """Create and configure Gradio interface"""
579
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
580
+ gr.Markdown("# CAD Complexity Analyzer (IGES/STEP)")
581
+
582
+ if not OCC_AVAILABLE:
583
+ gr.Markdown("""
584
+ ⚠️ **Demo Mode**: OpenCascade (pythonocc-core) is not available.
585
+ The interface will show demo data instead of actual CAD analysis.
586
+
587
+ To enable full functionality, install: `pip install pythonocc-core`
588
+ """)
589
+
590
+ gr.Markdown("""
591
+ Upload a **.stp/.step/.igs/.iges** file (or a **.zip** containing one),
592
+ adjust parameters (optional), and click **Analyze**.
593
+ """)
594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  with gr.Row():
596
+ file_in = gr.File(
597
+ label="CAD File (STEP/IGES or ZIP)",
598
+ file_types=[".stp", ".step", ".igs", ".iges", ".zip"]
599
+ )
600
+
601
+ with gr.Accordion("Complexity Parameters (recommended defaults)", open=False):
602
+ gr.Markdown("**Weights (sum ≈ 1):**")
603
+ with gr.Row():
604
+ w_long = gr.Slider(0, 1, value=0.25, step=0.01, label="Max Dimension Weight (w_long)")
605
+ w_area = gr.Slider(0, 1, value=0.15, step=0.01, label="Area Weight (w_area)")
606
+ w_ar = gr.Slider(0, 1, value=0.15, step=0.01, label="Aspect Ratio Weight (w_ar)")
607
+ with gr.Row():
608
+ w_sv = gr.Slider(0, 1, value=0.10, step=0.01, label="Surface/Volume Weight (w_sv)")
609
+ w_sol = gr.Slider(0, 1, value=0.10, step=0.01, label="Solidity Weight (w_sol)")
610
+ w_feat = gr.Slider(0, 1, value=0.20, step=0.01, label="Feature Count Weight (w_feat)")
611
+ with gr.Row():
612
+ w_edns = gr.Slider(0, 1, value=0.05, step=0.01, label="Edge Density Weight (w_edns)")
613
+
614
+ gr.Markdown("**Class Thresholds:**")
615
+ with gr.Row():
616
+ th_easy = gr.Slider(0.0, 2.0, value=0.60, step=0.01, label="Easy Threshold (<)")
617
+ th_medium = gr.Slider(0.0, 2.5, value=1.20, step=0.01, label="Medium Threshold (< ; otherwise Difficult)")
618
+
619
+ btn = gr.Button("Analyze", variant="primary")
620
+
621
  with gr.Row():
622
+ complexity_out = gr.Textbox(label="Complexity", interactive=False)
623
+ card_out = gr.HTML(label="Results Card")
624
 
625
+ mesh_out = gr.Plot(
626
+ label="3D Preview (Plotly)" if PLOTLY_AVAILABLE else "3D Preview (disabled: Plotly not installed)",
627
+ height=520
628
+ )
629
+
630
+ df_out = gr.Dataframe(label="Complexity Parameters", row_count=1, wrap=True)
631
+ logs_out = gr.Textbox(label="Log", lines=12)
632
 
633
+ with gr.Row():
634
+ csv_out = gr.File(label="Download CSV", visible=True)
635
+ xlsx_out = gr.File(label="Download XLSX", visible=True)
636
 
637
+ btn.click(
638
+ analyze,
639
+ inputs=[file_in, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy, th_medium],
640
+ outputs=[complexity_out, card_out, mesh_out, df_out, logs_out, csv_out, xlsx_out]
641
+ )
 
642
 
643
+ return demo
 
 
644
 
 
 
 
 
 
645
 
646
+ # =============== Application Entry Point ===============
647
  if __name__ == "__main__":
648
+ # Create the interface
649
+ demo = create_interface()
650
+
651
+ # Launch configuration for HuggingFace Spaces
652
+ port = int(os.environ.get("PORT", 7860))
653
+
654
+ # Launch the app
655
+ demo.launch(
656
+ server_name="0.0.0.0",
657
+ server_port=port,
658
+ share=False, # Set to True for temporary public links
659
+ show_error=True,
660
+ debug=True
661
+ )