TrentIndeed/meshToParametric
GitHub: TrentIndeed/meshToParametric
Paramesh AI 是一个将 3D 扫描三角网格逆向转换为完全参数化 CAD 模型的算法 pipeline,包含 AI 特征识别和交互式 Web 编辑器。
Stars: 1 | Forks: 0
# Paramesh AI
将棱柱形机械零件的三角网格文件(STL/OBJ/PLY/3MF)转换为完全参数化 CAD 模型的逆向工程 pipeline。具备 AI 驱动的特征识别、构建时间轴以及交互式 Web 查看器。
**上线于 [parameshai.com](https://parameshai.com)**
**目标零件:** 支架、底座、板件 —— 具有平面、圆柱孔、型腔、凸台、圆角和倒角的棱柱形组件。
## Pipeline
| 阶段 | 模块 | 描述 |
|-------|--------|-------------|
| 1 | `ingest.py` | 加载网格,修复拓扑,修复法线,移除退化面 |
| 2 | `segment.py` | 结合法线聚类和曲率分裂的区域生长分割 |
| 3 | `fit.py` | 针对平面/圆柱体/圆锥体/球体进行 RANSAC + 最小二乘法拟合 |
| 4 | `topology.py` | 曲面邻接图、二面角、边缘分类 |
| 5 | `recognize.py` | 特征识别:拉伸、孔、型腔、倒角、圆角 |
| 6a | `export_onshape.py` | FeatureScript 生成 + Onshape REST API(空跑或实时运行) |
| 6b | `export_cadquery.py` | CadQuery 实体:草图 → 拉伸 → 挖孔 → 导出 STEP |
## Web App
三栏式 CAD 查看器,内置 AI 助手,访问地址 [parameshai.com/app](https://parameshai.com/app)。
- **左侧**:特征树 —— 带类型的图标(草图、拉伸、孔、倒角)以及编辑器:编辑尺寸并选择边缘以添加圆角/倒角(悬停高亮 + 拖拽箭头,测量半径自动捕捉),重新导出编辑后的 STEP
- **中间**:具备轨道控制和 XYZ 网格的交互式 Three.js 3D 查看器
- **右侧**:AI 聊天机器人 —— 描述零件、建议编辑、分析尺寸
- **底部**:时间轴,每个构建步骤均带有 3D 渲染的缩略图
- **文件夹浏览器**:打开本地文件夹(无上传提示)以生成浏览器内缩略图(STL/OBJ/PLY/3MF),按路径/大小/mtime 进行缓存
- **上传流程**:全分辨率预览(+ 质量切换以 A/B 对比简化后的网格) → 复杂度指示器 → AI 思考过程 → CAD 模型
### 免费工具
- **网格清理器** ([parameshai.com/clean](https://parameshai.com/clean)) —— 修复法线、移除退化面、合并顶点、填补孔洞、调整大小。无需注册。
- **学习** ([parameshai.com/learn](https://parameshai.com/learn)) —— 20 篇关于网格到 CAD 转换的 SEO 指南
### 页面
| 路由 | 描述 |
|-------|-------------|
| `/` | 包含视频主视觉、定价、FAQ、候补名单的落地页 |
| `/app` | CAD 查看器工具(需要登录) |
| `/clean` | 免费网格清理器(需要登录) |
| `/home` | 登录后主页 —— 带有使用量进度条的工具选择器 |
| `/learn` | 20 个 SEO 指南页面 |
| `/changelog` | 最新动态 + 路线图 + Git 历史 |
| `/contact` | 支持表单(保存至数据库 + 发送邮件) |
| `/login` | 登录 / 注册 |
| `/privacy` | 隐私政策 |
| `/terms` | 服务条款 |
## 设置
```
pip install -e ".[dev]"
#### pip install cadquery shapely
For AI features:
```bash
npm install -g @anthropic-ai/claude-code
#### claude "hello" # 一次性认证
## 运行
```bash
# Web app(原生——无 alpha-wrap 修复;AI/scan 网格会退化)
python -m mesh2param.viz_server.app
# 打开 http://127.0.0.1:8080
# CLI
#### mesh2param convert input.stl --dry-run --output feature_tree.json
### Docker(推荐用于 AI/scan 网格)
pymeshlab's alpha-wrap watertight repair + the exact-IoU arbiter are **Linux-only**, so dev and AI work run in a container:
```bash
./start-server.sh # build + run the server image → http://127.0.0.1:8080
#### ./start-server.sh --no-build # 几秒钟内重启(--native 用于无 Docker 路径)
Ctrl+C (or closing the terminal) stops and removes the container; starting the script always (re)launches a fresh one. Source is bind-mounted (frontend edits live on refresh; Python edits → stop + restart the script). DB lives in a named volume.
## 测试
```bash
pytest # all tests
python run_visuals.py # visual reports
python run_visuals.py --stage 4 5 6 # specific stages
pytest tests/test_step_comparison.py -v # STEP comparison
#### pytest tests/test_all_meshes.py -v # 所有 5 个网格
### 模型对
#### 将一个包含网格和参考 STEP 的文件夹放入 `models/` 即可自动测试:
models/Mesh1/Body1Mesh.3mf + Body1Step.step
models/Mesh2/BottomMount.3mf
models/Mesh3/Front Plate.3mf
models/Mesh4/Bottom.3mf
#### models/Mesh5/Component3.3mf
## 架构
```
mesh2param/
pipeline/ — 6 个阶段模块(ingest → segment → fit → topology → recognize → export)
models/ — dataclass(CleanedMesh, FaceRegion, FittedSurface, Feature, FeatureTree)
viz/ — 每个阶段的 PyVista 3D + matplotlib 2D 可视化
viz_server/ — FastAPI Web 应用 + Three.js 查看器 + 任务队列
db/ — SQLite(User, MeshJob, JobStep, WaitlistEntry, SupportTicket)
api/ — REST 端点(auth, chat, settings)
viz_server/modal_runner.py — 可选的 Modal 调度(USE_MODAL) → 每个任务对应一个独立的云端容器
ai.py — Claude CLI 封装,具备注入防护 + 预算上限
config.py — 用于所有阶段的 Pydantic 设置
modal_app.py — Modal 应用:build_mesh = 完整 pipeline + 在独立容器中进行精确的 IoU 计算
scripts/ — 几何诊断(diag_render_sheet, diag_stage_sheet, diag_ai_build, diag_ai_parallel)
tests/ — pytest 测试套件(视觉、正确性、STEP 对比、全网格)
#### models/ — 网格 + 参考 STEP 对(+ AI1..AI15 测试零件)
### 云端并行(Modal)
一个 Modal 函数(`build_mesh`)是用于开发和生产环境的并行单元 —— 每次调用都在其独立的容器中运行整个 pipeline,从而绕过了 pymeshlab 无法并发运行的障碍:
```
modal run modal_app.py # all 15 AI test parts in parallel (~3 min)
#### modal run modal_app.py --parts AI9,AI12
Cloud jobs are at **full parity** with local: `build_mesh` calls the same `pipeline/convert.py:analyze_part` (ingest → decompose → per-body recognition → cross-body stitched fillets) the server runs, then the same `export_cadquery`, and streams steps + returns the editor bundle. One source of truth, so cloud == local geometry (full AI-set mean IoU 0.81).
For the public launch, `modal deploy modal_app.py` once and set `USE_MODAL=1` + Modal tokens on the server — the worker then spawns each customer job into its own container. Default off → unchanged local worker. See [docs/MODAL_DEPLOY.md](docs/MODAL_DEPLOY.md).
## 定价(基于 Token)
| Plan | Price | Tokens/mo | Upload Limit | Features |
|------|-------|-----------|-------------|----------|
| Free | $0 | 100 | 10 MB | Cleaner unlimited, 3-5 CAD conversions |
| Starter | $9/mo | 1,000 | 50 MB | STEP export, AI dimension planner |
| Pro | $29/mo | 5,000 | 200 MB | Onshape + AI chat + priority support |
| Unlimited | $79/mo | Unlimited | 500 MB | Everything |
Token cost: ~20 for a simple bracket, ~55 for a complex mount with chamfers.
## 安全
- Per-IP token tracking prevents free trial abuse (100 tokens + 10 jobs per IP)
- Complexity meter rejects meshes >10,000 faces or non-watertight >5,000 faces
- AI calls: system prefix, prompt sanitization, 8K cap, 20 calls/session, 5s rate limit
- Auth: PBKDF2-SHA256 salted password hashes, signed expiring tokens, per-IP login rate limiting
- Database: SQLite on Docker volume, survives rebuilds
## 部署
### 本地开发服务器
```bash
python -m mesh2param.viz_server.app
# 运行于 http://127.0.0.1:8080/app
```
### 部署到 VPS
通过 SSH 部署到运行 Docker + Caddy(端口 8081)的 Hetzner VPS,与 operatorDashboard 并存。
```
# 部署(VPS IP: YOUR_VPS_IP)
#### python deploy.py --ip YOUR_VPS_IP --domain parameshai.com
The deploy script will:
1. SSH into the VPS
2. Clone or pull the `main` branch to `/opt/meshToParametric`
3. Write `.env` with `AUTH_SECRET` and `DATABASE_URL`
4. Run `docker compose up -d --build` (container port 8080 → host port 8081)
5. Append Caddy reverse proxy config for the domain if not already present
**Note:** Container restart clears in-memory job data. User accounts persist in SQLite (`./data/mesh2param.db` volume).
### 同时执行两者
```bash
# 并行启动本地服务器并部署到 VPS
python -m mesh2param.viz_server.app &
#### python deploy.py --ip YOUR_VPS_IP --domain parameshai.com
## 关键库
trimesh, cadquery, pyvista, matplotlib, numpy, scipy, scikit-learn, networkx, fastapi, sqlalchemy, three.js
## 进展与工程笔记
This section tracks major changes to the core pipeline, problems hit, and how they were addressed. The pipeline works end-to-end on simple prismatic parts; the main ongoing work is **reliable segmentation on coarse, radial-fan triangulations** (common in 3MF exports from slicers).
### AI/scan 导入——在 voxel 之前进行 alpha-wrap,以保留圆孔
AI-generated (Meshy/Tripo) and scanned meshes arrive non-watertight, and `voxel_repair` (res 96) quantizes cylindrical bores into blocky circles that fail cylinder detection downstream. The ingest repair order is now **fill_holes → poisson → alpha-wrap (before voxel) → voxel_repair → face cap → late alpha-wrap**: alpha-wrap watertights the mesh while preserving round bores, so voxel_repair rarely fires. Validated AI12 IoU 0.58 → 0.77; full 15-part mean IoU 0.81. Alpha-wrap (pymeshlab) is **Linux-only** — runs in Docker/Modal, no-ops natively on Windows. Reliable exact (boolean) IoU is likewise container-only.
### Pipeline 重构——逐 body 分割 + 缝合 fillets
The original flow segmented the whole mesh, peeled its fillets, then decomposed into bodies. That made the per-body segmenter see an already-peeled mesh with no fillets to find, so fillet records for CAD export came only from the whole-mesh peel and had no symmetry between mirror features.
Restructured to: **ingest → decompose → per-body segment/fit/topology/identify_blends → stitched-fillets stage (groups per-body records into cross-body clusters via vertex KDTree proximity + axis-aligned continuation) → CAD apply (vertex-proximity edge matching with per-edge measured radius)**. The stitched stage replaced the earlier collinear-merge reconciliation step. The apply pass no longer trusts upstream record radii — it measures each CAD edge's radius from the same mesh vertex set the stitched group carries.
### Segmentation:axis-class + 平滑
The original flood-fill segmenter crept across rolled edges (plate → fillet → next plate all ended up in one chaotic region). Replaced `_region_growing` with **axis-class segmentation**: every face classifies into one of 7 classes (±X, ±Y, ±Z, curved) by principal-axis alignment within 5°. Mesh connectivity within each class gives regions. Prismatic parts have principal-axis-aligned faces as a hard constraint, so this cuts cleanly where the flood-fill failed.
Two smoothing passes run on the class assignment:
1. **Aligned → curved** — isolated axis-aligned faces (no same-class neighbors) get reassigned to curved. Catches pie-slice fan triangulation on cones where each slice lands in a different cardinal class.
2. **Curved → aligned** — curved faces whose majority of neighbors share one aligned class get pulled back to it. Catches plate triangles 5-10° off axis (tessellation noise) that initially landed in class 6 but belong to the plate.
Post-passes kept:
- `_merge_small_regions` with two-pass fallback: similar-normal neighbor first, then **longest-shared-boundary neighbor** if nothing matches within 75°. Absorbs long-thin sliver triangles that have no neighbor with matching normal.
- `_merge_adjacent_cylinder_fragments` stitches class-split cylinder quadrants back together, using the cumulative normal spread across the group to verify cylinder signature.
- `_smooth_region_boundaries` flips faces whose strict majority of neighbors belong to a different region.
### Blend 检测过滤器 (`peel_via_topology.py`)
The fillet-identification filter evolved through several generations. Original version used edge-tag transitions from topology — unreliable because the fitter sometimes classifies fillet arcs as SPHERE/CONE or merges through-holes into 360° cylinders, so topology tags adjacent edges CHAMFER or SHARP instead of FILLET.
Current filter walks the region adjacency graph and accepts a region as a fillet when:
- Non-plane (CYLINDER/SPHERE/CONE), small radius (≤ 8% bbox diag), reasonable face count (≤ 300).
- **For CYLINDER/CONE, at least one plane neighbor is perpendicular to the surface axis** — positive geometric signature of an edge blend. Hole bores, boss walls, and countersinks (axis parallel to plate normal) fail this and are correctly rejected.
- Falls back to using the cylinder axis as the edge line when adjacency gives only one plane neighbor or two parallel planes (rolled-over thin plates, rim fillets).
Chamfer detection requires a narrow plane region whose normal roughly bisects its two plane neighbors — rejects thin flat strips alongside rounded edges that have plate-matching normals.
### 遇到的问题及其解决方法
| Issue | Root cause | Fix |
|-------|-----------|-----|
| Symmetric features produced asymmetric regions | Source 3MF triangulation is asymmetric; flood-fill amplified that | Axis-class segmentation deterministic per face — symmetric geometry produces symmetric regions when the triangulation is |
| Remesh step silently failed | `pymeshfix.repair(verbose=False)` — unsupported kwarg in installed version | Dropped the kwarg; later confirmed remesh destroyed mesh geometry (-11% volume), so disabled in viz-server path |
| Isotropic remesh (pymeshlab) corrupted small features | Vertex smoothing + reprojection tolerance too wide | Kept the code gated behind `IngestConfig(remesh=True)` for future use; viz-server path uses `remesh=False` |
| Through-hole wall split into 4 cardinal blotches | Axis-class segmentation naturally splits a 360° cylinder along ±Y ±Z | `_merge_adjacent_cylinder_fragments` stitches fragments whose combined normal spread exceeds 0.2 |
| Pink triangular wedges poking into flat plate | Smoothing flipped flat triangles to curved whenever >50% of neighbors were curved | Tightened: only flip aligned→curved on **zero** same-class neighbors; added reverse smoothing for slightly-tilted plate triangles |
| Tab cylinders pie-sliced into 8 wedges | Radial-fan triangulation: each slice's normal in a different cardinal class | Iterative aligned→curved smoothing dissolves isolated slices; fragment-merger re-unifies |
| 139 segfaults after pipeline runs | Native-code crash (likely OCP/CadQuery on some mesh); no Python trace | Added `faulthandler.enable()` to server startup — next segfault will print a C-level stack trace |
| Fillet radii mismatched across body seams | Per-body RANSAC fits each edge independently | Apply stage measures radius per CAD edge from the stitched group's mesh vertex set; the upstream fit radius is no longer read |
### 侧板通孔边缘 fillets——六次 segmentation 细化
The original segmenter produced through-hole regions where the bore wall, both rim fillets, and the surrounding tangent strips on the plate face all collapsed into one or two giant labels. Visually that meant a single color blob around each hole instead of distinct highlights for each feature, with one plate looking different from the other depending on tessellation luck. Working through the failure modes one at a time produced this stack of passes in `mesh2param/pipeline/segment.py`:
1. **`_split_disconnected_components`** — `_split_flat_from_curved` peels flat faces out of a mixed region but leaves the remaining curved faces under one label even when they form mesh-disconnected components. Splits each label into one label per mesh-connected component so the bore wall and the rim fillet (separated by the plate in the mesh) don't share a label.
2. **`_unify_cylinder_features`** — finds cylinder-face candidates via 2-hop SVD rank-2 neighborhoods, DBSCAN-clusters them spatially, and unifies each cluster into one label. Uses a world-axis (X/Y/Z) bore-axis pick and filters to `|n·axis| < 0.25` so the bore wall is unified without absorbing the rim fillet's tilted-normal faces.
3. **`_split_disconnected_components` (second pass, `min_component_faces=50`)** — `_unify_cylinder_features`'s DBSCAN can bridge mesh-disconnected components if their centroids fall within `eps` of each other through the bore (front rim fillet ↔ back rim fillet on a thin plate). The post-unify split ensures front rim and back rim end up as distinct labels even when DBSCAN merged them.
4. **`_peel_axial_tangents_from_curves`** — for every region with mean-normal magnitude in `[0.3, 0.9]` (a tilted curved region, i.e. fillet arc), finds mesh-connected components of nearly-axial faces (`|n·axis| ≥ 0.985`) and peels them into their own label. Catches the very-first-row tangent strip of a fillet that otherwise visually merges with the adjacent plate. Tight 10° threshold so it doesn't eat mid-arc faces and split rolled-edge fillets along their length.
5. **`_peel_hole_rim_fillets`** — detects every through-hole by clustering radial-normal faces via DBSCAN per world axis, gated by `mean_normal_magnitude < 0.35` to reject partial arcs (rounded plate corners that are not actual through-holes). For each genuine hole, pulls hole-local rim faces (`|n·axis|` in `[0.3, 0.95]`) split by front-vs-back face side, but only when the dominant source label is plate-spanning (its bbox > 2× hole diameter). Eliminates the case where small through-holes' rim fillets sat inside the rolled-edge perimeter region.
6. **Cylinder-parent guard in `_split_flat_from_curved`** — checks the parent region's normals for rank-2 coplanar structure before peeling axial faces, so a 360° bore's cardinal-direction strips (where the arc crosses ±Y, ±Z) don't get yanked out as "flat" sub-regions and split the bore into 1 curved + 4 cardinal pieces.
End result for `models/Mesh5/RackMount.3mf`:
- Side plates: 17 / 15 regions each (was 1-2 chaotic regions for everything curved). Main through-hole has bore wall + front-rim + back-rim as three distinct labels.
- Bottom plate: 41 regions (was 52 before the round/bounded/mean-mag gates ruled out plate-corner false positives). Tabs each have 1 bore wall, 2 rims, and per-tab fillets.
- Rolled-edge fillets along plate perimeters stay unbroken along their length (the 10° tangent-peel threshold leaves mid-fillet arcs intact).
### 这项工作衍生出的另外三项基础设施修复
- **`scipy.optimize.least_squares` method `lm` → `trf`** in all three call sites (`_fit_sphere`, `_fit_cylinder`, `_fit_cone`). The C-level MINPACK backend behind `lm` segfaulted with a Windows access violation on degenerate inputs (narrow tangent-strip regions fitted as spheres during `_disambiguate_sphere_cylinder`) and was also not thread-safe. `trf` is pure-Python trust-region reflective and handles singular Jacobians gracefully. Added `np.isfinite` input guards as defense in depth.
- **`viz_server/job_queue.py`: parallel body processing → sequential.** The `ThreadPoolExecutor` running one worker per body let segmentation on body B race against fitting on body A through MKL/BLAS shared state, producing intermittent crashes even after the fit-stage lock. Sequential processing costs 1-2s on a 3-body part and eliminates the crash class.
- **`_color_regions_no_adjacent_match` greedy graph-coloring** for the segmentation viz. Replaced `palette[ri % len(palette)]` round-robin with an algorithm that visits regions largest-first, builds region-region adjacency from edge-shared faces, and assigns each region the first palette index not used by any already-colored mesh-adjacent neighbor. Guarantees no two adjacent regions render in the same color (non-adjacent regions can still share a color with a 12-color palette).
### 测试套件
Fillet-specific regression tests (`tests/test_fillet_detection.py`, `tests/test_export_fillet_consistency.py`) were removed when the upstream collinear-merge reconciliation pass was replaced by the stitched-fillets stage; the invariants they encoded (radius agreement across body seams, sphere-fit filtering at export time) no longer apply because the apply pass measures radii per edge from raw mesh vertices rather than reading any upstream fit. Future regression coverage for the new vertex-proximity matcher is open work.
Per-feature invariants in `tests/test_rackmount_segmentation.py` (20 tests, all passing) covering each body of `models/Mesh5/RackMount.3mf`:
- Side plates: two large flat regions, main through-hole bore wall in ONE region, rim fillets exist as labels distinct from the bore, no tiny axis-aligned fragments, rim fillets are hole-local (bbox ≲ 3× diameter), exactly one genuine 360° through-hole per plate.
- Bottom plate: total region count 10–60, large flat base face exists, all 4 tab through-hole bore walls each in ONE region, no excessive tiny fragments, tab flat-face fragmentation bounded.
- Cross-body: 3-body decomposition; side plates have matching region counts and matching curved-region counts (mirror-symmetry check).
### 快捷方式 / 未解决的问题
- **Side-plate long-edge fillets** on the RackMount test mesh aren't captured as regions — they're absorbed into the plate or the "curved" class by the segmenter. No good fix without either a cleaner input mesh or per-edge fillet fitting.
- **Tab cylinders** on bottom plate (Body 2 of RackMount) still produce ~30 regions due to radial-fan triangulation; the fragment merger only catches the obviously-cylinder groupings.
- **Curvature estimate** is noisy on coarse meshes; the curvature-drift guard I tried in region growing fragmented cylinders, so it's disabled. Re-enable with smoothed curvature if we want to handle rolled-edge detection more aggressively.
- **Triangle subdivision during ingest** was tried and reverted — even with `process=False` preserving coplanarity, subdivision exposed per-triangle normal noise in the source mesh that fragmented segmentation further.
- **Session stability:** occasional segfaults in the FastAPI worker after long sessions. Faulthandler armed for next crash; no stack trace yet.
```
标签:CAD转换, 三维建模, 云资产清单, 人工智能, 用户模式Hook绕过, 算法流水线, 请求拦截, 逆向工具, 逆向工程