Spaces:
Paused
Paused
| import os, io, zipfile, math, traceback, datetime, tempfile | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| # Plotly optional (if not installed, app still starts) | |
| try: | |
| import plotly.graph_objects as go | |
| PLOTLY_AVAILABLE = True | |
| except Exception: | |
| go = None | |
| PLOTLY_AVAILABLE = False | |
| import gradio as gr | |
| # ------------ OpenCascade (pythonocc-core) ------------ | |
| try: | |
| from OCC.Core.IGESControl import IGESControl_Reader | |
| from OCC.Core.STEPControl import STEPControl_Reader, STEPControl_AsIs | |
| from OCC.Core.IFSelect import IFSelect_ReturnStatus | |
| # ProgressRange might not be available on some builds | |
| try: | |
| from OCC.Core.Message import Message_ProgressRange | |
| HAS_PROGRESS = True | |
| except Exception: | |
| Message_ProgressRange = None | |
| HAS_PROGRESS = False | |
| from OCC.Core.TopoDS import TopoDS_Shape | |
| from OCC.Core.Bnd import Bnd_Box | |
| from OCC.Core.BRepBndLib import brepbndlib | |
| from OCC.Core.GProp import GProp_GProps | |
| from OCC.Core.BRepGProp import brepgprop | |
| from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Sewing | |
| from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh | |
| from OCC.Core.BRep import BRep_Tool | |
| from OCC.Extend.TopologyUtils import TopologyExplorer | |
| from OCC.Core.TopLoc import TopLoc_Location | |
| from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE | |
| from OCC.Core.StlAPI import StlAPI_Writer | |
| OCC_AVAILABLE = True | |
| except ImportError as e: | |
| print(f"OpenCascade not available: {e}") | |
| OCC_AVAILABLE = False | |
| # Define dummy classes to prevent errors | |
| class TopoDS_Shape: | |
| def IsNull(self): return True | |
| class TopAbs_FACE: pass | |
| class TopAbs_EDGE: pass | |
| # =============== Helpers geometrici =============== | |
| def _bbox(shape): | |
| """Calculate bounding box of shape""" | |
| if not OCC_AVAILABLE: | |
| return (0, 0, 0, 100, 100, 100) | |
| bbox = Bnd_Box() | |
| brepbndlib.Add(shape, bbox) | |
| return bbox.Get() # xmin, ymin, zmin, xmax, ymax, zmax | |
| def _area(shape): | |
| """Calculate surface area of shape""" | |
| if not OCC_AVAILABLE: | |
| return 1000.0 | |
| gp = GProp_GProps() | |
| brepgprop.SurfaceProperties(shape, gp) | |
| return gp.Mass() | |
| def _volume(shape): | |
| """Calculate volume of shape""" | |
| if not OCC_AVAILABLE: | |
| return 100.0 | |
| gp = GProp_GProps() | |
| brepgprop.VolumeProperties(shape, gp) | |
| return gp.Mass() | |
| def _try_sew(shape, tol=1e-3): | |
| """Try to sew shape faces together""" | |
| if not OCC_AVAILABLE: | |
| return shape | |
| sew = BRepBuilderAPI_Sewing(tol, True, True, True, False) | |
| sew.Add(shape) | |
| sew.Perform() | |
| return sew.SewedShape() | |
| def _count_sub(shape, kind): | |
| """Count faces or edges""" | |
| if not OCC_AVAILABLE: | |
| return 50 if kind == TopAbs_FACE else 200 | |
| cnt = 0 | |
| topo = TopologyExplorer(shape) | |
| if kind == TopAbs_FACE: | |
| for _ in topo.faces(): | |
| cnt += 1 | |
| elif kind == TopAbs_EDGE: | |
| for _ in topo.edges(): | |
| cnt += 1 | |
| return cnt | |
| def _fmt(x): | |
| """Format numbers for display""" | |
| if x is None: | |
| return "—" | |
| try: | |
| if isinstance(x, (int,)) or (isinstance(x, float) and abs(x) >= 1000): | |
| return f"{x:,.0f}".replace(",", " ") | |
| if isinstance(x, float): | |
| return f"{x:.4f}" | |
| except Exception: | |
| pass | |
| return str(x) | |
| # =============== File handling =============== | |
| def _save_uploaded_to_tmp(uploaded_file): | |
| """Save Gradio upload to temp file and return path""" | |
| if uploaded_file is None: | |
| return None | |
| # Handle different Gradio file object types | |
| if hasattr(uploaded_file, 'name'): | |
| file_name = Path(uploaded_file.name).name | |
| file_path = uploaded_file.name | |
| else: | |
| file_name = "uploaded_file" | |
| file_path = uploaded_file | |
| # Copy to temp directory with proper name | |
| dst = Path(tempfile.gettempdir()) / file_name | |
| try: | |
| if hasattr(uploaded_file, 'name') and os.path.exists(uploaded_file.name): | |
| # File is already saved by Gradio | |
| import shutil | |
| shutil.copy2(uploaded_file.name, dst) | |
| else: | |
| # Read file content | |
| if hasattr(uploaded_file, 'read'): | |
| data = uploaded_file.read() | |
| else: | |
| with open(uploaded_file, 'rb') as f: | |
| data = f.read() | |
| with open(dst, "wb") as f: | |
| f.write(data) | |
| except Exception as e: | |
| print(f"Error saving file: {e}") | |
| return None | |
| return str(dst) | |
| def _maybe_unzip_and_pick(path_in_tmp): | |
| """Extract ZIP and find CAD file""" | |
| p = Path(path_in_tmp) | |
| if p.suffix.lower() != ".zip": | |
| return str(p) | |
| out_dir = Path(tempfile.gettempdir()) / "cad_zip" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| # Clean up existing files | |
| for c in out_dir.iterdir(): | |
| try: | |
| if c.is_file(): | |
| c.unlink() | |
| except Exception: | |
| pass | |
| try: | |
| with zipfile.ZipFile(str(p), "r") as zf: | |
| zf.extractall(str(out_dir)) | |
| except Exception as e: | |
| print(f"Error extracting ZIP: {e}") | |
| return None | |
| # Look for CAD files | |
| for ext in (".stp", ".step", ".igs", ".iges", ".STP", ".STEP", ".IGS", ".IGES"): | |
| cand = list(out_dir.rglob(f"*{ext}")) | |
| if cand: | |
| return str(cand[0]) | |
| return None | |
| def _read_step_with_fallback(tmp_path): | |
| """Read STEP file with fallback for different API versions""" | |
| if not OCC_AVAILABLE: | |
| return None | |
| sreader = STEPControl_Reader() | |
| status = sreader.ReadFile(tmp_path) | |
| if int(status) != int(IFSelect_ReturnStatus.IFSelect_RetDone): | |
| return None | |
| # Try new API first (with ProgressRange) | |
| try: | |
| nroots = sreader.NbRootsForTransfer() | |
| for i in range(1, nroots + 1): | |
| if HAS_PROGRESS: | |
| sreader.TransferRoot(i, STEPControl_AsIs, Message_ProgressRange()) | |
| else: | |
| raise TypeError("force fallback") | |
| except Exception: | |
| # Fallback to classic API | |
| try: | |
| nroots = sreader.NbRootsForTransfer() | |
| for i in range(1, nroots + 1): | |
| sreader.TransferRoot(i, STEPControl_AsIs) | |
| except Exception: | |
| sreader.TransferRoots() | |
| shape = sreader.OneShape() | |
| return None if shape.IsNull() else shape | |
| def _read_shape_any(tmp_path): | |
| """Read any supported CAD file format""" | |
| if not OCC_AVAILABLE: | |
| return None | |
| ext = Path(tmp_path).suffix.lower() | |
| if ext in [".igs", ".iges"]: | |
| rdr = IGESControl_Reader() | |
| st = rdr.ReadFile(tmp_path) | |
| if int(st) != int(IFSelect_ReturnStatus.IFSelect_RetDone): | |
| return None | |
| rdr.TransferRoots() | |
| shp = rdr.OneShape() | |
| if shp.IsNull(): | |
| rdr.TransferAll() | |
| shp = rdr.OneShape() | |
| return None if shp.IsNull() else shp | |
| if ext in [".stp", ".step"]: | |
| return _read_step_with_fallback(tmp_path) | |
| return None | |
| # =============== 3D Visualization =============== | |
| def shape_to_plotly_mesh(shape, deflection=0.8): | |
| """Convert CAD shape to Plotly 3D mesh""" | |
| if not PLOTLY_AVAILABLE or not OCC_AVAILABLE: | |
| return None | |
| # Triangulate the shape | |
| BRepMesh_IncrementalMesh(shape, deflection, True, 0.5, True) | |
| vertices = [] | |
| faces_i, faces_j, faces_k = [], [], [] | |
| vmap = {} | |
| def add_vertex(p): | |
| key = (round(p.X(), 6), round(p.Y(), 6), round(p.Z(), 6)) | |
| idx = vmap.get(key) | |
| if idx is None: | |
| idx = len(vertices) | |
| vertices.append([key[0], key[1], key[2]]) | |
| vmap[key] = idx | |
| return idx | |
| topo = TopologyExplorer(shape) | |
| for face in topo.faces(): | |
| loc = TopLoc_Location() | |
| tri = BRep_Tool.Triangulation(face, loc) | |
| if tri is None: | |
| continue | |
| trsf = loc.Transformation() | |
| # Handle different API versions | |
| has_nodes_attr = hasattr(tri, "Nodes") and callable(getattr(tri, "Nodes")) | |
| has_tris_attr = hasattr(tri, "Triangles") and callable(getattr(tri, "Triangles")) | |
| if has_nodes_attr and has_tris_attr: | |
| nodes = tri.Nodes() | |
| tris = tri.Triangles() | |
| for t in range(1, tri.NbTriangles() + 1): | |
| n1, n2, n3 = tris.Value(t).Get() | |
| p1 = nodes.Value(n1).Transformed(trsf) | |
| p2 = nodes.Value(n2).Transformed(trsf) | |
| p3 = nodes.Value(n3).Transformed(trsf) | |
| i1 = add_vertex(p1) | |
| i2 = add_vertex(p2) | |
| i3 = add_vertex(p3) | |
| faces_i.append(i1) | |
| faces_j.append(i2) | |
| faces_k.append(i3) | |
| else: | |
| # Alternative API | |
| for t in range(1, tri.NbTriangles() + 1): | |
| tri_t = tri.Triangle(t) | |
| n1, n2, n3 = tri_t.Get() | |
| p1 = tri.Node(n1).Transformed(trsf) | |
| p2 = tri.Node(n2).Transformed(trsf) | |
| p3 = tri.Node(n3).Transformed(trsf) | |
| i1 = add_vertex(p1) | |
| i2 = add_vertex(p2) | |
| i3 = add_vertex(p3) | |
| faces_i.append(i1) | |
| faces_j.append(i2) | |
| faces_k.append(i3) | |
| if len(vertices) == 0 or len(faces_i) == 0: | |
| return None # Empty triangulation | |
| verts = np.array(vertices, dtype=float) | |
| fig = go.Figure(data=[ | |
| go.Mesh3d( | |
| x=verts[:, 0], y=verts[:, 1], z=verts[:, 2], | |
| i=faces_i, j=faces_j, k=faces_k, | |
| flatshading=True, opacity=1.0, showscale=False | |
| ) | |
| ]) | |
| fig.update_layout( | |
| scene_aspectmode="data", | |
| margin=dict(l=0, r=0, t=30, b=0), | |
| title="3D Preview" | |
| ) | |
| fig.update_scenes( | |
| xaxis_visible=False, | |
| yaxis_visible=False, | |
| zaxis_visible=False | |
| ) | |
| return fig | |
| # =============== Complexity Scoring =============== | |
| def score_complessita(dimx, dimy, dimz, area, volume, | |
| n_faces=None, n_edges=None, | |
| aspect_ratio=None, sv_ratio=None, | |
| solidity=None, edge_density=None, | |
| weights=None): | |
| """Calculate complexity score based on various geometric parameters""" | |
| # Default weights | |
| w = { | |
| "w_long": 0.25, "w_area": 0.15, "w_ar": 0.15, | |
| "w_sv": 0.10, "w_sol": 0.10, "w_feat": 0.20, "w_edns": 0.05 | |
| } | |
| if weights: | |
| w.update(weights) | |
| longest = max(dimx, dimy, dimz) | |
| s_long = min(longest / 600.0, 2.0) | |
| s_area = min(area / 1.5e6, 2.0) | |
| s_ar = min((aspect_ratio or 1.0) / 3.0, 2.0) | |
| s_sv = min((sv_ratio or 0.0) / 0.02, 2.0) | |
| s_sol = 2.0 - min((solidity or 1.0), 2.0) | |
| s_feat = min(((n_faces or 0) / 200.0) + ((n_edges or 0) / 1000.0), 2.0) | |
| s_edns = min((edge_density or 0.0) / 0.005, 2.0) | |
| score = (w["w_long"] * s_long + w["w_area"] * s_area + w["w_ar"] * s_ar + | |
| w["w_sv"] * s_sv + w["w_sol"] * s_sol + w["w_feat"] * s_feat + | |
| w["w_edns"] * s_edns) | |
| return float(score) | |
| def classe_da_score(score, th_easy=0.6, th_medium=1.2): | |
| """Classify complexity based on score""" | |
| if score < th_easy: | |
| return "facile" | |
| if score < th_medium: | |
| return "medio" | |
| return "difficile" | |
| # =============== HTML Card Rendering =============== | |
| def render_card_html(file_name, volume, area, dimx, dimy, dimz, classe=None, score=None): | |
| """Render results as HTML card""" | |
| def f(x): | |
| return _fmt(x) | |
| badge = f"{classe.upper()} — score {score:.2f}" if (classe and score is not None) else "—" | |
| color = {"facile": "#22c55e", "medio": "#f59e0b", "difficile": "#ef4444"}.get((classe or ""), "#0ea5e9") | |
| html = f""" | |
| <div style="font-family: ui-sans-serif,system-ui; max-width: 980px; border-radius:16px; padding:20px 24px; | |
| background: linear-gradient(135deg,#0f172a 0%,#0b132b 45%,#1b2a49 100%); color:#e5e7eb; | |
| box-shadow:0 10px 30px rgba(0,0,0,.35);"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> | |
| <div style="font-size:18px;opacity:.9;">CAD Analysis (IGES/STEP)</div> | |
| <div style="font-size:12px;opacity:.6;">Units as in model</div> | |
| </div> | |
| <div style="font-size:22px;font-weight:700;margin-bottom:16px;">{file_name}</div> | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;"> | |
| <div style="background:#111827;border:1px solid #1f2937;border-radius:14px;padding:16px;"> | |
| <div style="font-size:12px;color:#9ca3af;">Volume</div> | |
| <div style="font-size:22px;font-weight:700;margin-top:4px;">{f(volume)} <span style="font-size:12px;opacity:.7;">u³</span></div> | |
| </div> | |
| <div style="background:#111827;border:1px solid #1f2937;border-radius:14px;padding:16px;"> | |
| <div style="font-size:12px;color:#9ca3af;">Area</div> | |
| <div style="font-size:22px;font-weight:700;margin-top:4px;">{f(area)} <span style="font-size:12px;opacity:.7;">u²</span></div> | |
| </div> | |
| <div style="background:#111827;border:1px solid #1f2937;border-radius:14px;padding:16px;"> | |
| <div style="font-size:12px;color:#9ca3af;">Dims (X·Y·Z)</div> | |
| <div style="font-size:18px;font-weight:700;margin-top:4px;">{f(dimx)} × {f(dimy)} × {f(dimz)}</div> | |
| </div> | |
| <div style="background:#0b1220;border:1px solid #1f2937;border-radius:14px;padding:16px;"> | |
| <div style="font-size:12px;color:#9ca3af;">Complexity</div> | |
| <div style="font-size:18px;font-weight:800;margin-top:4px;color:{color};">{badge}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return html | |
| # =============== Main Analysis Pipeline =============== | |
| def analyze(file_obj, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy, th_medium): | |
| """Main analysis function""" | |
| logs = [] | |
| def log(x): | |
| ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| line = f"[{ts}] {x}" | |
| logs.append(line) | |
| print(line) # Also print to console for debugging | |
| try: | |
| if not OCC_AVAILABLE: | |
| log("OpenCascade (pythonocc-core) not available - demo mode") | |
| # Return demo data | |
| demo_df = pd.DataFrame([{ | |
| "File": "demo_model.stp", | |
| "Volume (u^3)": 1000.0, | |
| "Area (u^2)": 2000.0, | |
| "Dim X": 100.0, "Dim Y": 50.0, "Dim Z": 20.0, | |
| "BBox xmin": 0.0, "BBox ymin": 0.0, "BBox zmin": 0.0, | |
| "BBox xmax": 100.0, "BBox ymax": 50.0, "BBox zmax": 20.0, | |
| "# Facce": 50, "# Spigoli": 200, | |
| "Aspect ratio": 5.0, | |
| "Surface/Volume": 2.0, | |
| "Solidity (Vol/BBoxVol)": 1.0, | |
| "Densità spigoli (edges/area)": 0.1, | |
| "Score complessità": 0.8, | |
| "Classe complessità": "medio" | |
| }]) | |
| card = render_card_html("demo_model.stp", 1000, 2000, 100, 50, 20, "medio", 0.8) | |
| return ("medio (score 0.80)", card, None, demo_df, | |
| "\n".join(logs) + "\nDemo mode - OpenCascade not available", None, None) | |
| if file_obj is None: | |
| return ("Select a file", None, None, pd.DataFrame(), | |
| "\n".join(logs), None, None) | |
| # 1) Save temp file | |
| tmp_path = _save_uploaded_to_tmp(file_obj) | |
| if tmp_path is None: | |
| log("Failed to save uploaded file") | |
| return ("File save error", None, None, pd.DataFrame(), | |
| "\n".join(logs), None, None) | |
| log(f"Upload saved to: {tmp_path}") | |
| # 2) Handle ZIP files | |
| picked = _maybe_unzip_and_pick(tmp_path) | |
| if picked is None: | |
| log("ZIP without supported CAD files (.stp/.igs)") | |
| return ("ZIP without STEP/IGES", None, None, pd.DataFrame(), | |
| "\n".join(logs), None, None) | |
| if picked != tmp_path: | |
| log(f"ZIP extracted → selected: {picked}") | |
| else: | |
| log(f"File selected: {picked}") | |
| # 3) Read CAD file | |
| shape = _read_shape_any(picked) | |
| if (shape is None) or shape.IsNull(): | |
| log("CAD reading failed (null shape)") | |
| return ("CAD reading error", None, None, pd.DataFrame(), | |
| "\n".join(logs), None, None) | |
| # 4) Calculate metrics | |
| xmin, ymin, zmin, xmax, ymax, zmax = _bbox(shape) | |
| dimx, dimy, dimz = (xmax - xmin), (ymax - ymin), (zmax - zmin) | |
| area = _area(shape) | |
| log(f"Area: {area}") | |
| volume = _volume(shape) | |
| log(f"Volume: {volume}") | |
| # Try sewing if volume is zero | |
| if volume == 0.0: | |
| log("Volume=0 → trying sewing...") | |
| sewn = _try_sew(shape, tol=1e-3) | |
| if not sewn.IsNull(): | |
| v2 = _volume(sewn) | |
| log(f"Volume after sewing: {v2}") | |
| if v2 > 0: | |
| shape, volume = sewn, v2 | |
| n_faces = _count_sub(shape, TopAbs_FACE) | |
| log(f"#Faces: {n_faces}") | |
| n_edges = _count_sub(shape, TopAbs_EDGE) | |
| log(f"#Edges: {n_edges}") | |
| bbox_vol = (dimx * dimy * dimz) if all(v > 0 for v in (dimx, dimy, dimz)) else 0.0 | |
| sv_ratio = (area / volume) if volume and volume > 0 else None | |
| solidity = (volume / bbox_vol) if bbox_vol and volume and volume > 0 else None | |
| aspect_ratio = max(dimx, dimy, dimz) / max(1e-9, min(dimx, dimy, dimz)) | |
| edge_density = (n_edges / area) if area and area > 0 else None | |
| # 5) Calculate complexity score and class | |
| weights = dict( | |
| w_long=w_long, w_area=w_area, w_ar=w_ar, | |
| w_sv=w_sv, w_sol=w_sol, w_feat=w_feat, w_edns=w_edns | |
| ) | |
| score = score_complessita(dimx, dimy, dimz, area, volume, | |
| n_faces, n_edges, aspect_ratio, sv_ratio, | |
| solidity, edge_density, weights=weights) | |
| classe = classe_da_score(score, th_easy=th_easy, th_medium=th_medium) | |
| log(f"Score: {score:.3f} | Class: {classe}") | |
| # 6) Create DataFrame | |
| df = pd.DataFrame([{ | |
| "File": Path(picked).name, | |
| "Volume (u^3)": float(volume), | |
| "Area (u^2)": float(area), | |
| "Dim X": float(dimx), "Dim Y": float(dimy), "Dim Z": float(dimz), | |
| "BBox xmin": float(xmin), "BBox ymin": float(ymin), "BBox zmin": float(zmin), | |
| "BBox xmax": float(xmax), "BBox ymax": float(ymax), "BBox zmax": float(zmax), | |
| "# Facce": int(n_faces), "# Spigoli": int(n_edges), | |
| "Aspect ratio": float(aspect_ratio), | |
| "Surface/Volume": float(sv_ratio) if sv_ratio is not None else None, | |
| "Solidity (Vol/BBoxVol)": float(solidity) if solidity is not None else None, | |
| "Densità spigoli (edges/area)": float(edge_density) if edge_density is not None else None, | |
| "Score complessità": float(score), | |
| "Classe complessità": classe | |
| }]) | |
| # 7) Export CSV/XLSX (downloadable files) | |
| tmpdir = Path(tempfile.gettempdir()) | |
| csv_path = tmpdir / "cad_report.csv" | |
| df.to_csv(csv_path, index=False) | |
| out_csv = str(csv_path) | |
| xlsx_path = tmpdir / "cad_report.xlsx" | |
| out_xlsx = None | |
| try: | |
| df.to_excel(xlsx_path, index=False) | |
| out_xlsx = str(xlsx_path) | |
| log(f"Export: {csv_path}, {xlsx_path}") | |
| except Exception as e: | |
| log(f"Excel not created (openpyxl missing or error): {e}") | |
| # 8) Generate 3D visualization | |
| fig = shape_to_plotly_mesh(shape, deflection=0.8) | |
| if fig is None and PLOTLY_AVAILABLE: | |
| # Empty triangulation → try STL for debug | |
| try: | |
| stl_path = tmpdir / "mesh_fallback.stl" | |
| StlAPI_Writer().Write(shape, str(stl_path)) | |
| log(f"Empty triangulation → STL saved: {stl_path}") | |
| except Exception as ee: | |
| log(f"STL fallback failed: {ee}") | |
| # 9) Generate HTML card | |
| card = render_card_html(Path(picked).name, volume, area, dimx, dimy, dimz, classe, score) | |
| # Return results | |
| return ( | |
| f"{classe} (score {score:.2f})", | |
| card, | |
| fig if PLOTLY_AVAILABLE else None, | |
| df, | |
| "\n".join(logs), | |
| out_csv, | |
| out_xlsx | |
| ) | |
| except Exception: | |
| logs.append("EXCEPTION:\n" + traceback.format_exc()) | |
| return ("Analysis error", None, None, pd.DataFrame(), | |
| "\n".join(logs), None, None) | |
| # =============== Gradio Interface =============== | |
| def create_interface(): | |
| """Create and configure Gradio interface""" | |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo: | |
| gr.Markdown("# CAD Complexity Analyzer (IGES/STEP)") | |
| if not OCC_AVAILABLE: | |
| gr.Markdown(""" | |
| ⚠️ **Demo Mode**: OpenCascade (pythonocc-core) is not available. | |
| The interface will show demo data instead of actual CAD analysis. | |
| To enable full functionality, install: `pip install pythonocc-core` | |
| """) | |
| gr.Markdown(""" | |
| Upload a **.stp/.step/.igs/.iges** file (or a **.zip** containing one), | |
| adjust parameters (optional), and click **Analyze**. | |
| """) | |
| with gr.Row(): | |
| file_in = gr.File( | |
| label="CAD File (STEP/IGES or ZIP)", | |
| file_types=[".stp", ".step", ".igs", ".iges", ".zip"] | |
| ) | |
| with gr.Accordion("Complexity Parameters (recommended defaults)", open=False): | |
| gr.Markdown("**Weights (sum ≈ 1):**") | |
| with gr.Row(): | |
| w_long = gr.Slider(0, 1, value=0.25, step=0.01, label="Max Dimension Weight (w_long)") | |
| w_area = gr.Slider(0, 1, value=0.15, step=0.01, label="Area Weight (w_area)") | |
| w_ar = gr.Slider(0, 1, value=0.15, step=0.01, label="Aspect Ratio Weight (w_ar)") | |
| with gr.Row(): | |
| w_sv = gr.Slider(0, 1, value=0.10, step=0.01, label="Surface/Volume Weight (w_sv)") | |
| w_sol = gr.Slider(0, 1, value=0.10, step=0.01, label="Solidity Weight (w_sol)") | |
| w_feat = gr.Slider(0, 1, value=0.20, step=0.01, label="Feature Count Weight (w_feat)") | |
| with gr.Row(): | |
| w_edns = gr.Slider(0, 1, value=0.05, step=0.01, label="Edge Density Weight (w_edns)") | |
| gr.Markdown("**Class Thresholds:**") | |
| with gr.Row(): | |
| th_easy = gr.Slider(0.0, 2.0, value=0.60, step=0.01, label="Easy Threshold (<)") | |
| th_medium = gr.Slider(0.0, 2.5, value=1.20, step=0.01, label="Medium Threshold (< ; otherwise Difficult)") | |
| btn = gr.Button("Analyze", variant="primary") | |
| with gr.Row(): | |
| complexity_out = gr.Textbox(label="Complexity", interactive=False) | |
| card_out = gr.HTML(label="Results Card") | |
| mesh_out = gr.Plot( | |
| label="3D Preview (Plotly)" if PLOTLY_AVAILABLE else "3D Preview (disabled: Plotly not installed)", | |
| height=520 | |
| ) | |
| df_out = gr.Dataframe(label="Complexity Parameters", row_count=1, wrap=True) | |
| logs_out = gr.Textbox(label="Log", lines=12) | |
| with gr.Row(): | |
| csv_out = gr.File(label="Download CSV", visible=True) | |
| xlsx_out = gr.File(label="Download XLSX", visible=True) | |
| btn.click( | |
| analyze, | |
| inputs=[file_in, w_long, w_area, w_ar, w_sv, w_sol, w_feat, w_edns, th_easy, th_medium], | |
| outputs=[complexity_out, card_out, mesh_out, df_out, logs_out, csv_out, xlsx_out] | |
| ) | |
| return demo | |
| # =============== Application Entry Point =============== | |
| if __name__ == "__main__": | |
| # Create the interface | |
| demo = create_interface() | |
| # Launch configuration for HuggingFace Spaces | |
| port = int(os.environ.get("PORT", 7860)) | |
| # Launch the app | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=port, | |
| share=False, # Set to True for temporary public links | |
| show_error=True, | |
| debug=True | |
| ) | |