Engineer-Areeb commited on
Commit
116caab
·
verified ·
1 Parent(s): e9b96a2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +447 -0
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