Spaces:
Running
Running
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
|
| 3 |
+
import gradio as gr
|
| 4 |
+
import pyvista as pv
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
import matplotlib.cm as cm
|
| 7 |
+
import numpy as np
|
| 8 |
+
import trimesh
|
| 9 |
+
import plotly.graph_objects as go
|
| 10 |
+
from PIL import Image
|
| 11 |
+
import io
|
| 12 |
+
import re
|
| 13 |
+
import os
|
| 14 |
+
import tempfile
|
| 15 |
+
import warnings
|
| 16 |
+
from shapely.geometry import box, Point
|
| 17 |
+
from shapely.affinity import scale
|
| 18 |
+
|
| 19 |
+
# --- General Setup ---
|
| 20 |
+
warnings.filterwarnings('ignore')
|
| 21 |
+
plt.style.use('seaborn-v0_8-darkgrid')
|
| 22 |
+
# Ensure a temporary directory exists for file outputs
|
| 23 |
+
TEMP_DIR = "temp_outputs"
|
| 24 |
+
if not os.path.exists(TEMP_DIR):
|
| 25 |
+
os.makedirs(TEMP_DIR)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ###########################################################################
|
| 29 |
+
# ## 🛠️ TOOL 1: CAD to 2D Orthographic Views
|
| 30 |
+
# ###########################################################################
|
| 31 |
+
|
| 32 |
+
def generate_ortho_views(input_file):
|
| 33 |
+
"""
|
| 34 |
+
Takes an uploaded 3D model file and generates a single image
|
| 35 |
+
containing the front, top, and side orthographic views with dimensions.
|
| 36 |
+
"""
|
| 37 |
+
if input_file is None:
|
| 38 |
+
raise gr.Error("Please upload a 3D model file (e.g., .stl, .obj).")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
mesh = pv.read(input_file.name)
|
| 42 |
+
except Exception as e:
|
| 43 |
+
raise gr.Error(f"Error loading the 3D model: {e}")
|
| 44 |
+
|
| 45 |
+
# --- Generate Base Views with PyVista ---
|
| 46 |
+
views = ['xy', 'xz', 'yz']
|
| 47 |
+
temp_image_files = []
|
| 48 |
+
|
| 49 |
+
# Configure plotter for headless environment
|
| 50 |
+
plotter = pv.Plotter(off_screen=True, window_size=[1000, 1000])
|
| 51 |
+
plotter.background_color = 'white'
|
| 52 |
+
plotter.enable_parallel_projection()
|
| 53 |
+
|
| 54 |
+
outline_mesh = mesh.extract_feature_edges()
|
| 55 |
+
outline_kwargs = {'color': 'black', 'line_width': 2}
|
| 56 |
+
|
| 57 |
+
for view in views:
|
| 58 |
+
plotter.clear()
|
| 59 |
+
plotter.add_mesh(outline_mesh, **outline_kwargs)
|
| 60 |
+
plotter.camera_position = view
|
| 61 |
+
plotter.reset_camera()
|
| 62 |
+
plotter.camera.zoom(1.2)
|
| 63 |
+
|
| 64 |
+
# Save to a temporary file
|
| 65 |
+
temp_fd, temp_path = tempfile.mkstemp(suffix=".png", dir=TEMP_DIR)
|
| 66 |
+
os.close(temp_fd)
|
| 67 |
+
plotter.screenshot(temp_path)
|
| 68 |
+
temp_image_files.append(temp_path)
|
| 69 |
+
|
| 70 |
+
# --- Combine and Add Dimensions with Matplotlib ---
|
| 71 |
+
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
|
| 72 |
+
fig.patch.set_facecolor('white')
|
| 73 |
+
|
| 74 |
+
bounds = mesh.bounds
|
| 75 |
+
x_dim, y_dim, z_dim = (bounds[1] - bounds[0]), (bounds[3] - bounds[2]), (bounds[5] - bounds[4])
|
| 76 |
+
|
| 77 |
+
dims_data = [(x_dim, y_dim), (x_dim, z_dim), (y_dim, z_dim)]
|
| 78 |
+
view_titles = ["Front View (XY)", "Top View (XZ)", "Side View (YZ)"]
|
| 79 |
+
|
| 80 |
+
for i, ax in enumerate(axes):
|
| 81 |
+
img = plt.imread(temp_image_files[i])
|
| 82 |
+
ax.imshow(img)
|
| 83 |
+
ax.set_title(view_titles[i], fontsize=12, pad=10)
|
| 84 |
+
ax.axis('off')
|
| 85 |
+
|
| 86 |
+
img_w, img_h = img.shape[1], img.shape[0]
|
| 87 |
+
dim1, dim2 = dims_data[i]
|
| 88 |
+
|
| 89 |
+
# Horizontal Dimension
|
| 90 |
+
ax.annotate(f"{dim1:.2f}", xy=(img_w * 0.2, img_h * 0.9), xytext=(img_w * 0.8, img_h * 0.9),
|
| 91 |
+
arrowprops=dict(arrowstyle='<->', color='dimgray', lw=1.5),
|
| 92 |
+
va='bottom', ha='center', fontsize=10, color='black')
|
| 93 |
+
|
| 94 |
+
# Vertical Dimension
|
| 95 |
+
ax.annotate(f"{dim2:.2f}", xy=(img_w * 0.1, img_h * 0.2), xytext=(img_w * 0.1, img_h * 0.8),
|
| 96 |
+
arrowprops=dict(arrowstyle='<->', color='dimgray', lw=1.5),
|
| 97 |
+
rotation=90, va='center', ha='right', fontsize=10, color='black')
|
| 98 |
+
|
| 99 |
+
plt.tight_layout(rect=[0, 0, 1, 0.96])
|
| 100 |
+
|
| 101 |
+
# Save final combined image
|
| 102 |
+
final_fd, final_path = tempfile.mkstemp(suffix=".png", dir=TEMP_DIR)
|
| 103 |
+
os.close(final_fd)
|
| 104 |
+
plt.savefig(final_path, dpi=200, facecolor=fig.get_facecolor())
|
| 105 |
+
plt.close(fig)
|
| 106 |
+
|
| 107 |
+
# Clean up intermediate files
|
| 108 |
+
for temp_file in temp_image_files:
|
| 109 |
+
os.remove(temp_file)
|
| 110 |
+
|
| 111 |
+
return final_path
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# ###########################################################################
|
| 115 |
+
# ## 💨 TOOL 2: 2D CFD Simulator
|
| 116 |
+
# ###########################################################################
|
| 117 |
+
|
| 118 |
+
def run_cfd_simulation(Lx, Ly, Nx, Ny, u_in, rho, mu, ox1, oy1, ox2, oy2, max_iter, p_iter, tol, progress=gr.Progress()):
|
| 119 |
+
"""
|
| 120 |
+
Runs the 2D CFD simulation and returns the result plot and a summary.
|
| 121 |
+
"""
|
| 122 |
+
# --- Setup ---
|
| 123 |
+
dx, dy = Lx / (Nx - 1), Ly / (Ny - 1)
|
| 124 |
+
x, y = np.linspace(0, Lx, Nx), np.linspace(0, Ly, Ny)
|
| 125 |
+
X, Y = np.meshgrid(x, y)
|
| 126 |
+
dt = 0.001
|
| 127 |
+
nu = mu / rho
|
| 128 |
+
alpha_u, alpha_v, alpha_p = 0.4, 0.4, 0.1 # Relaxation factors
|
| 129 |
+
|
| 130 |
+
# --- Obstacle Mask ---
|
| 131 |
+
obstacle_mask = np.zeros((Ny, Nx), dtype=bool)
|
| 132 |
+
if not (ox1 == 0 and oy1 == 0 and ox2 == 0 and oy2 == 0):
|
| 133 |
+
i_ox1, j_oy1 = int(round(ox1 / dx)), int(round(oy1 / dy))
|
| 134 |
+
i_ox2, j_oy2 = int(round(ox2 / dx)), int(round(oy2 / dy))
|
| 135 |
+
obstacle_mask[j_oy1:j_oy2, i_ox1:i_ox2] = True
|
| 136 |
+
|
| 137 |
+
# --- Initialization ---
|
| 138 |
+
u, v, p = np.zeros((Ny, Nx)), np.zeros((Ny, Nx)), np.ones((Ny, Nx))
|
| 139 |
+
reynolds = u_in * Lx / nu
|
| 140 |
+
|
| 141 |
+
# --- Main Loop ---
|
| 142 |
+
progress(0, desc="Starting Simulation...")
|
| 143 |
+
for step in range(max_iter):
|
| 144 |
+
un, vn = u.copy(), v.copy()
|
| 145 |
+
|
| 146 |
+
# Boundary Conditions
|
| 147 |
+
u[:, 0], v[:, 0], p[:, 0] = u_in, 0.0, p[:, 1] # Inlet
|
| 148 |
+
u[:, -1], v[:, -1], p[:, -1] = u[:, -2], v[:, -2], 0.0 # Outlet
|
| 149 |
+
u[0, :], v[0, :], p[0, :] = 0.0, 0.0, p[1, :] # Bottom Wall
|
| 150 |
+
u[-1, :], v[-1, :], p[-1, :] = 0.0, 0.0, p[-2, :] # Top Wall
|
| 151 |
+
u[obstacle_mask], v[obstacle_mask] = 0.0, 0.0
|
| 152 |
+
|
| 153 |
+
# Pressure Source Term (b)
|
| 154 |
+
b = rho * (1/dt * ((u[1:-1, 2:] - u[1:-1, 0:-2]) / (2*dx) + (v[2:, 1:-1] - v[0:-2, 1:-1]) / (2*dy)))
|
| 155 |
+
|
| 156 |
+
# Pressure Poisson Solver
|
| 157 |
+
for _ in range(p_iter):
|
| 158 |
+
pn = p.copy()
|
| 159 |
+
p[1:-1, 1:-1] = alpha_p * (((pn[1:-1, 2:] + pn[1:-1, 0:-2]) * dy**2 + (pn[2:, 1:-1] + pn[0:-2, 1:-1]) * dx**2 - b * dx**2 * dy**2) / (2 * (dx**2 + dy**2))) + (1 - alpha_p) * p[1:-1, 1:-1]
|
| 160 |
+
p[obstacle_mask] = 0 # Dummy value inside obstacle
|
| 161 |
+
|
| 162 |
+
# Momentum Update
|
| 163 |
+
u[1:-1, 1:-1] = alpha_u * (un[1:-1, 1:-1] - un[1:-1, 1:-1] * dt / dx * (un[1:-1, 1:-1] - un[1:-1, 0:-2]) \
|
| 164 |
+
- vn[1:-1, 1:-1] * dt / dy * (un[1:-1, 1:-1] - un[0:-2, 1:-1]) \
|
| 165 |
+
- dt / (2 * rho * dx) * (p[1:-1, 2:] - p[1:-1, 0:-2]) \
|
| 166 |
+
+ nu * (dt / dx**2 * (un[1:-1, 2:] - 2 * un[1:-1, 1:-1] + un[1:-1, 0:-2]) \
|
| 167 |
+
+ dt / dy**2 * (un[2:, 1:-1] - 2 * un[1:-1, 1:-1] + un[0:-2, 1:-1]))) + (1 - alpha_u) * u[1:-1, 1:-1]
|
| 168 |
+
|
| 169 |
+
v[1:-1, 1:-1] = alpha_v * (vn[1:-1, 1:-1] - un[1:-1, 1:-1] * dt / dx * (vn[1:-1, 1:-1] - vn[1:-1, 0:-2]) \
|
| 170 |
+
- vn[1:-1, 1:-1] * dt / dy * (vn[1:-1, 1:-1] - vn[0:-2, 1:-1]) \
|
| 171 |
+
- dt / (2 * rho * dy) * (p[2:, 1:-1] - p[0:-2, 1:-1]) \
|
| 172 |
+
+ nu * (dt / dx**2 * (vn[1:-1, 2:] - 2 * vn[1:-1, 1:-1] + vn[1:-1, 0:-2]) \
|
| 173 |
+
+ dt / dy**2 * (vn[2:, 1:-1] - 2 * vn[1:-1, 1:-1] + vn[0:-2, 1:-1]))) + (1 - alpha_v) * v[1:-1, 1:-1]
|
| 174 |
+
|
| 175 |
+
diff = np.max(np.abs(u - un))
|
| 176 |
+
if diff < tol:
|
| 177 |
+
break
|
| 178 |
+
|
| 179 |
+
if step % 100 == 0:
|
| 180 |
+
progress(step / max_iter, desc=f"Iteration {step}, Diff: {diff:.2e}")
|
| 181 |
+
|
| 182 |
+
# --- Plotting ---
|
| 183 |
+
u[obstacle_mask], v[obstacle_mask], p[obstacle_mask] = np.nan, np.nan, np.nan
|
| 184 |
+
velocity_mag = np.sqrt(u**2 + v**2)
|
| 185 |
+
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
|
| 186 |
+
|
| 187 |
+
# Velocity Plot
|
| 188 |
+
im = axes[0].pcolormesh(X, Y, velocity_mag, cmap=cm.jet, shading='auto')
|
| 189 |
+
fig.colorbar(im, ax=axes[0], label='Velocity (m/s)')
|
| 190 |
+
axes[0].streamplot(X, Y, u, v, color='white', linewidth=0.8, density=1.5)
|
| 191 |
+
axes[0].set_title('Velocity Field and Streamlines')
|
| 192 |
+
axes[0].set_aspect('equal', adjustable='box')
|
| 193 |
+
|
| 194 |
+
# Pressure Plot
|
| 195 |
+
im = axes[1].pcolormesh(X, Y, p, cmap=cm.coolwarm, shading='auto')
|
| 196 |
+
fig.colorbar(im, ax=axes[1], label='Pressure (Pa)')
|
| 197 |
+
axes[1].set_title('Pressure Field')
|
| 198 |
+
axes[1].set_aspect('equal', adjustable='box')
|
| 199 |
+
|
| 200 |
+
plt.tight_layout()
|
| 201 |
+
|
| 202 |
+
summary = (f"**Simulation Summary**\n"
|
| 203 |
+
f"- Convergence reached at iteration: {step}\n"
|
| 204 |
+
f"- Reynolds Number: {reynolds:.2f}\n"
|
| 205 |
+
f"- Max Velocity: {np.nanmax(velocity_mag):.4f} m/s\n"
|
| 206 |
+
f"- Max Pressure: {np.nanmax(p):.4f} Pa")
|
| 207 |
+
|
| 208 |
+
return fig, summary
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ###########################################################################
|
| 212 |
+
# ## 📝 TOOL 3: Text to CAD Model
|
| 213 |
+
# ###########################################################################
|
| 214 |
+
|
| 215 |
+
class TextToCADGenerator:
|
| 216 |
+
def __init__(self):
|
| 217 |
+
self.shapes_library = {
|
| 218 |
+
'cube': self._create_cube, 'box': self._create_cube, 'plate': self._create_plate,
|
| 219 |
+
'sphere': self._create_sphere, 'ball': self._create_sphere,
|
| 220 |
+
'cylinder': self._create_cylinder, 'tube': self._create_cylinder, 'rod': self._create_rod, 'pipe': self._create_pipe,
|
| 221 |
+
'cone': self._create_cone, 'pyramid': self._create_pyramid,
|
| 222 |
+
'torus': self._create_torus, 'ring': self._create_torus, 'washer': self._create_washer, 'bearing': self._create_bearing,
|
| 223 |
+
'gear': self._create_gear, 'bracket': self._create_bracket,
|
| 224 |
+
'screw': self._create_screw, 'bolt': self._create_screw,
|
| 225 |
+
'nut': self._create_nut, 'flange': self._create_flange
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
def _extract_dimensions(self, prompt):
|
| 229 |
+
dims = {'length': 10, 'width': 10, 'height': 10, 'radius': 5, 'thickness': 2}
|
| 230 |
+
patterns = {
|
| 231 |
+
'length': r'length.*?(\d+\.?\d*)', 'width': r'width.*?(\d+\.?\d*)',
|
| 232 |
+
'height': r'height.*?(\d+\.?\d*)', 'radius': r'radius.*?(\d+\.?\d*)',
|
| 233 |
+
'diameter': r'diameter.*?(\d+\.?\d*)', 'thickness': r'thick.*?\s(\d+\.?\d*)'
|
| 234 |
+
}
|
| 235 |
+
for key, pattern in patterns.items():
|
| 236 |
+
match = re.search(pattern, prompt, re.IGNORECASE)
|
| 237 |
+
if match:
|
| 238 |
+
val = float(match.group(1))
|
| 239 |
+
if key == 'diameter': dims['radius'] = val / 2
|
| 240 |
+
else: dims[key] = val
|
| 241 |
+
return dims
|
| 242 |
+
|
| 243 |
+
def run(self, prompt):
|
| 244 |
+
prompt = prompt.lower()
|
| 245 |
+
params = self._extract_dimensions(prompt)
|
| 246 |
+
shape_type = 'cube'
|
| 247 |
+
for shape_key in self.shapes_library.keys():
|
| 248 |
+
if shape_key in prompt:
|
| 249 |
+
shape_type = shape_key
|
| 250 |
+
break
|
| 251 |
+
|
| 252 |
+
mesh = self.shapes_library.get(shape_type, self._create_cube)(params)
|
| 253 |
+
drawing = self._generate_2d_drawing(mesh, shape_type.title(), params)
|
| 254 |
+
plotly_fig = self._generate_3d_viz(mesh)
|
| 255 |
+
|
| 256 |
+
# Save mesh to a temporary STL file
|
| 257 |
+
fd, stl_path = tempfile.mkstemp(suffix=".stl", dir=TEMP_DIR)
|
| 258 |
+
os.close(fd)
|
| 259 |
+
mesh.export(stl_path)
|
| 260 |
+
|
| 261 |
+
return drawing, plotly_fig, stl_path
|
| 262 |
+
|
| 263 |
+
def _generate_2d_drawing(self, mesh, title, params):
|
| 264 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
| 265 |
+
fig.suptitle(f"2D Drawing: {title}", fontsize=16)
|
| 266 |
+
bounds = mesh.bounds
|
| 267 |
+
dims = {'width': bounds[1]-bounds[0], 'depth': bounds[3]-bounds[2], 'height': bounds[5]-bounds[4]}
|
| 268 |
+
|
| 269 |
+
views = ['Front', 'Top', 'Side']
|
| 270 |
+
extents = [
|
| 271 |
+
(dims['width'], dims['height']),
|
| 272 |
+
(dims['width'], dims['depth']),
|
| 273 |
+
(dims['depth'], dims['height'])
|
| 274 |
+
]
|
| 275 |
+
|
| 276 |
+
for i, ax in enumerate(axes):
|
| 277 |
+
ax.set_title(f"{views[i]} View")
|
| 278 |
+
rect = plt.Rectangle((-extents[i][0]/2, -extents[i][1]/2), extents[i][0], extents[i][1], fill=False, edgecolor='black', linewidth=2)
|
| 279 |
+
ax.add_patch(rect)
|
| 280 |
+
ax.set_aspect('equal', 'box')
|
| 281 |
+
ax.set_xlim(-extents[i][0], extents[i][0])
|
| 282 |
+
ax.set_ylim(-extents[i][1], extents[i][1])
|
| 283 |
+
ax.grid(True, linestyle='--', alpha=0.6)
|
| 284 |
+
ax.text(0, -extents[i][1]*0.8, f"W: {extents[i][0]:.2f}", ha='center')
|
| 285 |
+
ax.text(-extents[i][0]*0.8, 0, f"H: {extents[i][1]:.2f}", va='center', rotation=90)
|
| 286 |
+
|
| 287 |
+
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
| 288 |
+
return fig
|
| 289 |
+
|
| 290 |
+
def _generate_3d_viz(self, mesh):
|
| 291 |
+
fig = go.Figure(data=[go.Mesh3d(
|
| 292 |
+
x=mesh.vertices[:, 0], y=mesh.vertices[:, 1], z=mesh.vertices[:, 2],
|
| 293 |
+
i=mesh.faces[:, 0], j=mesh.faces[:, 1], k=mesh.faces[:, 2],
|
| 294 |
+
color='lightblue', opacity=0.9
|
| 295 |
+
)])
|
| 296 |
+
fig.update_layout(title="Interactive 3D Model", scene=dict(aspectmode='data'))
|
| 297 |
+
return fig
|
| 298 |
+
|
| 299 |
+
# --- Shape Creation Methods ---
|
| 300 |
+
def _create_cube(self, d): return trimesh.creation.box(extents=[d['length'], d['width'], d['height']])
|
| 301 |
+
def _create_plate(self, d): return trimesh.creation.box(extents=[d['length'], d['width'], d['thickness']])
|
| 302 |
+
def _create_sphere(self, d): return trimesh.creation.icosphere(radius=d['radius'])
|
| 303 |
+
def _create_cylinder(self, d): return trimesh.creation.cylinder(radius=d['radius'], height=d.get('height', d.get('length')))
|
| 304 |
+
def _create_rod(self, d): return trimesh.creation.cylinder(radius=d.get('radius', 1), height=d.get('length', 50))
|
| 305 |
+
def _create_cone(self, d): return trimesh.creation.cone(radius=d['radius'], height=d['height'])
|
| 306 |
+
def _create_pyramid(self, d):
|
| 307 |
+
size = d.get('width', 10)
|
| 308 |
+
vertices = np.array([[0,0,d['height']], [-size/2,-size/2,0], [size/2,-size/2,0], [size/2,size/2,0], [-size/2,size/2,0]])
|
| 309 |
+
faces = np.array([[0,1,2], [0,2,3], [0,3,4], [0,4,1], [1,4,3], [1,3,2]])
|
| 310 |
+
return trimesh.Trimesh(vertices=vertices, faces=faces)
|
| 311 |
+
def _create_torus(self, d): return trimesh.creation.torus(major_radius=d['radius'], minor_radius=d['thickness'])
|
| 312 |
+
def _create_washer(self, d):
|
| 313 |
+
outer = trimesh.creation.cylinder(radius=d['radius'], height=d['thickness'])
|
| 314 |
+
inner = trimesh.creation.cylinder(radius=d['radius']*0.6, height=d['thickness']*1.2)
|
| 315 |
+
return outer.difference(inner)
|
| 316 |
+
def _create_bearing(self, d): return self._create_washer(d)
|
| 317 |
+
def _create_pipe(self, d):
|
| 318 |
+
outer = trimesh.creation.cylinder(radius=d['radius'], height=d.get('length', 100))
|
| 319 |
+
inner = trimesh.creation.cylinder(radius=d['radius']-d['thickness'], height=d.get('length', 100)*1.2)
|
| 320 |
+
return outer.difference(inner)
|
| 321 |
+
def _create_gear(self, d): return trimesh.creation.gear(tooth_number=d.get('teeth', 12), radial_pitch=d['radius'], tooth_width=d['thickness'])
|
| 322 |
+
def _create_bracket(self, d):
|
| 323 |
+
base = trimesh.creation.box(extents=[d['length'], d['width'], d['thickness']])
|
| 324 |
+
wall = trimesh.creation.box(extents=[d['thickness'], d['width'], d['height']])
|
| 325 |
+
wall.apply_translation([d['length']/2 - d['thickness']/2, 0, d['height']/2 - d['thickness']/2])
|
| 326 |
+
return base + wall
|
| 327 |
+
def _create_screw(self, d):
|
| 328 |
+
shaft = trimesh.creation.cylinder(radius=d['radius'], height=d['length'])
|
| 329 |
+
head = trimesh.creation.cylinder(radius=d['radius']*2, height=d['thickness'])
|
| 330 |
+
head.apply_translation([0, 0, d['length']/2])
|
| 331 |
+
return shaft + head
|
| 332 |
+
def _create_nut(self, d):
|
| 333 |
+
hex_prism = trimesh.creation.cylinder(radius=d['radius'], height=d['height'], sections=6)
|
| 334 |
+
hole = trimesh.creation.cylinder(radius=d['radius']*0.5, height=d['height']*1.2)
|
| 335 |
+
return hex_prism.difference(hole)
|
| 336 |
+
def _create_flange(self, d):
|
| 337 |
+
outer = trimesh.creation.cylinder(radius=d['radius'], height=d['height'])
|
| 338 |
+
inner = trimesh.creation.cylinder(radius=d['radius']*0.4, height=d['height']*1.2)
|
| 339 |
+
return outer.difference(inner)
|
| 340 |
+
|
| 341 |
+
text_to_cad_generator = TextToCADGenerator()
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# ###########################################################################
|
| 345 |
+
# ## 🔡 TOOL 4: Text to G-Code
|
| 346 |
+
# ###########################################################################
|
| 347 |
+
|
| 348 |
+
def parse_gcode_description(description):
|
| 349 |
+
"""Parses text to extract parameters for G-code generation."""
|
| 350 |
+
parsed = {"width": 100, "height": 50, "holes": [], "slots": []}
|
| 351 |
+
|
| 352 |
+
size_match = re.search(r'(\d+)\s*[xX]\s*(\d+)', description)
|
| 353 |
+
if size_match:
|
| 354 |
+
parsed["width"] = float(size_match.group(1))
|
| 355 |
+
parsed["height"] = float(size_match.group(2))
|
| 356 |
+
|
| 357 |
+
for hole_match in re.finditer(r'(\d+)\s*mm\s+hole\s+at\s+\((\d+),\s*(\d+)\)', description):
|
| 358 |
+
parsed["holes"].append({'d': float(hole_match.group(1)), 'x': float(hole_match.group(2)), 'y': float(hole_match.group(3))})
|
| 359 |
+
|
| 360 |
+
return parsed
|
| 361 |
+
|
| 362 |
+
def generate_text_to_gcode(description):
|
| 363 |
+
"""Generates a 3-view drawing, G-code, and an SVG from text."""
|
| 364 |
+
params = parse_gcode_description(description)
|
| 365 |
+
w, h = params['width'], params['height']
|
| 366 |
+
|
| 367 |
+
# --- Generate Drawing ---
|
| 368 |
+
fig, ax = plt.subplots(figsize=(8, 5))
|
| 369 |
+
ax.set_aspect('equal')
|
| 370 |
+
shape = box(0, 0, w, h)
|
| 371 |
+
x, y = shape.exterior.xy
|
| 372 |
+
ax.plot(x, y, color='black', label='Outline')
|
| 373 |
+
for hole in params['holes']:
|
| 374 |
+
r = hole['d'] / 2
|
| 375 |
+
circle = Point(hole['x'], hole['y']).buffer(r)
|
| 376 |
+
hx, hy = circle.exterior.xy
|
| 377 |
+
ax.plot(hx, hy, color='blue', label=f'Hole D={hole["d"]}')
|
| 378 |
+
ax.set_xlim(-10, w + 10)
|
| 379 |
+
ax.set_ylim(-10, h + 10)
|
| 380 |
+
ax.grid(True)
|
| 381 |
+
ax.set_title("2D Part Drawing")
|
| 382 |
+
|
| 383 |
+
# Save drawing
|
| 384 |
+
fd, drawing_path = tempfile.mkstemp(suffix=".png", dir=TEMP_DIR)
|
| 385 |
+
os.close(fd)
|
| 386 |
+
fig.savefig(drawing_path)
|
| 387 |
+
plt.close(fig)
|
| 388 |
+
|
| 389 |
+
# Save SVG
|
| 390 |
+
fd, svg_path = tempfile.mkstemp(suffix=".svg", dir=TEMP_DIR)
|
| 391 |
+
os.close(fd)
|
| 392 |
+
fig.savefig(svg_path)
|
| 393 |
+
plt.close(fig)
|
| 394 |
+
|
| 395 |
+
# --- Generate G-Code ---
|
| 396 |
+
gcode = ["G21 ; Units: mm", "G90 ; Absolute Positioning", "G0 Z5 ; Lift Z"]
|
| 397 |
+
gcode.append("; Cut outline")
|
| 398 |
+
gcode.extend([f"G0 X0 Y0", "G1 Z-1 F100", f"G1 X{w} F500", f"G1 Y{h}", "G1 X0", "G1 Y0", "G0 Z5"])
|
| 399 |
+
for hole in params['holes']:
|
| 400 |
+
x, y, r = hole['x'], hole['y'], hole['d'] / 2
|
| 401 |
+
gcode.append(f"; Drill hole at ({x}, {y})")
|
| 402 |
+
gcode.extend([f"G0 X{x - r} Y{y}", "G1 Z-1 F100", f"G2 I{r} J0 F300 ; Circular interpolation", "G0 Z5"])
|
| 403 |
+
gcode.append("M30 ; End Program")
|
| 404 |
+
|
| 405 |
+
return drawing_path, "\n".join(gcode), svg_path
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# ###########################################################################
|
| 409 |
+
# ## 🖼️ GRADIO INTERFACE
|
| 410 |
+
# ###########################################################################
|
| 411 |
+
|
| 412 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="CAD-Wizard") as demo:
|
| 413 |
+
gr.Markdown("# 🧙♂️ CAD-Wizard: Your All-in-One CAD Assistant")
|
| 414 |
+
gr.Markdown("Select a tool from the tabs below to get started.")
|
| 415 |
+
|
| 416 |
+
with gr.Tabs():
|
| 417 |
+
# --- TAB 1: CAD to 2D ---
|
| 418 |
+
with gr.TabItem("CAD to 2D Views", id=0):
|
| 419 |
+
with gr.Row():
|
| 420 |
+
with gr.Column(scale=1):
|
| 421 |
+
cad_file_input = gr.File(label="Upload 3D Model (.stl, .obj, .ply)")
|
| 422 |
+
cad_2d_button = gr.Button("Generate 2D Views", variant="primary")
|
| 423 |
+
with gr.Column(scale=2):
|
| 424 |
+
cad_2d_output = gr.Image(label="Orthographic Views with Dimensions")
|
| 425 |
+
gr.Examples(
|
| 426 |
+
[["examples/bracket.stl"], ["examples/gear.stl"]],
|
| 427 |
+
inputs=cad_file_input,
|
| 428 |
+
outputs=cad_2d_output,
|
| 429 |
+
fn=generate_ortho_views,
|
| 430 |
+
cache_examples=True
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
# --- TAB 2: 2D CFD Simulator ---
|
| 434 |
+
with gr.TabItem("2D CFD Simulator", id=1):
|
| 435 |
+
with gr.Row():
|
| 436 |
+
with gr.Column(scale=1):
|
| 437 |
+
gr.Markdown("### Fluid & Domain Properties")
|
| 438 |
+
cfd_u_in = gr.Slider(0.001, 0.1, value=0.01, step=0.001, label="Inlet Velocity (m/s)")
|
| 439 |
+
cfd_rho = gr.Number(value=1.0, label="Density (kg/m³)")
|
| 440 |
+
cfd_mu = gr.Number(value=0.02, label="Viscosity (Pa.s)")
|
| 441 |
+
cfd_Lx = gr.Slider(1.0, 5.0, value=2.0, label="Channel Length (m)")
|
| 442 |
+
cfd_Ly = gr.Slider(0.5, 2.0, value=1.0, label="Channel Height (m)")
|
| 443 |
+
|
| 444 |
+
gr.Markdown("### Obstacle (set all to 0 for none)")
|
| 445 |
+
cfd_ox1 = gr.Slider(0.0, 5.0, value=0.5, label="Obstacle X1")
|
| 446 |
+
cfd_oy1 = gr.Slider(0.0, 2.0, value=0.4, label="Obstacle Y1")
|
| 447 |
+
cfd_ox2 = gr.Slider(0.0, 5.0, value=0
|