Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38eb6d92d9 | ||
|
|
0093e92b26 | ||
|
|
9b40d4c36d | ||
|
|
c657417d6b |
+68
-50
@@ -12,12 +12,15 @@ STEP(`.stp` / `.step`)檔案做確認與量測,不需要安裝 SolidWorks
|
|||||||
核心價值:
|
核心價值:
|
||||||
|
|
||||||
- **多檔匯入**:一次載入多個 CAD 檔(STEP / STL / DXF),每個檔案自成一棵樹
|
- **多檔匯入**:一次載入多個 CAD 檔(STEP / STL / DXF),每個檔案自成一棵樹
|
||||||
- **裝配樹**:STEP product structure 還原成樹狀節點(組件→零件),逐節點 顯示/隱藏、換色、Zoom-to
|
- **裝配樹**:STEP product structure 還原成樹狀節點(組件→零件),逐節點 顯示/隱藏、換色、Zoom-to;
|
||||||
|
名稱搜尋過濾、隔離顯示(只顯示此節點/反轉/全顯)、tooltip 外形尺寸
|
||||||
- **量測**:點座標、兩點距離、邊長、面(面積/類型/法向量)、圓(心/半徑/直徑/周長)、
|
- **量測**:點座標、兩點距離、邊長、面(面積/類型/法向量)、圓(心/半徑/直徑/周長)、
|
||||||
兩面/兩邊夾角、面到面最短距離;單位 mm ⇄ inch 即時切換
|
兩面/兩邊夾角、面到面最短距離、體積/質心;吸附含**圓心**(量孔對孔 pitch);
|
||||||
- **剖面**:X/Y/Z 軸向剖切,位置滑桿 + 反向,CPU 網格裁切(量測不受影響)
|
快速鍵 P/D/E/F/C/A/M + Esc;單位 mm ⇄ inch 即時切換
|
||||||
- **匯出**:量測結果 CSV、3D 視圖 PNG 截圖(2x)
|
- **剖面**:X/Y/Z 軸向 + **3點任意平面**,位置滑桿/數值輸入 + 反向,CPU 網格裁切(量測不受影響)
|
||||||
- 單機離線執行,無網路相依
|
- **視圖**:標準視圖(等角/前/上/右)+ 正交⇄透視投影
|
||||||
|
- **匯出**:量測結果 CSV、3D 視圖 PNG 截圖(2x)、**目前對齊位置寫成新 STEP 檔**
|
||||||
|
- 單機離線執行,無網路相依;視窗/單位/最近檔案自動保存(settings.json)
|
||||||
|
|
||||||
使用者:單一桌面使用者(無登入 / 角色系統)。
|
使用者:單一桌面使用者(無登入 / 角色系統)。
|
||||||
|
|
||||||
@@ -99,16 +102,17 @@ STPViewer/
|
|||||||
│
|
│
|
||||||
├── Models/
|
├── Models/
|
||||||
│ ├── FaceInfo.cs # GeometryModel3D ↔ B-rep Face 對照(STL 為 null)
|
│ ├── FaceInfo.cs # GeometryModel3D ↔ B-rep Face 對照(STL 為 null)
|
||||||
│ ├── MeasureMode.cs # enum: None/Point/Distance/Edge/Face/Circle/Angle/FaceDistance/Align/Align3/Drag/Interference
|
│ ├── MeasureMode.cs # enum: None/Point/Distance/Edge/Face/Circle/Angle/FaceDistance/Align/Align3/Drag/Interference/Volume
|
||||||
│ ├── MeasurementResult.cs # 量測結果(雙單位 lambda、3D 標籤同步)
|
│ ├── MeasurementResult.cs # 量測結果(雙單位 lambda、3D 標籤同步)
|
||||||
│ └── UnitSystem.cs # mm/inch + Units 格式化
|
│ └── UnitSystem.cs # mm/inch + Units 格式化
|
||||||
│
|
│
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
||||||
│ ├── MeasurementService.cs # 點/線/面/圓/角度/面距 幾何計算
|
│ ├── MeasurementService.cs # 點/線/面/圓/角度/面距/體積質心 幾何計算(吸附含圓心)
|
||||||
│ ├── InterferenceService.cs # 干涉檢查:三角形-三角形相交(區間法)+均勻網格加速;無干涉時近似最小間隙
|
│ ├── InterferenceService.cs # 干涉檢查:三角形-三角形相交(區間法)+均勻網格加速;無干涉時近似最小間隙
|
||||||
│ ├── RigidAlign.cs # 三點對齊/旋轉的剛體變換數學(Matrix3D列向量 ↔ ModOp行向量 轉換)
|
│ ├── RigidAlign.cs # 三點對齊/旋轉的剛體變換數學(Matrix3D列向量 ↔ ModOp行向量 轉換)
|
||||||
│ └── SectionService.cs # 剖面:網格/線段半空間裁切
|
│ ├── SectionService.cs # 剖面:網格/線段半空間裁切
|
||||||
|
│ └── SettingsService.cs # 使用者設定 settings.json(視窗/單位/MRU;%LOCALAPPDATA%\STPViewer)
|
||||||
│
|
│
|
||||||
└── ViewModels/ # MainViewModel 為 partial class,依職責分檔
|
└── ViewModels/ # MainViewModel 為 partial class,依職責分檔
|
||||||
├── MainViewModel.cs # 核心:匯入、裝配樹、量測、剛體變換、邊線、單位
|
├── MainViewModel.cs # 核心:匯入、裝配樹、量測、剛體變換、邊線、單位
|
||||||
@@ -127,29 +131,30 @@ STPViewer/
|
|||||||
桌面程式無資料庫;核心為記憶體內模型:
|
桌面程式無資料庫;核心為記憶體內模型:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 一個匯入檔 = 一個圖層
|
// 裝配樹節點:root = 匯入檔、group = STEP 裝配、leaf = 零件幾何(可見性/顏色向下 cascade)
|
||||||
class LayerItemViewModel
|
class ModelNodeViewModel
|
||||||
{
|
{
|
||||||
string Name; // 檔名(不含路徑)
|
string Name; // root = 檔名;group/leaf = STEP product 名
|
||||||
string FilePath;
|
string? FilePath; // root 才有值
|
||||||
bool IsVisible; // 切換 viewport 中的 ModelVisual3D
|
bool IsVisible; // 切換 viewport 中的 ModelVisual3D(cascade)
|
||||||
Color Color; // 圖層色(換色重建材質)
|
bool IsFilterVisible; // 樹搜尋過濾結果(只影響樹面板顯示)
|
||||||
|
Color Color; // 節點色(換色只改共用 Brush)
|
||||||
int SolidCount, FaceCount, TriangleCount;
|
int SolidCount, FaceCount, TriangleCount;
|
||||||
ModelVisual3D BodyVisual; // 面網格
|
ModelVisual3D BodyVisual; // leaf:面網格容器(合併/逐面兩種內容切換)
|
||||||
ModelVisual3D EdgeVisual; // 輪廓線
|
LinesVisual3D EdgeVisual; // root:整檔合併輪廓線
|
||||||
Rect3D Bounds; // Zoom-to 用
|
Rect3D Bounds; // Zoom-to / 外形尺寸 / 旋轉中心
|
||||||
}
|
}
|
||||||
|
|
||||||
// HitTest 反查:渲染物件 → B-rep
|
// HitTest 反查:渲染物件 → B-rep
|
||||||
class FaceInfo
|
class FaceInfo
|
||||||
{
|
{
|
||||||
object Face; // CADability Face(量測用 B-rep)
|
Face? BrepFace; // CADability Face(量測用 B-rep;STL 為 null)
|
||||||
LayerItemViewModel Owner;
|
ModelNodeViewModel Owner;
|
||||||
MeshGeometry3D Mesh; // 面積近似 / 頂點吸附
|
MeshGeometry3D Mesh; // 面積近似 / 頂點吸附
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MeasureMode { None, Point, Distance, Edge, Face, Circle,
|
enum MeasureMode { None, Point, Distance, Edge, Face, Circle,
|
||||||
Angle, FaceDistance, Align, Align3, Drag, Interference }
|
Angle, FaceDistance, Align, Align3, Drag, Interference, Volume }
|
||||||
|
|
||||||
class MeasurementResult
|
class MeasurementResult
|
||||||
{
|
{
|
||||||
@@ -182,11 +187,15 @@ class MeasurementResult
|
|||||||
| 旋轉 | 樹面板選檔案 + 工具列 ↻X/↻Y/↻Z | 繞檔案中心 +90°(連按累加;方向不合時先轉正再對齊) |
|
| 旋轉 | 樹面板選檔案 + 工具列 ↻X/↻Y/↻Z | 繞檔案中心 +90°(連按累加;方向不合時先轉正再對齊) |
|
||||||
| 拖曳 | 模式「🖐 拖曳」(手形游標)+ 左鍵按住零件拖 | 沿螢幕平面移動該檔案,放開烘進 B-rep(拖曳中僅暫時 Transform,不卡) |
|
| 拖曳 | 模式「🖐 拖曳」(手形游標)+ 左鍵按住零件拖 | 沿螢幕平面移動該檔案,放開烘進 B-rep(拖曳中僅暫時 Transform,不卡) |
|
||||||
| 操作器 | 「⊹ 操作器」+ 樹面板選檔案 | XYZ 箭頭沿軸移動、旋轉環繞軸轉任意角度(與視角無關);每次放開烘進 B-rep |
|
| 操作器 | 「⊹ 操作器」+ 樹面板選檔案 | XYZ 箭頭沿軸移動、旋轉環繞軸轉任意角度(與視角無關);每次放開烘進 B-rep |
|
||||||
| 干涉 | 工具列「🧩 干涉」(需剛好 2 個可見檔案) | 相交→紅色交線+相交三角形對數;無相交→最小間隙 gap(≈0 即配合 match) |
|
| 干涉 | 工具列「🧩 干涉」(≥2 個可見檔案,兩兩配對檢查) | 每組配對:相交→紅色交線+相交三角形對數;無相交→最小間隙 gap(≈0 即配合 match) |
|
||||||
| 剖面 | 工具列「✂ 剖面」+ 軸向/位置/反向 | CPU 裁切渲染網格(原始幾何保留,量測仍精確) |
|
| 剖面 | 工具列「✂ 剖面」+ 軸向(X/Y/Z/**3點任意平面**)/位置(滑桿+數值輸入)/反向 | CPU 裁切渲染網格(原始幾何保留,量測仍精確);3點=在模型上點 3 點定義平面 |
|
||||||
| 單位 | 工具列 mm ⇄ in | 既有量測(清單+3D 標籤)即時換算 |
|
| 單位 | 工具列 mm ⇄ in(隨設定保存) | 既有量測(清單+3D 標籤)即時換算 |
|
||||||
| 匯出 | 💾 CSV / 📷 截圖 | UTF-8 BOM CSV;2x PNG |
|
| 匯出 | 💾 CSV / 📷 截圖 / 📤 STEP | UTF-8 BOM CSV;2x PNG;可見檔案目前位置寫成**新** STEP 檔(對齊結果交接 CAD) |
|
||||||
| 視圖 | 滑鼠右鍵旋轉/滾輪縮放/中鍵平移(Helix 預設)、ViewCube | |
|
| 視圖 | 滑鼠右鍵旋轉/滾輪縮放/中鍵平移(Helix 預設)、ViewCube、工具列 等角/前/上/右、正交⇄透視 | |
|
||||||
|
| 快速鍵 | P 點 / D 距離 / E 邊 / F 面 / C 圓 / A 角度 / M 面距;同鍵再按=退出;Esc=取消進行中量測→退出模式 | |
|
||||||
|
| 樹操作 | 搜尋框過濾節點名稱;右鍵:只顯示此節點 / 反轉顯示 / 全部顯示 / 量體積質心 | tooltip 含外形尺寸 L×W×H |
|
||||||
|
| 體積/質心 | 樹節點右鍵「⚖ 量體積/質心」 | signed volume(封閉實體)+ 質心標記 + AABB 外形尺寸 |
|
||||||
|
| 設定/MRU | 自動記住視窗位置大小、單位;「▾」最近 10 個檔案 | %LOCALAPPDATA%\STPViewer\settings.json |
|
||||||
|
|
||||||
支援格式:`.stp` / `.step`(B-rep + 裝配樹)、`.stl`(純網格,僅點/距離/角度/面距量測)、
|
支援格式:`.stp` / `.step`(B-rep + 裝配樹)、`.stl`(純網格,僅點/距離/角度/面距量測)、
|
||||||
`.dxf`(線架構檢視)。**IGES 不支援**(CADability 無 IGES reader)。
|
`.dxf`(線架構檢視)。**IGES 不支援**(CADability 無 IGES reader)。
|
||||||
@@ -393,35 +402,44 @@ WPF retained-mode 每幀重走 visual tree → frame rate 崩。瀏覽模式因
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Development Phases — 第六輪(Future Extensions 實作 v0.5.0,全部完成)
|
||||||
|
|
||||||
|
### Phase 14 — 功能擴充(工作量:L)
|
||||||
|
|
||||||
|
**量測/操作:**
|
||||||
|
- [x] 圓心吸附 — `Snap` 點到圓形邊(容差內)吸附圓心,與頂點比誰離命中點近;距離模式可直接量兩孔 pitch
|
||||||
|
- [x] Esc 取消 — 先清進行中的多段量測(保留模式)→ 退回瀏覽 → 關操作器;量測快速鍵 P/D/E/F/C/A/M
|
||||||
|
(同鍵再按=退出;焦點在輸入框時不攔截)
|
||||||
|
- [x] 體積/質心 — `MeasurementService.MeshVolume` signed volume + 加權質心;有向體積抵銷比例過高
|
||||||
|
(開放殼)判為不可靠並拒算;樹右鍵「⚖ 量體積/質心」→ 量測結果 + 質心標記
|
||||||
|
|
||||||
|
**視圖/UI:**
|
||||||
|
- [x] 標準視圖(等角/前/上/右,Z 向上機構慣例)+ 正交⇄透視(`HelixViewport3D.Orthographic`);
|
||||||
|
gizmo 疊圖層相機「型別」跟隨主相機切換(透視/正交投影一致,操作器不錯位)
|
||||||
|
- [x] GridSplitter ×2 — 樹面板/量測面板可調寬(MinWidth 防拖到消失)
|
||||||
|
- [x] 節點外形尺寸 — `Bounds` setter 通知 `Stats`/`ToolTipText`,tooltip 顯示 L×W×H(變換後自動更新)
|
||||||
|
- [x] 樹搜尋/過濾 — 名稱過濾(符合者+祖先+子樹顯示、自動展開);只影響樹面板,不影響 3D;新匯入檔套用現行過濾
|
||||||
|
- [x] 隔離顯示 — 樹右鍵:只顯示此節點/反轉顯示/全部顯示;`SetVisibleRecursive` 顯式遞迴
|
||||||
|
(不能只設 root:setter 同值不觸發 cascade,會留下混合狀態)
|
||||||
|
- [x] 記住設定 + MRU — `SettingsService`(settings.json):視窗位置大小(含虛擬桌面範圍檢查)、
|
||||||
|
mm/inch、最近 10 檔;工具列「▾」下拉
|
||||||
|
|
||||||
|
**檔案/剖面:**
|
||||||
|
- [x] 干涉 ≥2 可見檔 — 兩兩配對逐組檢查(網格先快照),每組一筆結果,狀態列總結相交組數
|
||||||
|
- [x] 匯出 STEP — `CADability.ExportStep.WriteToFile` + `Project.CreateSimpleProject`;可見檔案的
|
||||||
|
Solid/Shell(目前對齊位置)寫成新檔;SmokeTest `--export-test` 往返驗證(8 實體出→回讀 8 實體一致)
|
||||||
|
- [x] 3點任意剖面 — 軸向下拉加「3點」:模型上點 3 點定義平面(共線防呆、Esc 取消、換軸重置);
|
||||||
|
平面位置改為「場景 AABB 8 角投影到法向」內插(軸向=舊行為、任意法向也成立);位置加數值輸入框(0–100 防呆)
|
||||||
|
|
||||||
|
**驗收:** ClipTest / AlignTest / InterferenceTest / ExportTest 全過;test.stp 匯入正常;建置 0 警告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Future Extensions(下一輪)
|
## Future Extensions(下一輪)
|
||||||
|
|
||||||
依價值/成本排序(v0.4.0 盤點):
|
|
||||||
|
|
||||||
### 低成本高價值(建議下一輪優先)
|
|
||||||
|
|
||||||
- **圓心吸附** — 距離模式點到圓邊時吸附到圓心 → 直接量兩孔 pitch(心到心距離),
|
|
||||||
連接器 pin pitch 確認的殺手級量測;實作只需在 `MeasurementService.Snap` 加掃該面的圓形 Edge
|
|
||||||
- **Esc 取消** — 兩段式量測(距離/角度/三點對齊)按 Esc 清除 pending、再按退回瀏覽模式;
|
|
||||||
順手加量測模式快速鍵(P 點 / D 距離 / E 邊 / F 面 / C 圓…)
|
|
||||||
- **GridSplitter** — 樹面板 / 量測面板寬度可調(現為固定 280 / 300 px,深層裝配樹名稱會被截斷)
|
|
||||||
- **標準視圖 + 正交投影** — 前/上/右/等角視圖按鈕 + `OrthographicCamera` 切換(Helix 原生支援);
|
|
||||||
工程師確認尺寸時正交投影是剛需,透視會騙眼睛
|
|
||||||
- **節點外形尺寸** — 樹節點顯示該零件/組件 L×W×H(`Bounds` 已有,加進 Stats/tooltip 或做成一種量測)
|
|
||||||
- **隔離顯示(Isolate)** — 樹節點右鍵「只顯示此節點」/「反轉顯示」,大組件找零件比逐個勾 checkbox 快
|
|
||||||
- **記住設定 + 最近檔案** — 視窗大小、mm/inch、邊線開關存 user settings;匯入按鈕加 MRU 下拉
|
|
||||||
|
|
||||||
### 中等成本
|
|
||||||
|
|
||||||
- **體積/質心** — 封閉實體用網格 signed volume(各三角形對原點的四面體有向體積加總)近似,
|
|
||||||
對 match 驗證與料號確認有用
|
|
||||||
- **樹搜尋/過濾** — 裝配樹上百零件時依名稱過濾
|
|
||||||
- **干涉支援 >2 個可見檔** — 跳選擇配對對話框或兩兩檢查(現硬性要求剛好 2 個)
|
|
||||||
- **對齊結果匯出 STEP** — 把目前場景(對齊後位置)輸出成新 STEP 檔,把配合結果交接出去;
|
|
||||||
需先驗證 CADability 有無 STEP writer;寫「新檔」不違反唯讀原則
|
|
||||||
- **任意剖面** — 三點定義剖切平面 + 位置數值輸入框(`ClipMesh` 本就吃任意 plane point/normal,純 UI 工作)
|
|
||||||
- **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證),
|
- **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證),
|
||||||
重開同檔免等 CADability 解析(39MB 實測 276 秒)。取捨:B-rep 無法快取 → 量測退化成網格模式
|
重開同檔免等 CADability 解析(39MB 實測 276 秒)。取捨:B-rep 無法快取 → 量測退化成網格模式
|
||||||
或背景補載;是否值得取決於「重複開同一大檔」的頻率
|
或背景補載;是否值得取決於「重複開同一大檔」的頻率(v0.5.0 決議暫緩)
|
||||||
|
|
||||||
### 既有清單(維持)
|
### 既有清單(維持)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
## 專案簡介
|
## 專案簡介
|
||||||
|
|
||||||
CAD 3D 檢視器(Windows 桌面 WPF, .NET 8):STEP/STL/DXF 匯入、STEP 裝配樹、
|
CAD 3D 檢視器(Windows 桌面 WPF, .NET 8):STEP/STL/DXF 匯入、STEP 裝配樹(搜尋/隔離顯示)、
|
||||||
點/距離/邊/面/圓/角度/面距量測、兩點對齊(平移)、三點對齊(旋轉+平移)、軸向旋轉 90°、
|
點/距離/邊/面/圓/角度/面距/體積質心量測(吸附含圓心)、兩點對齊(平移)、三點對齊(旋轉+平移)、
|
||||||
拖曳模式(手形游標直接拖零件)、干涉檢查、剖面、mm⇄inch、CSV/截圖匯出。
|
軸向旋轉 90°、拖曳模式、干涉檢查(≥2 檔兩兩配對)、剖面(X/Y/Z + 3點任意平面)、
|
||||||
|
標準視圖+正交投影、量測快速鍵+Esc、mm⇄inch、CSV/截圖/STEP 匯出、設定與 MRU 保存。
|
||||||
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||||
|
|
||||||
## 技術棧
|
## 技術棧
|
||||||
@@ -41,7 +42,18 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
邊界由面頂點數決定、平移不改 → 永久有效,`RebuildMerged` 不需重算。**不要因為「量測要逐面」而把渲染改回逐面**
|
邊界由面頂點數決定、平移不改 → 永久有效,`RebuildMerged` 不需重算。**不要因為「量測要逐面」而把渲染改回逐面**
|
||||||
- 合併網格的 `BackMaterial`:封閉實體(`SolidCount≥1` 且 `HasBrep`)**不設**(WPF 兩面渲染成本砍半);開放殼/STL 才設。
|
- 合併網格的 `BackMaterial`:封閉實體(`SolidCount≥1` 且 `HasBrep`)**不設**(WPF 兩面渲染成本砍半);開放殼/STL 才設。
|
||||||
**拾取不受材質影響**(WPF 3D hit-test 純幾何、不剔背面),故打合併網格仍命中孔內壁等背向面。逐面 `FacesContent` 一律保留 BackMaterial(剖切要看內部)
|
**拾取不受材質影響**(WPF 3D hit-test 純幾何、不剔背面),故打合併網格仍命中孔內壁等背向面。逐面 `FacesContent` 一律保留 BackMaterial(剖切要看內部)
|
||||||
- 量測值以 B-rep 為準(圓半徑、邊長、角度),面積/面距用網格近似
|
- 量測值以 B-rep 為準(圓半徑、邊長、角度),面積/面距/體積/質心用網格近似
|
||||||
|
- `Snap` 吸附順序(v0.5.0):頂點與「圓形邊」都在容差內比誰離命中點近,圓邊勝出時回傳**圓心**
|
||||||
|
(量孔對孔 pitch 靠這個)。不要把圓心吸附拿掉或改成永遠優先頂點
|
||||||
|
- 鍵盤快速鍵在 `MainWindow.Window_PreviewKeyDown`:**焦點在 TextBoxBase/ComboBox 時直接 return**
|
||||||
|
(樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理
|
||||||
|
- 使用者設定 `SettingsService`(%LOCALAPPDATA%\STPViewer\settings.json):視窗(MainWindow 管)+
|
||||||
|
單位/MRU(VM `LoadSettings`/`SaveSettingsInto`);壞檔回預設、儲存失敗靜默
|
||||||
|
- 匯出 STEP 走 `CADability.ExportStep.WriteToFile(file, Project.CreateSimpleProject()+Model.Add)`;
|
||||||
|
只收 Solid/Shell(STL/DXF 無 B-rep 進不了);改動要跑 `SmokeTest --export-test` 往返驗證
|
||||||
|
- 3點剖面(SectionAxisIndex==3):`_customNormal` null = 拾取中(`HandleSectionPlanePick` 在
|
||||||
|
OnViewportClick 最前面攔點擊);換軸/關剖面/Esc 會重置。平面位置一律用「AABB 8 角投影到法向」內插,
|
||||||
|
軸向與任意法向共用同一段程式,不要改回逐軸特化
|
||||||
- 量測文字一律 `Func<UnitSystem,string>` 延後產生(mm⇄inch 即時切換);內部數值永遠存 mm
|
- 量測文字一律 `Func<UnitSystem,string>` 延後產生(mm⇄inch 即時切換);內部數值永遠存 mm
|
||||||
- 裝配樹節點(`ModelNodeViewModel`)的可見性/邊線/顏色向下 cascade
|
- 裝配樹節點(`ModelNodeViewModel`)的可見性/邊線/顏色向下 cascade
|
||||||
- 剖面只換 `GeometryModel3D.Geometry`(`FaceInfo.Mesh` 保留原始 frozen mesh 供還原與量測)
|
- 剖面只換 `GeometryModel3D.Geometry`(`FaceInfo.Mesh` 保留原始 frozen mesh 供還原與量測)
|
||||||
@@ -98,7 +110,8 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
- 不寫入原始檔(唯讀工具);WPF 限 Windows,不要嘗試移植 vbox/Linux
|
- 不寫入原始檔(唯讀工具);WPF 限 Windows,不要嘗試移植 vbox/Linux
|
||||||
- 干涉檢查需剛好 2 個可見檔案(樹面板勾選);共面貼合(無穿透)不算干涉、gap≈0 視為配合(match)
|
- 干涉檢查需剛好 2 個可見檔案(樹面板勾選);共面貼合(無穿透)不算干涉、gap≈0 視為配合(match)
|
||||||
- SmokeTest 工具:`--tree`(裝配樹)、`--clip-test`(剖切數學)、`--interference-test`(干涉相交/分離/貼合)、
|
- SmokeTest 工具:`--tree`(裝配樹)、`--clip-test`(剖切數學)、`--interference-test`(干涉相交/分離/貼合)、
|
||||||
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)
|
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)、
|
||||||
|
`--export-test <in.stp> <out.stp>`(STEP 匯出往返:寫出→回讀比對實體數)
|
||||||
- **絕不要用 PowerShell regex/Set-Content 改 .cs 檔** — Windows PowerShell 5.1 預設編碼會把 UTF-8 中文弄成亂碼(已踩過,靠反編譯 DLL 救回)。文字取代一律用 Edit 工具
|
- **絕不要用 PowerShell regex/Set-Content 改 .cs 檔** — Windows PowerShell 5.1 預設編碼會把 UTF-8 中文弄成亂碼(已踩過,靠反編譯 DLL 救回)。文字取代一律用 Edit 工具
|
||||||
|
|
||||||
## For_AI/
|
## For_AI/
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
> A Windows desktop 3D viewer for STP/STEP CAD files — multi-file import, assembly tree, and point / distance / edge / face / circle measurement. Built with C# .NET 8 WPF.
|
> A Windows desktop 3D viewer for STP/STEP CAD files — multi-file import, assembly tree, and point / distance / edge / face / circle measurement. Built with C# .NET 8 WPF.
|
||||||
|
|
||||||
   
|
   
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,30 +28,33 @@
|
|||||||
|
|
||||||
Open mechanical STEP parts for quick review and measurement without a heavyweight CAD suite (SolidWorks / Creo).
|
Open mechanical STEP parts for quick review and measurement without a heavyweight CAD suite (SolidWorks / Creo).
|
||||||
|
|
||||||
- **Multi-file import** — STEP / STL / DXF, via the toolbar (multi-select), drag-and-drop, or command line: `STPViewer.exe a.stp b.stl`
|
- **Multi-file import** — STEP / STL / DXF, via the toolbar (multi-select), drag-and-drop, command line (`STPViewer.exe a.stp b.stl`), or the recent-files dropdown (▾, last 10 files)
|
||||||
- **Assembly tree** — STEP product structure restored as a tree (assembly → part); per-node show/hide, recolor (cascades to children), zoom-to; file-level remove and outline-edge toggle
|
- **Assembly tree** — STEP product structure restored as a tree (assembly → part); per-node show/hide, recolor (cascades to children), zoom-to; file-level remove and outline-edge toggle; **name search filter**; right-click **Isolate / Invert visibility / Show all / Volume & centroid**; tooltips show each part's L×W×H
|
||||||
- **Measurement** (toolbar mode toggle, then click the model):
|
- **Measurement** (toolbar mode toggle or hotkey, then click the model):
|
||||||
|
|
||||||
| Mode | Output |
|
| Mode | Hotkey | Output |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| 📍 Point | XYZ coordinate (auto-snaps to nearby B-rep vertex) |
|
| 📍 Point | P | XYZ coordinate (auto-snaps to nearby B-rep vertex **or circle center**) |
|
||||||
| 📏 Distance | Straight-line distance + ΔX/ΔY/ΔZ |
|
| 📏 Distance | D | Straight-line distance + ΔX/ΔY/ΔZ — clicking two hole rims snaps to their **centers** = pin pitch |
|
||||||
| 📐 Edge | Line length / curve length / arc length + radius |
|
| 📐 Edge | E | Line length / curve length / arc length + radius |
|
||||||
| ⬛ Face | Area (mesh approximation) + surface type (plane normal, cylinder radius/axis) |
|
| ⬛ Face | F | Area (mesh approximation) + surface type (plane normal, cylinder radius/axis) |
|
||||||
| ⭕ Circle | Center / radius / diameter / circumference |
|
| ⭕ Circle | C | Center / radius / diameter / circumference |
|
||||||
| ∠ Angle | Angle between two faces (normals) or two straight edges + supplement |
|
| ∠ Angle | A | Angle between two faces (normals) or two straight edges + supplement |
|
||||||
| ⇔ Face distance | Shortest face-to-face distance (mesh approximation) + closest point pair |
|
| ⇔ Face distance | M | Shortest face-to-face distance (mesh approximation) + closest point pair |
|
||||||
| ⤚ Align (2-pt) | Pick a point on the moving part + a target point → pure translation so the two points coincide |
|
| ⚖ Volume | (tree right-click) | Mesh signed volume + centroid marker + bounding size (closed solids) |
|
||||||
| 🎯 Align (3-pt) | 3 source points + 3 target points → rotation + translation in one shot |
|
| ⤚ Align (2-pt) | | Pick a point on the moving part + a target point → pure translation so the two points coincide |
|
||||||
|
| 🎯 Align (3-pt) | | 3 source points + 3 target points → rotation + translation in one shot |
|
||||||
|
|
||||||
|
Press the same hotkey again to exit; **Esc** cancels an in-progress pick, then exits the mode.
|
||||||
- **Rotate** — select a file in the tree, then ↻X / ↻Y / ↻Z to rotate +90° about its center (for re-orienting; repeat to accumulate)
|
- **Rotate** — select a file in the tree, then ↻X / ↻Y / ↻Z to rotate +90° about its center (for re-orienting; repeat to accumulate)
|
||||||
- **Drag** 🖐 — hand-cursor mode; hold the left button to drag a part along the screen plane, release to place (right-button view orbit unaffected)
|
- **Drag** 🖐 — hand-cursor mode; hold the left button to drag a part along the screen plane, release to place (right-button view orbit unaffected)
|
||||||
- **Gizmo** ⊹ — select a file → XYZ tri-color arrows + rotation rings (Fusion 360 style); drag an arrow to move along that axis (view-independent), drag a ring to rotate. Always floats on top, never occluded
|
- **Gizmo** ⊹ — select a file → XYZ tri-color arrows + rotation rings (Fusion 360 style); drag an arrow to move along that axis (view-independent), drag a ring to rotate. Always floats on top, never occluded
|
||||||
- **Interference check** 🧩 — with exactly 2 visible files, shows red intersection curves + intersecting triangle-pair count; otherwise reports the minimum gap (gap ≈ 0 means a fit/match; coplanar contact is not interference)
|
- **Interference check** 🧩 — with 2+ visible files, checks **every pair**: red intersection curves + intersecting triangle-pair count, or the minimum gap (gap ≈ 0 means a fit/match; coplanar contact is not interference)
|
||||||
- **Section plane** ✂ — X/Y/Z axis + position slider + flip; CPU mesh clipping, original geometry preserved (measurement stays exact)
|
- **Section plane** ✂ — X/Y/Z axis **or an arbitrary plane picked from 3 points on the model**; position slider + numeric input + flip; CPU mesh clipping (parallel, in the background), original geometry preserved (measurement stays exact)
|
||||||
- **Units** — one-click mm ⇄ inch; existing measurements (list + 3D labels) convert live
|
- **Units** — one-click mm ⇄ inch; existing measurements (list + 3D labels) convert live
|
||||||
- **Export** — measurement results to CSV (UTF-8 BOM, no mojibake in Excel) and a 2× PNG screenshot of the 3D view
|
- **Export** — measurement results to CSV (UTF-8 BOM, no mojibake in Excel), a 2× PNG screenshot, and 📤 **STEP export**: writes all visible parts at their current aligned positions into a new STEP file (originals untouched)
|
||||||
- **View** — right-button orbit, wheel zoom, middle-button pan, ViewCube
|
- **View** — right-button orbit, wheel zoom, middle-button pan, ViewCube; one-click **Iso / Front / Top / Right** views and an **orthographic ⇄ perspective** toggle
|
||||||
|
- **Remembers your setup** — window size/position, unit choice and recent files persist across sessions; side panels are resizable
|
||||||
|
|
||||||
Measurement principle: edge length, circle radius and angles use **exact B-rep values**; area is a triangle-mesh sum approximation (triangulation precision adapts to model size, 0.02–0.5 mm).
|
Measurement principle: edge length, circle radius and angles use **exact B-rep values**; area is a triangle-mesh sum approximation (triangulation precision adapts to model size, 0.02–0.5 mm).
|
||||||
|
|
||||||
@@ -71,8 +74,8 @@ Measurement principle: edge length, circle radius and angles use **exact B-rep v
|
|||||||
|
|
||||||
Download a release build and run it — no install required.
|
Download a release build and run it — no install required.
|
||||||
|
|
||||||
- **Framework-dependent** (smaller): requires the .NET 8 Desktop Runtime. Run `STPViewer v0.3.2.exe`.
|
- **Framework-dependent** (smaller): requires the .NET 8 Desktop Runtime. Run `STPViewer v0.5.0.exe`.
|
||||||
- **Portable** (self-contained): runtime bundled, no install / admin. Run `STPViewer v0.3.2.exe`.
|
- **Portable** (self-contained): runtime bundled, no install / admin. Run `STPViewer v0.5.0.exe`.
|
||||||
|
|
||||||
Or build from source (see below).
|
Or build from source (see below).
|
||||||
|
|
||||||
@@ -102,11 +105,11 @@ Then import a `.stp` file (toolbar **Import**, drag-and-drop, or command-line ar
|
|||||||
## 📚 Usage Guide
|
## 📚 Usage Guide
|
||||||
|
|
||||||
1. **Import** one or more CAD files. Each file becomes a root in the assembly tree and the view zooms to fit.
|
1. **Import** one or more CAD files. Each file becomes a root in the assembly tree and the view zooms to fit.
|
||||||
2. **Navigate** the tree — toggle visibility, recolor, zoom to a node, or remove a file.
|
2. **Navigate** the tree — search by name, toggle visibility (or right-click → Isolate), recolor, zoom to a node, or remove a file.
|
||||||
3. **Measure** — pick a mode on the toolbar (Point / Distance / Edge / Face / Circle / Angle / Face-distance), then click the model. Results appear in the right-hand panel; delete individually or clear all.
|
3. **Measure** — pick a mode on the toolbar or by hotkey (P/D/E/F/C/A/M), then click the model. Clicking near a hole rim snaps to the circle center — two clicks measure a pin pitch. Esc cancels. Results appear in the right-hand panel; delete individually or clear all.
|
||||||
4. **Assemble** — use Align (2-pt / 3-pt), Rotate, Drag, or the Gizmo to position parts; then run the Interference check to verify fit.
|
4. **Assemble** — use Align (2-pt / 3-pt), Rotate, Drag, or the Gizmo to position parts; then run the Interference check (any number of visible files, all pairs) to verify fit.
|
||||||
5. **Section** — toggle ✂, choose an axis, and slide to cut through the model; measurement stays exact on the original geometry.
|
5. **Section** — toggle ✂, choose an axis or pick 3 points for an arbitrary plane, and slide (or type a %) to cut through the model; measurement stays exact on the original geometry.
|
||||||
6. **Export** — save measurements to CSV or capture a 2× PNG of the view.
|
6. **Export** — save measurements to CSV, capture a 2× PNG, or write the aligned scene to a new STEP file for hand-off to CAD.
|
||||||
|
|
||||||
Headless import-pipeline and geometry-math verification (no UI):
|
Headless import-pipeline and geometry-math verification (no UI):
|
||||||
|
|
||||||
@@ -115,6 +118,7 @@ dotnet run --project tools/SmokeTest -- "path\to\model.stp" # import + assembl
|
|||||||
dotnet run --project tools/SmokeTest -- --clip-test # section clipping math
|
dotnet run --project tools/SmokeTest -- --clip-test # section clipping math
|
||||||
dotnet run --project tools/SmokeTest -- --interference-test # interference: intersect / separate / contact
|
dotnet run --project tools/SmokeTest -- --interference-test # interference: intersect / separate / contact
|
||||||
dotnet run --project tools/SmokeTest -- --align-test # 3-point rigid-transform math
|
dotnet run --project tools/SmokeTest -- --align-test # 3-point rigid-transform math
|
||||||
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP export round-trip
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -142,8 +146,8 @@ STPViewer/
|
|||||||
│ ├── STPViewer.csproj # net8.0-windows, UseWPF, single-source <Version>
|
│ ├── STPViewer.csproj # net8.0-windows, UseWPF, single-source <Version>
|
||||||
│ ├── MainWindow.xaml / .cs # Layout + mouse-pick forwarding
|
│ ├── MainWindow.xaml / .cs # Layout + mouse-pick forwarding
|
||||||
│ ├── Models/ # FaceInfo, MeasureMode, MeasurementResult, UnitSystem
|
│ ├── Models/ # FaceInfo, MeasureMode, MeasurementResult, UnitSystem
|
||||||
│ ├── Services/ # StepImport, Measurement, Interference, Section, RigidAlign
|
│ ├── Services/ # StepImport, Measurement, Interference, Section, RigidAlign, Settings
|
||||||
│ └── ViewModels/ # MainViewModel, ModelNodeViewModel (assembly tree)
|
│ └── ViewModels/ # MainViewModel (partial: Drag/Gizmo/Section/Interference/Export), ModelNodeViewModel
|
||||||
└── tools/SmokeTest/ # Headless import + geometry-math verification
|
└── tools/SmokeTest/ # Headless import + geometry-math verification
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -155,7 +159,8 @@ STPViewer/
|
|||||||
- Files with more than 30,000 outline segments disable edges by default (WPF `LinesVisual3D` cost while orbiting); re-enable per file in the tree.
|
- Files with more than 30,000 outline segments disable edges by default (WPF `LinesVisual3D` cost while orbiting); re-enable per file in the tree.
|
||||||
- **IGES is not supported** (CADability has no IGES reader). STL has no B-rep (point / distance / angle / face-distance only). DXF is wireframe view.
|
- **IGES is not supported** (CADability has no IGES reader). STL has no B-rep (point / distance / angle / face-distance only). DXF is wireframe view.
|
||||||
- Section cuts have no cap fill — the opened face shows the interior back material (dark gray).
|
- Section cuts have no cap fill — the opened face shows the interior back material (dark gray).
|
||||||
- Area and face-distance are mesh approximations; edge length / circle radius / angle are exact B-rep values.
|
- STEP export writes the solids/shells at their current positions as a flat list — the original assembly hierarchy and product names are not preserved. STL meshes and DXF wireframes are not exported (no B-rep).
|
||||||
|
- Area, face-distance and volume/centroid are mesh approximations; edge length / circle radius / angle are exact B-rep values.
|
||||||
- A few AP242 files are incompletely supported by CADability; failed imports show a message (no crash).
|
- A few AP242 files are incompletely supported by CADability; failed imports show a message (no crash).
|
||||||
- Read-only viewer — never writes to or modifies the source file.
|
- Read-only viewer — never writes to or modifies the source file.
|
||||||
|
|
||||||
@@ -171,6 +176,15 @@ STPViewer/
|
|||||||
|
|
||||||
## 📜 Version History
|
## 📜 Version History
|
||||||
|
|
||||||
|
### v0.5.0
|
||||||
|
|
||||||
|
- **Feature round:** circle-center snap (measure hole-to-hole pitch in two clicks), measurement hotkeys (P/D/E/F/C/A/M) + Esc, standard views (Iso/Front/Top/Right) + orthographic projection, assembly-tree search / isolate / invert, volume & centroid, 3-point arbitrary section plane + numeric position input, interference check across 3+ files (all pairs), STEP export of the aligned scene, resizable panels, and persisted window / unit / recent-files settings.
|
||||||
|
|
||||||
|
### v0.4.0
|
||||||
|
|
||||||
|
- **Perf:** section clipping now runs parallel in the background (no more UI freeze while dragging the slider on large files); part-transform bake (drag/gizmo release) parallelized; interference gap refinement accelerated with AABB early rejection.
|
||||||
|
- **Stability:** global exception handling (log to `%LOCALAPPDATA%\STPViewer\error.log` + message box instead of crashing); geometry-mutating commands disabled while a background computation runs; import reentrancy guarded.
|
||||||
|
|
||||||
### v0.3.2
|
### v0.3.2
|
||||||
|
|
||||||
- **Perf:** large-assembly measurement no longer lags while orbiting. Measurement modes now render the merged mesh (one model per file) and resolve the picked face from the hit triangle's vertex index, instead of rendering tens of thousands of per-face models. Per-face rendering is kept only for section mode.
|
- **Perf:** large-assembly measurement no longer lags while orbiting. Measurement modes now render the merged mesh (one model per file) and resolve the picked face from the hit triangle's vertex index, instead of rendering tens of thousands of per-face models. Per-face rendering is kept only for section mode.
|
||||||
|
|||||||
+43
-29
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
> STP/STEP 3D 檢視器(Windows 桌面程式,C# .NET 8 WPF)— 多檔匯入、裝配樹、點/距離/邊/面/圓量測。
|
> STP/STEP 3D 檢視器(Windows 桌面程式,C# .NET 8 WPF)— 多檔匯入、裝配樹、點/距離/邊/面/圓量測。
|
||||||
|
|
||||||
   
|
   
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,30 +28,33 @@
|
|||||||
|
|
||||||
快速開啟機構件 STEP 檔做確認與量測,不需安裝 SolidWorks / Creo 等重量級 CAD。
|
快速開啟機構件 STEP 檔做確認與量測,不需安裝 SolidWorks / Creo 等重量級 CAD。
|
||||||
|
|
||||||
- **多檔匯入** — STEP / STL / DXF,工具列匯入(可複選)、拖放到視窗,或命令列 `STPViewer.exe a.stp b.stl`
|
- **多檔匯入** — STEP / STL / DXF,工具列匯入(可複選)、拖放到視窗、命令列 `STPViewer.exe a.stp b.stl`,或最近檔案下拉(▾,最近 10 筆)
|
||||||
- **裝配樹** — STEP product structure 還原成樹(組件→零件);逐節點 顯示/隱藏、換色(cascade)、Zoom-to;檔案層級可移除、開關輪廓邊線
|
- **裝配樹** — STEP product structure 還原成樹(組件→零件);逐節點 顯示/隱藏、換色(cascade)、Zoom-to;檔案層級可移除、開關輪廓邊線;**名稱搜尋過濾**;右鍵 **只顯示此節點 / 反轉顯示 / 全部顯示 / 量體積質心**;tooltip 顯示外形尺寸 L×W×H
|
||||||
- **量測**(工具列切換模式,點擊模型):
|
- **量測**(工具列切換模式或快速鍵,點擊模型):
|
||||||
|
|
||||||
| 模式 | 輸出 |
|
| 模式 | 快速鍵 | 輸出 |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| 📍 點 | XYZ 座標(自動吸附鄰近 B-rep 頂點) |
|
| 📍 點 | P | XYZ 座標(自動吸附鄰近 B-rep 頂點**或圓心**) |
|
||||||
| 📏 距離 | 兩點直線距離 + ΔX/ΔY/ΔZ |
|
| 📏 距離 | D | 兩點直線距離 + ΔX/ΔY/ΔZ — 點兩個孔緣即吸附**圓心**=直接量 pin pitch |
|
||||||
| 📐 邊 | 直線長 / 曲線長 / 圓弧長 + 半徑 |
|
| 📐 邊 | E | 直線長 / 曲線長 / 圓弧長 + 半徑 |
|
||||||
| ⬛ 面 | 面積(網格近似)+ 曲面類型(平面法向量、圓柱半徑/軸向) |
|
| ⬛ 面 | F | 面積(網格近似)+ 曲面類型(平面法向量、圓柱半徑/軸向) |
|
||||||
| ⭕ 圓 | 圓心 / 半徑 / 直徑 / 周長 |
|
| ⭕ 圓 | C | 圓心 / 半徑 / 直徑 / 周長 |
|
||||||
| ∠ 角度 | 兩面(法向量)/ 兩直線邊 夾角 + 補角 |
|
| ∠ 角度 | A | 兩面(法向量)/ 兩直線邊 夾角 + 補角 |
|
||||||
| ⇔ 面距 | 面到面最短距離(網格近似)+ 最近點對 |
|
| ⇔ 面距 | M | 面到面最短距離(網格近似)+ 最近點對 |
|
||||||
| ⤚ 對齊(兩點) | 點「要移動零件」一點 + 目標點 → 純平移使兩點貼合 |
|
| ⚖ 體積 | (樹右鍵) | 網格 signed volume + 質心標記 + 外形尺寸(封閉實體) |
|
||||||
| 🎯 對齊(三點) | 來源檔 3 點 + 目標檔 3 對應點 → 旋轉+平移一次貼合 |
|
| ⤚ 對齊(兩點) | | 點「要移動零件」一點 + 目標點 → 純平移使兩點貼合 |
|
||||||
|
| 🎯 對齊(三點) | | 來源檔 3 點 + 目標檔 3 對應點 → 旋轉+平移一次貼合 |
|
||||||
|
|
||||||
|
同鍵再按 = 退出模式;**Esc** 先取消進行中的點選、再按退出模式。
|
||||||
- **旋轉** — 樹面板選檔案 + 工具列 ↻X/↻Y/↻Z 繞中心 +90°(擺正方向用,連按累加)
|
- **旋轉** — 樹面板選檔案 + 工具列 ↻X/↻Y/↻Z 繞中心 +90°(擺正方向用,連按累加)
|
||||||
- **拖曳** 🖐 — 手形游標模式,左鍵按住零件沿螢幕平面拖動、放開定位(右鍵轉視角不受影響)
|
- **拖曳** 🖐 — 手形游標模式,左鍵按住零件沿螢幕平面拖動、放開定位(右鍵轉視角不受影響)
|
||||||
- **操作器** ⊹ — 樹面板選檔案 → XYZ 三色箭頭 + 旋轉環(Fusion 360 風格);拖箭頭沿該軸移動(與視角無關)、拖環繞軸轉。永遠浮在最上層、不被實體遮擋
|
- **操作器** ⊹ — 樹面板選檔案 → XYZ 三色箭頭 + 旋轉環(Fusion 360 風格);拖箭頭沿該軸移動(與視角無關)、拖環繞軸轉。永遠浮在最上層、不被實體遮擋
|
||||||
- **干涉檢查** 🧩 — 勾選剛好 2 個可見檔案 → 相交時顯示紅色干涉交線 + 相交三角形對數;無相交時回報最小間隙 gap(gap≈0 即為配合 match,共面貼合不算干涉)
|
- **干涉檢查** 🧩 — 2 個以上可見檔案**兩兩配對**檢查:相交時顯示紅色干涉交線 + 相交三角形對數;無相交時回報最小間隙 gap(gap≈0 即為配合 match,共面貼合不算干涉)
|
||||||
- **剖面** ✂ — X/Y/Z 軸 + 位置滑桿 + 反向;CPU 網格裁切,原始幾何保留(量測仍精確)
|
- **剖面** ✂ — X/Y/Z 軸或**在模型上點 3 點定義任意平面**;位置滑桿 + 數值輸入 + 反向;CPU 網格裁切(背景平行運算),原始幾何保留(量測仍精確)
|
||||||
- **單位** — mm ⇄ inch 一鍵切換,既有量測(清單與 3D 標籤)即時換算
|
- **單位** — mm ⇄ inch 一鍵切換,既有量測(清單與 3D 標籤)即時換算
|
||||||
- **匯出** — 量測結果 CSV(UTF-8 BOM,Excel 中文不亂碼)、3D 視圖 2× PNG 截圖
|
- **匯出** — 量測結果 CSV(UTF-8 BOM,Excel 中文不亂碼)、3D 視圖 2× PNG 截圖、📤 **STEP 匯出**:可見零件以目前對齊位置寫成新 STEP 檔(原始檔不動)
|
||||||
- **視角** — 右鍵旋轉、滾輪縮放、中鍵平移、ViewCube
|
- **視角** — 右鍵旋轉、滾輪縮放、中鍵平移、ViewCube;一鍵 **等角/前/上/右** 標準視圖與 **正交⇄透視** 切換
|
||||||
|
- **記住設定** — 視窗位置大小、單位、最近檔案自動保存;左右面板可拖曳調寬
|
||||||
|
|
||||||
量測原則:邊長、圓半徑、角度取 **B-rep 精確值**;面積為三角網格加總近似(三角化精度依模型尺寸自適應 0.02–0.5 mm)。
|
量測原則:邊長、圓半徑、角度取 **B-rep 精確值**;面積為三角網格加總近似(三角化精度依模型尺寸自適應 0.02–0.5 mm)。
|
||||||
|
|
||||||
@@ -71,8 +74,8 @@
|
|||||||
|
|
||||||
下載發佈版直接執行,免安裝。
|
下載發佈版直接執行,免安裝。
|
||||||
|
|
||||||
- **框架相依版**(檔案較小):需 .NET 8 Desktop Runtime,執行 `STPViewer v0.3.2.exe`。
|
- **框架相依版**(檔案較小):需 .NET 8 Desktop Runtime,執行 `STPViewer v0.5.0.exe`。
|
||||||
- **Portable 版**(self-contained):內含 runtime,免安裝 / 免系統管理員,執行 `STPViewer v0.3.2.exe`。
|
- **Portable 版**(self-contained):內含 runtime,免安裝 / 免系統管理員,執行 `STPViewer v0.5.0.exe`。
|
||||||
|
|
||||||
或從原始碼建置(見下)。
|
或從原始碼建置(見下)。
|
||||||
|
|
||||||
@@ -102,11 +105,11 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
## 📚 使用指南
|
## 📚 使用指南
|
||||||
|
|
||||||
1. **匯入** 一個或多個 CAD 檔,每個檔案成為裝配樹的一個 root,視角自動 Zoom 到全景。
|
1. **匯入** 一個或多個 CAD 檔,每個檔案成為裝配樹的一個 root,視角自動 Zoom 到全景。
|
||||||
2. **操作樹** — 顯示/隱藏、換色、Zoom-to 某節點,或移除檔案。
|
2. **操作樹** — 名稱搜尋、顯示/隱藏(或右鍵隔離顯示)、換色、Zoom-to 某節點,或移除檔案。
|
||||||
3. **量測** — 工具列選模式(點 / 距離 / 邊 / 面 / 圓 / 角度 / 面距),點擊模型;結果顯示於右側面板,可單筆刪除或全部清除。
|
3. **量測** — 工具列選模式或按快速鍵(P/D/E/F/C/A/M),點擊模型;點孔緣自動吸圓心,兩下就是 pin pitch;Esc 取消。結果顯示於右側面板,可單筆刪除或全部清除。
|
||||||
4. **裝配** — 用 對齊(兩點/三點)、旋轉、拖曳、操作器擺位,再用干涉檢查驗證配合。
|
4. **裝配** — 用 對齊(兩點/三點)、旋轉、拖曳、操作器擺位,再用干涉檢查(可見檔全部兩兩檢查)驗證配合。
|
||||||
5. **剖面** — 開啟 ✂、選軸向、滑動剖切;量測仍以原始幾何精確計算。
|
5. **剖面** — 開啟 ✂、選軸向或點 3 點定義任意平面、滑動(或輸入 %)剖切;量測仍以原始幾何精確計算。
|
||||||
6. **匯出** — 量測結果存 CSV,或截 2× PNG。
|
6. **匯出** — 量測結果存 CSV、截 2× PNG,或把對齊後的場景寫成新 STEP 檔交接給 CAD。
|
||||||
|
|
||||||
無 UI 匯入管線與幾何數學驗證:
|
無 UI 匯入管線與幾何數學驗證:
|
||||||
|
|
||||||
@@ -115,6 +118,7 @@ dotnet run --project tools/SmokeTest -- "path\to\model.stp" # 匯入 + 裝配
|
|||||||
dotnet run --project tools/SmokeTest -- --clip-test # 剖切裁切數學
|
dotnet run --project tools/SmokeTest -- --clip-test # 剖切裁切數學
|
||||||
dotnet run --project tools/SmokeTest -- --interference-test # 干涉 相交/分離/貼合
|
dotnet run --project tools/SmokeTest -- --interference-test # 干涉 相交/分離/貼合
|
||||||
dotnet run --project tools/SmokeTest -- --align-test # 三點對齊剛體變換數學
|
dotnet run --project tools/SmokeTest -- --align-test # 三點對齊剛體變換數學
|
||||||
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP 匯出往返驗證
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -142,8 +146,8 @@ STPViewer/
|
|||||||
│ ├── STPViewer.csproj # net8.0-windows、UseWPF、單一版號來源 <Version>
|
│ ├── STPViewer.csproj # net8.0-windows、UseWPF、單一版號來源 <Version>
|
||||||
│ ├── MainWindow.xaml / .cs # 版面 + 滑鼠拾取轉發
|
│ ├── MainWindow.xaml / .cs # 版面 + 滑鼠拾取轉發
|
||||||
│ ├── Models/ # FaceInfo、MeasureMode、MeasurementResult、UnitSystem
|
│ ├── Models/ # FaceInfo、MeasureMode、MeasurementResult、UnitSystem
|
||||||
│ ├── Services/ # StepImport、Measurement、Interference、Section、RigidAlign
|
│ ├── Services/ # StepImport、Measurement、Interference、Section、RigidAlign、Settings
|
||||||
│ └── ViewModels/ # MainViewModel、ModelNodeViewModel(裝配樹)
|
│ └── ViewModels/ # MainViewModel(partial:Drag/Gizmo/Section/Interference/Export)、ModelNodeViewModel
|
||||||
└── tools/SmokeTest/ # 無 UI 匯入 + 幾何數學驗證
|
└── tools/SmokeTest/ # 無 UI 匯入 + 幾何數學驗證
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -155,7 +159,8 @@ STPViewer/
|
|||||||
- 輪廓邊線超過 30,000 線段的檔案預設關閉邊線(WPF `LinesVisual3D` 轉動視角時效能限制),可在樹面板手動開啟。
|
- 輪廓邊線超過 30,000 線段的檔案預設關閉邊線(WPF `LinesVisual3D` 轉動視角時效能限制),可在樹面板手動開啟。
|
||||||
- **IGES 不支援**(CADability 無 IGES reader);STL 無 B-rep,僅支援 點/距離/角度/面距 量測;DXF 為線架構檢視。
|
- **IGES 不支援**(CADability 無 IGES reader);STL 無 B-rep,僅支援 點/距離/角度/面距 量測;DXF 為線架構檢視。
|
||||||
- 剖切面無封口(cap),剖開處顯示內部背面材質(深灰)。
|
- 剖切面無封口(cap),剖開處顯示內部背面材質(深灰)。
|
||||||
- 面積與面距為三角網格近似值;邊長/圓半徑/角度為 B-rep 精確值。
|
- STEP 匯出為「目前位置的 Solid/Shell 平面清單」,不保留原始裝配階層與 product 名稱;STL 網格、DXF 線架構無 B-rep 不匯出。
|
||||||
|
- 面積、面距、體積/質心為三角網格近似值;邊長/圓半徑/角度為 B-rep 精確值。
|
||||||
- 少數 AP242 檔案 CADability 支援不完整,匯入失敗會提示訊息(不閃退)。
|
- 少數 AP242 檔案 CADability 支援不完整,匯入失敗會提示訊息(不閃退)。
|
||||||
- 唯讀檢視器,不寫入/修改原始檔案。
|
- 唯讀檢視器,不寫入/修改原始檔案。
|
||||||
|
|
||||||
@@ -171,6 +176,15 @@ STPViewer/
|
|||||||
|
|
||||||
## 📜 版本紀錄
|
## 📜 版本紀錄
|
||||||
|
|
||||||
|
### v0.5.0
|
||||||
|
|
||||||
|
- **功能擴充:** 圓心吸附(兩下量孔對孔 pitch)、量測快速鍵(P/D/E/F/C/A/M)+ Esc、標準視圖(等角/前/上/右)+ 正交投影、裝配樹搜尋/隔離顯示/反轉、體積與質心、3點任意剖切平面 + 位置數值輸入、干涉檢查支援 3+ 檔(兩兩配對)、對齊後場景匯出 STEP、面板可調寬、視窗/單位/最近檔案自動保存。
|
||||||
|
|
||||||
|
### v0.4.0
|
||||||
|
|
||||||
|
- **效能:** 剖面裁切改背景平行運算(大檔拉滑桿不再凍結 UI);零件變換烘焙(拖曳/操作器放開)平行化;干涉間隙精修以 AABB 快速拒絕加速。
|
||||||
|
- **穩定性:** 全域例外處理(記 log 至 `%LOCALAPPDATA%\STPViewer\error.log` + 訊息框,不閃退);背景運算期間停用會改動幾何的指令;匯入防重入。
|
||||||
|
|
||||||
### v0.3.2
|
### v0.3.2
|
||||||
|
|
||||||
- **效能:** 大組件量測模式下轉動視角不再卡頓。量測模式改為渲染合併網格(每檔 1 個 model),並用命中三角形的頂點 index 反查回是哪個面,不再掛數萬個逐面 model;逐面渲染只保留給剖面模式。
|
- **效能:** 大組件量測模式下轉動視角不再卡頓。量測模式改為渲染合併網格(每檔 1 個 model),並用命中三角形的頂點 index 反查回是哪個面,不再掛數萬個逐面 model;逐面渲染只保留給剖面模式。
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# v0.5.0 — Hole-pitch measuring, 3-point section planes and STEP export
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Measure hole-to-hole pitch in two clicks — snap to circle centers**
|
||||||
|
* Point / Distance / Align picks now **snap to the center of a circular edge** when you click near
|
||||||
|
a hole or a round boss — measure connector pin pitch by simply clicking two hole rims.
|
||||||
|
* Vertex snapping still works; whichever feature is closest to your click wins.
|
||||||
|
|
||||||
|
**Keyboard shortcuts and Esc**
|
||||||
|
* Measure modes now have hotkeys: **P** point, **D** distance, **E** edge, **F** face, **C** circle,
|
||||||
|
**A** angle, **M** face-to-face. Press the same key again to go back to browsing.
|
||||||
|
* **Esc** cancels an in-progress multi-step measurement first (keeping the mode), then exits the
|
||||||
|
mode, then closes the manipulator — no more re-clicking toolbar buttons after a mis-pick.
|
||||||
|
|
||||||
|
**Standard views and orthographic projection**
|
||||||
|
* One-click **Iso / Front / Top / Right** view buttons (Z-up, mechanical convention).
|
||||||
|
* New **orthographic ⇄ perspective** toggle — check dimensions without perspective distortion.
|
||||||
|
The 3-axis manipulator stays perfectly aligned in both projections.
|
||||||
|
|
||||||
|
**Assembly tree: search, isolate, volume**
|
||||||
|
* A **search box** above the tree filters nodes by name (ancestors and children of a match stay
|
||||||
|
visible, branches auto-expand) — find one part in a hundred instantly.
|
||||||
|
* Right-click any node: **Isolate** (show only this node), **Invert visibility**, **Show all**.
|
||||||
|
* Right-click **Volume / centroid**: closed solids get mesh-based volume, a centroid marker in the
|
||||||
|
view, and overall L×W×H. Node tooltips now show each part's bounding size too.
|
||||||
|
|
||||||
|
**Cut along any plane — 3-point section + numeric position**
|
||||||
|
* The section axis picker gains a **"3-pt"** option: click three points on the model to define an
|
||||||
|
arbitrary cutting plane, then sweep it with the position slider as usual.
|
||||||
|
* The position value is now also an **editable number field** (0–100 %).
|
||||||
|
|
||||||
|
**Interference check across 3+ files**
|
||||||
|
* The check now accepts **two or more** visible files and tests **every pair**, reporting each
|
||||||
|
pair's result (intersection lines or minimum gap) plus a summary of how many pairs collide.
|
||||||
|
|
||||||
|
**Hand off your alignment — export the scene as STEP**
|
||||||
|
* New **📤 STEP** button writes all visible parts **at their current aligned positions** into a
|
||||||
|
brand-new STEP file — mate connectors in STPViewer, then hand the assembly to any CAD.
|
||||||
|
Original files are never modified.
|
||||||
|
|
||||||
|
**Quality of life**
|
||||||
|
* Window size/position, mm ⇄ inch choice and the **last 10 opened files** (▾ next to Import) are
|
||||||
|
remembered between sessions.
|
||||||
|
* The tree and measurement panels are now **resizable** with drag splitters.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Two builds are produced:
|
||||||
|
|
||||||
|
| Build | Needs .NET 8 Desktop Runtime? | Notes |
|
||||||
|
|-------|-------------------------------|-------|
|
||||||
|
| **Standard** (`STPViewer_v0.5.0`) | ✅ Yes | Smaller; for machines that already have the runtime |
|
||||||
|
| **Portable** (`STPViewer_v0.5.0_portable`) | ❌ No | Runtime bundled — unzip and run, no install / admin needed |
|
||||||
|
|
||||||
|
Run `STPViewer v0.5.0.exe`.
|
||||||
|
|
||||||
|
**Full changelog:** [v0.4.0...v0.5.0](https://github.com/ETWen/STPViewer/compare/v0.4.0...v0.5.0)
|
||||||
+133
-79
@@ -7,7 +7,8 @@
|
|||||||
Title="STPViewer — STP/STEP 3D 檢視器"
|
Title="STPViewer — STP/STEP 3D 檢視器"
|
||||||
Height="820" Width="1320"
|
Height="820" Width="1320"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver">
|
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver"
|
||||||
|
PreviewKeyDown="Window_PreviewKeyDown">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<conv:EnumToBoolConverter x:Key="ModeConv" />
|
<conv:EnumToBoolConverter x:Key="ModeConv" />
|
||||||
@@ -25,23 +26,37 @@
|
|||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Command="{Binding ImportCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding ImportCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
||||||
|
<Button Click="Recent_Click" Padding="4,4" FontSize="13"
|
||||||
|
ToolTip="最近開啟的檔案">▾</Button>
|
||||||
<Button Command="{Binding ZoomAllCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding ZoomAllCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<TextBlock Text="視圖:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="等角視圖">等角</Button>
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Front" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="前視圖(-Y 方向看,Z 向上)">前</Button>
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Top" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="上視圖(+Z 往下看)">上</Button>
|
||||||
|
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Right" Padding="6,4" FontSize="13"
|
||||||
|
ToolTip="右視圖(+X 方向看,Z 向上)">右</Button>
|
||||||
|
<ToggleButton IsChecked="{Binding OrthographicView}" Padding="6,4" FontSize="13" Margin="2,0"
|
||||||
|
ToolTip="正交投影(平行投影,量尺寸不受透視變形;再按切回透視)">正交</ToggleButton>
|
||||||
|
<Separator />
|
||||||
<TextBlock Text="量測:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
<TextBlock Text="量測:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點/圓心)— 快速鍵 P"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}">📍 點</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}">📍 點</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)— 快速鍵 D"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}">📏 距離</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}">📏 距離</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)— 快速鍵 E"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}">📐 邊</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}">📐 邊</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量 — 快速鍵 F"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}">⬛ 面</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}">⬛ 面</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長 — 快速鍵 C"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}">⭕ 圓</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}">⭕ 圓</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)— 快速鍵 A"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}">∠ 角度</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}">∠ 角度</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)— 快速鍵 M"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}">⇔ 面距</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}">⇔ 面距</ToggleButton>
|
||||||
<Separator />
|
<Separator />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
@@ -65,10 +80,10 @@
|
|||||||
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
||||||
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="干涉檢查:檢查 2 個可見檔案是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
ToolTip="干涉檢查:2 個以上可見檔案兩兩檢查是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Button Command="{Binding ClearMeasurementsCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding ClearMeasurementsCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="清除全部量測與標記">🧹 清除</Button>
|
ToolTip="清除全部量測與標記(Esc = 取消進行中量測/退出模式)">🧹 清除</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
<ToggleButton IsChecked="{Binding UseInch}" Padding="8,4" FontSize="13"
|
<ToggleButton IsChecked="{Binding UseInch}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
||||||
@@ -77,22 +92,27 @@
|
|||||||
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
||||||
<Button Command="{Binding SaveScreenshotCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding SaveScreenshotCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
||||||
|
<Button Command="{Binding ExportStepFileCommand}" Padding="8,4" FontSize="13"
|
||||||
|
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔 — 對齊插合結果可交接 CAD;不動原始檔">📤 STEP</Button>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<ToggleButton IsChecked="{Binding SectionEnabled}" Padding="8,4" FontSize="13"
|
<ToggleButton IsChecked="{Binding SectionEnabled}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="開啟/關閉剖面檢視">✂ 剖面</ToggleButton>
|
ToolTip="開啟/關閉剖面檢視">✂ 剖面</ToggleButton>
|
||||||
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="52" Margin="4,0"
|
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="60" Margin="4,0"
|
||||||
VerticalAlignment="Center" ToolTip="剖切軸向"
|
VerticalAlignment="Center" ToolTip="剖切軸向(3點 = 在模型上點 3 點定義任意平面)"
|
||||||
IsEnabled="{Binding SectionEnabled}">
|
IsEnabled="{Binding SectionEnabled}">
|
||||||
<ComboBoxItem>X</ComboBoxItem>
|
<ComboBoxItem>X</ComboBoxItem>
|
||||||
<ComboBoxItem>Y</ComboBoxItem>
|
<ComboBoxItem>Y</ComboBoxItem>
|
||||||
<ComboBoxItem>Z</ComboBoxItem>
|
<ComboBoxItem>Z</ComboBoxItem>
|
||||||
|
<ComboBoxItem>3點</ComboBoxItem>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Slider Value="{Binding SectionPosition}" Minimum="0" Maximum="100" Width="180"
|
<Slider Value="{Binding SectionPosition}" Minimum="0" Maximum="100" Width="180"
|
||||||
VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿軸 %)"
|
VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿法向 %)"
|
||||||
IsEnabled="{Binding SectionEnabled}" />
|
IsEnabled="{Binding SectionEnabled}" />
|
||||||
<TextBlock Text="{Binding SectionPosition, StringFormat={}{0:F0}%}" Width="36"
|
<TextBox Text="{Binding SectionPosition, StringFormat={}{0:F0}}" Width="34"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" ToolTip="剖面位置 %(0–100,可直接輸入,離開欄位生效)"
|
||||||
|
IsEnabled="{Binding SectionEnabled}" />
|
||||||
|
<TextBlock Text="%" VerticalAlignment="Center" Margin="1,0,0,0" />
|
||||||
<CheckBox IsChecked="{Binding SectionFlip}" Content="反向" VerticalAlignment="Center"
|
<CheckBox IsChecked="{Binding SectionFlip}" Content="反向" VerticalAlignment="Center"
|
||||||
Margin="4,0" ToolTip="切換保留側" IsEnabled="{Binding SectionEnabled}" />
|
Margin="4,0" ToolTip="切換保留側" IsEnabled="{Binding SectionEnabled}" />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
@@ -111,75 +131,105 @@
|
|||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="280" />
|
<ColumnDefinition Width="280" MinWidth="160" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="300" />
|
<ColumnDefinition Width="*" MinWidth="300" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="300" MinWidth="160" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- ─── 裝配樹面板 ─── -->
|
<!-- ─── 裝配樹面板 ─── -->
|
||||||
<GroupBox Grid.Column="0" Header="模型 / 裝配樹" Margin="4">
|
<GroupBox Grid.Column="0" Header="模型 / 裝配樹" Margin="4">
|
||||||
<TreeView ItemsSource="{Binding Roots}"
|
<DockPanel>
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
<TextBox DockPanel.Dock="Top" Margin="0,2,0,4"
|
||||||
VirtualizingPanel.IsVirtualizing="True"
|
Text="{Binding TreeFilter, UpdateSourceTrigger=PropertyChanged}"
|
||||||
SelectedItemChanged="Tree_SelectedItemChanged">
|
ToolTip="輸入名稱過濾裝配樹(只影響樹面板顯示,不影響 3D 可見性;清空 = 全顯)" />
|
||||||
<TreeView.ItemContainerStyle>
|
<TreeView ItemsSource="{Binding Roots}"
|
||||||
<Style TargetType="TreeViewItem">
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
VirtualizingPanel.IsVirtualizing="True"
|
||||||
</Style>
|
SelectedItemChanged="Tree_SelectedItemChanged">
|
||||||
</TreeView.ItemContainerStyle>
|
<TreeView.ItemContainerStyle>
|
||||||
<TreeView.ItemTemplate>
|
<Style TargetType="TreeViewItem">
|
||||||
<HierarchicalDataTemplate DataType="{x:Type vm:ModelNodeViewModel}"
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
ItemsSource="{Binding Children}">
|
<Setter Property="Visibility"
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,1"
|
Value="{Binding IsFilterVisible, Converter={StaticResource BoolToVis}}" />
|
||||||
ToolTip="{Binding ToolTipText}">
|
</Style>
|
||||||
<CheckBox IsChecked="{Binding IsVisible}" VerticalAlignment="Center"
|
</TreeView.ItemContainerStyle>
|
||||||
ToolTip="顯示 / 隱藏(含子節點)" />
|
<TreeView.ItemTemplate>
|
||||||
<Button Width="14" Height="14" Margin="3,0"
|
<HierarchicalDataTemplate DataType="{x:Type vm:ModelNodeViewModel}"
|
||||||
VerticalAlignment="Center" ToolTip="點擊換色(含子節點)"
|
ItemsSource="{Binding Children}">
|
||||||
Command="{Binding CycleColorCommand}">
|
<StackPanel Orientation="Horizontal" Margin="0,1"
|
||||||
<Button.Template>
|
Background="Transparent"
|
||||||
<ControlTemplate TargetType="Button">
|
ToolTip="{Binding ToolTipText}"
|
||||||
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="2">
|
Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||||
<Border.Background>
|
<StackPanel.ContextMenu>
|
||||||
<SolidColorBrush Color="{Binding Color}" />
|
<ContextMenu>
|
||||||
</Border.Background>
|
<MenuItem Header="👁 只顯示此節點"
|
||||||
</Border>
|
Command="{Binding PlacementTarget.Tag.IsolateNodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
</ControlTemplate>
|
CommandParameter="{Binding}" />
|
||||||
</Button.Template>
|
<MenuItem Header="🔄 反轉顯示"
|
||||||
</Button>
|
Command="{Binding PlacementTarget.Tag.InvertVisibilityCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"
|
<MenuItem Header="☀ 全部顯示"
|
||||||
MaxWidth="130" TextTrimming="CharacterEllipsis">
|
Command="{Binding PlacementTarget.Tag.ShowAllCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
<TextBlock.Style>
|
<Separator />
|
||||||
<Style TargetType="TextBlock">
|
<MenuItem Header="⚖ 量體積 / 質心"
|
||||||
<Style.Triggers>
|
Command="{Binding PlacementTarget.Tag.MeasureVolumeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
<DataTrigger Binding="{Binding IsRoot}" Value="True">
|
CommandParameter="{Binding}" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
</ContextMenu>
|
||||||
</DataTrigger>
|
</StackPanel.ContextMenu>
|
||||||
</Style.Triggers>
|
<CheckBox IsChecked="{Binding IsVisible}" VerticalAlignment="Center"
|
||||||
</Style>
|
ToolTip="顯示 / 隱藏(含子節點)" />
|
||||||
</TextBlock.Style>
|
<Button Width="14" Height="14" Margin="3,0"
|
||||||
</TextBlock>
|
VerticalAlignment="Center" ToolTip="點擊換色(含子節點)"
|
||||||
<CheckBox IsChecked="{Binding ShowEdges}" Content="邊" FontSize="10"
|
Command="{Binding CycleColorCommand}">
|
||||||
VerticalAlignment="Center" Margin="4,0,0,0"
|
<Button.Template>
|
||||||
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
<ControlTemplate TargetType="Button">
|
||||||
ToolTip="顯示輪廓邊線(邊線量大時轉動視角會變慢)" />
|
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="2">
|
||||||
<Button Content="🔍" Width="20" Margin="3,0,0,0" Padding="0"
|
<Border.Background>
|
||||||
VerticalAlignment="Center" ToolTip="縮放至此節點"
|
<SolidColorBrush Color="{Binding Color}" />
|
||||||
Command="{Binding DataContext.ZoomNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
</Border.Background>
|
||||||
CommandParameter="{Binding}" />
|
</Border>
|
||||||
<Button Content="✕" Width="20" Padding="0" Margin="2,0,0,0"
|
</ControlTemplate>
|
||||||
VerticalAlignment="Center" ToolTip="移除此檔案"
|
</Button.Template>
|
||||||
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
</Button>
|
||||||
Command="{Binding DataContext.RemoveRootCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"
|
||||||
CommandParameter="{Binding}" />
|
MaxWidth="130" TextTrimming="CharacterEllipsis">
|
||||||
</StackPanel>
|
<TextBlock.Style>
|
||||||
</HierarchicalDataTemplate>
|
<Style TargetType="TextBlock">
|
||||||
</TreeView.ItemTemplate>
|
<Style.Triggers>
|
||||||
</TreeView>
|
<DataTrigger Binding="{Binding IsRoot}" Value="True">
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<CheckBox IsChecked="{Binding ShowEdges}" Content="邊" FontSize="10"
|
||||||
|
VerticalAlignment="Center" Margin="4,0,0,0"
|
||||||
|
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="顯示輪廓邊線(邊線量大時轉動視角會變慢)" />
|
||||||
|
<Button Content="🔍" Width="20" Margin="3,0,0,0" Padding="0"
|
||||||
|
VerticalAlignment="Center" ToolTip="縮放至此節點"
|
||||||
|
Command="{Binding DataContext.ZoomNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
<Button Content="✕" Width="20" Padding="0" Margin="2,0,0,0"
|
||||||
|
VerticalAlignment="Center" ToolTip="移除此檔案"
|
||||||
|
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
||||||
|
Command="{Binding DataContext.RemoveRootCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
</StackPanel>
|
||||||
|
</HierarchicalDataTemplate>
|
||||||
|
</TreeView.ItemTemplate>
|
||||||
|
</TreeView>
|
||||||
|
</DockPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
|
ToolTip="拖曳調整面板寬度" />
|
||||||
|
|
||||||
<!-- ─── 3D 視圖 ─── -->
|
<!-- ─── 3D 視圖 ─── -->
|
||||||
<h:HelixViewport3D x:Name="viewport" Grid.Column="1"
|
<h:HelixViewport3D x:Name="viewport" Grid.Column="2"
|
||||||
ShowViewCube="True" ShowCoordinateSystem="True"
|
ShowViewCube="True" ShowCoordinateSystem="True"
|
||||||
ShowFrameRate="False" ZoomExtentsWhenLoaded="True"
|
ShowFrameRate="False" ZoomExtentsWhenLoaded="True"
|
||||||
PanGesture="MiddleClick" PanGesture2="Shift+LeftClick"
|
PanGesture="MiddleClick" PanGesture2="Shift+LeftClick"
|
||||||
@@ -208,11 +258,15 @@
|
|||||||
|
|
||||||
<!-- 操作器疊圖層:透明 Viewport3D 疊在主視窗上、永遠最上層。
|
<!-- 操作器疊圖層:透明 Viewport3D 疊在主視窗上、永遠最上層。
|
||||||
只在操作器開啟時 IsHitTestVisible(否則空白處滑鼠才會穿透回主視窗) -->
|
只在操作器開啟時 IsHitTestVisible(否則空白處滑鼠才會穿透回主視窗) -->
|
||||||
<Viewport3D x:Name="gizmoOverlay" Grid.Column="1"
|
<Viewport3D x:Name="gizmoOverlay" Grid.Column="2"
|
||||||
IsHitTestVisible="{Binding GizmoEnabled}" />
|
IsHitTestVisible="{Binding GizmoEnabled}" />
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="3" Width="5" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
|
ToolTip="拖曳調整面板寬度" />
|
||||||
|
|
||||||
<!-- ─── 量測結果面板 ─── -->
|
<!-- ─── 量測結果面板 ─── -->
|
||||||
<GroupBox Grid.Column="2" Header="量測結果" Margin="4">
|
<GroupBox Grid.Column="4" Header="量測結果" Margin="4">
|
||||||
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
using STPViewer.ViewModels;
|
using STPViewer.ViewModels;
|
||||||
@@ -10,6 +13,7 @@ namespace STPViewer;
|
|||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly MainViewModel _vm = new();
|
private readonly MainViewModel _vm = new();
|
||||||
|
private readonly AppSettings _settings;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -21,12 +25,54 @@ public partial class MainWindow : Window
|
|||||||
_vm.Attach(viewport);
|
_vm.Attach(viewport);
|
||||||
_vm.AttachOverlay(gizmoOverlay);
|
_vm.AttachOverlay(gizmoOverlay);
|
||||||
Loaded += MainWindow_Loaded;
|
Loaded += MainWindow_Loaded;
|
||||||
|
Closing += MainWindow_Closing;
|
||||||
|
|
||||||
|
// 使用者設定:視窗位置大小(View 職責)+ 單位/MRU(VM 職責)
|
||||||
|
_settings = SettingsService.Load();
|
||||||
|
ApplyWindowSettings();
|
||||||
|
_vm.LoadSettings(_settings);
|
||||||
|
|
||||||
// Gizmo 操作器放在疊圖層;manipulator 會把 MouseUp 標 handled,需 handledEventsToo 才收得到
|
// Gizmo 操作器放在疊圖層;manipulator 會把 MouseUp 標 handled,需 handledEventsToo 才收得到
|
||||||
gizmoOverlay.AddHandler(MouseLeftButtonUpEvent,
|
gizmoOverlay.AddHandler(MouseLeftButtonUpEvent,
|
||||||
new MouseButtonEventHandler((_, _) => _vm.OnGizmoMouseUp()), handledEventsToo: true);
|
new MouseButtonEventHandler((_, _) => _vm.OnGizmoMouseUp()), handledEventsToo: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyWindowSettings()
|
||||||
|
{
|
||||||
|
if (_settings.WindowWidth >= 400 && _settings.WindowHeight >= 300)
|
||||||
|
{
|
||||||
|
Width = _settings.WindowWidth;
|
||||||
|
Height = _settings.WindowHeight;
|
||||||
|
}
|
||||||
|
// 位置要落在目前虛擬桌面內才還原(避免拔掉外接螢幕後視窗開在螢幕外)
|
||||||
|
if (!double.IsNaN(_settings.WindowLeft) && !double.IsNaN(_settings.WindowTop) &&
|
||||||
|
_settings.WindowLeft >= SystemParameters.VirtualScreenLeft - 50 &&
|
||||||
|
_settings.WindowTop >= SystemParameters.VirtualScreenTop - 50 &&
|
||||||
|
_settings.WindowLeft <= SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - 200 &&
|
||||||
|
_settings.WindowTop <= SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - 200)
|
||||||
|
{
|
||||||
|
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||||
|
Left = _settings.WindowLeft;
|
||||||
|
Top = _settings.WindowTop;
|
||||||
|
}
|
||||||
|
if (_settings.WindowMaximized)
|
||||||
|
WindowState = WindowState.Maximized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainWindow_Closing(object? sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
Rect r = WindowState == WindowState.Normal
|
||||||
|
? new Rect(Left, Top, Width, Height)
|
||||||
|
: RestoreBounds;
|
||||||
|
_settings.WindowLeft = r.Left;
|
||||||
|
_settings.WindowTop = r.Top;
|
||||||
|
_settings.WindowWidth = r.Width;
|
||||||
|
_settings.WindowHeight = r.Height;
|
||||||
|
_settings.WindowMaximized = WindowState == WindowState.Maximized;
|
||||||
|
_vm.SaveSettingsInto(_settings);
|
||||||
|
SettingsService.Save(_settings);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …</summary>
|
/// <summary>支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …</summary>
|
||||||
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -36,6 +82,47 @@ public partial class MainWindow : Window
|
|||||||
await _vm.ImportFilesAsync(files);
|
await _vm.ImportFilesAsync(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Esc / 量測模式快速鍵(焦點在輸入框時不攔截,樹搜尋、剖面數值可正常打字)</summary>
|
||||||
|
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (Keyboard.FocusedElement is TextBoxBase or ComboBox) return;
|
||||||
|
if (e.Key == Key.Escape)
|
||||||
|
{
|
||||||
|
_vm.OnEscape();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Keyboard.Modifiers != ModifierKeys.None) return;
|
||||||
|
if (_vm.OnHotKey(e.Key))
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>最近檔案下拉(▾):即時從 VM 的 MRU 清單組選單</summary>
|
||||||
|
private void Recent_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var menu = new ContextMenu
|
||||||
|
{
|
||||||
|
PlacementTarget = (UIElement)sender,
|
||||||
|
Placement = PlacementMode.Bottom,
|
||||||
|
};
|
||||||
|
if (_vm.RecentFiles.Count == 0)
|
||||||
|
{
|
||||||
|
menu.Items.Add(new MenuItem { Header = "(沒有最近開啟的檔案)", IsEnabled = false });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (string f in _vm.RecentFiles)
|
||||||
|
menu.Items.Add(new MenuItem
|
||||||
|
{
|
||||||
|
Header = System.IO.Path.GetFileName(f),
|
||||||
|
ToolTip = f,
|
||||||
|
Command = _vm.ImportRecentCommand,
|
||||||
|
CommandParameter = f,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
menu.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) =>
|
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) =>
|
||||||
_vm.SelectedNode = e.NewValue as ModelNodeViewModel;
|
_vm.SelectedNode = e.NewValue as ModelNodeViewModel;
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ public enum MeasureMode
|
|||||||
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
||||||
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
||||||
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
||||||
|
Volume, // 體積/質心(樹右鍵選單觸發,非點擊模式,僅作為結果類型)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ public static class Units
|
|||||||
public static string A(double mm2, UnitSystem u) =>
|
public static string A(double mm2, UnitSystem u) =>
|
||||||
u == UnitSystem.Millimeter ? $"{mm2:N3} mm²" : $"{mm2 / (MmPerInch * MmPerInch):N4} in²";
|
u == UnitSystem.Millimeter ? $"{mm2:N3} mm²" : $"{mm2 / (MmPerInch * MmPerInch):N4} in²";
|
||||||
|
|
||||||
|
/// <summary>體積</summary>
|
||||||
|
public static string V(double mm3, UnitSystem u) =>
|
||||||
|
u == UnitSystem.Millimeter ? $"{mm3:N2} mm³" : $"{mm3 / (MmPerInch * MmPerInch * MmPerInch):N4} in³";
|
||||||
|
|
||||||
/// <summary>座標分量(無單位字尾)</summary>
|
/// <summary>座標分量(無單位字尾)</summary>
|
||||||
public static string C(double mm, UnitSystem u) =>
|
public static string C(double mm, UnitSystem u) =>
|
||||||
u == UnitSystem.Millimeter ? $"{mm:F3}" : $"{mm / MmPerInch:F4}";
|
u == UnitSystem.Millimeter ? $"{mm:F3}" : $"{mm / MmPerInch:F4}";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Version>0.4.0</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
<Version>0.5.0</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ public class MeasurementService
|
|||||||
|
|
||||||
// ─── 拾取輔助 ────────────────────────────────────────────────
|
// ─── 拾取輔助 ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>命中點吸附到最近的 B-rep 頂點(容差內),否則回傳原始表面點</summary>
|
/// <summary>
|
||||||
|
/// 命中點吸附:點到圓形邊附近(容差內)→ 吸附**圓心**(量兩孔 pitch 的關鍵,v0.5.0);
|
||||||
|
/// 否則吸附最近的 B-rep 頂點(容差內);都沒有 → 回傳原始表面點。
|
||||||
|
/// 圓心與頂點都在容差內時,比「命中點到圓形邊」與「命中點到頂點」誰近(圓弧端點常與圓重疊,
|
||||||
|
/// 點在圓邊上時圓心優先勝出才符合直覺)。
|
||||||
|
/// </summary>
|
||||||
public Point3D Snap(FaceInfo fi, Point3D hit, double tolerance)
|
public Point3D Snap(FaceInfo fi, Point3D hit, double tolerance)
|
||||||
{
|
{
|
||||||
if (fi.BrepFace is null) return hit;
|
if (fi.BrepFace is null) return hit;
|
||||||
@@ -47,6 +52,22 @@ public class MeasurementService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* 頂點存取失敗則不吸附 */ }
|
catch { /* 頂點存取失敗則不吸附 */ }
|
||||||
|
|
||||||
|
// 圓形邊:以「命中點到邊曲線」的距離參與比較,勝出時吸附到圓心
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (Edge e in fi.BrepFace.AllEdges)
|
||||||
|
{
|
||||||
|
if (e.Curve3D is not Ellipse { IsCircle: true } el) continue;
|
||||||
|
double pos;
|
||||||
|
try { pos = el.PositionOf(ToGeo(hit)); } catch { continue; }
|
||||||
|
if (double.IsNaN(pos)) continue;
|
||||||
|
Point3D onCurve = ToP3(el.PointAt(Math.Clamp(pos, 0, 1)));
|
||||||
|
double d = (onCurve - hit).Length;
|
||||||
|
if (d < bestD) { bestD = d; best = ToP3(el.Center); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* 邊存取失敗則維持頂點吸附結果 */ }
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +427,47 @@ public class MeasurementService
|
|||||||
return (pa, pb);
|
return (pa, pb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 體積 / 質心(封閉網格 signed volume)────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 網格體積與質心:各三角形與原點構成的四面體有向體積加總(divergence theorem)。
|
||||||
|
/// 只對「封閉」網格有意義(開放殼會得到不可靠的值 → IsClosedReliable=false 供呼叫端提示)。
|
||||||
|
/// 多個 mesh(多實體)直接加總。
|
||||||
|
/// </summary>
|
||||||
|
public static (double VolumeMm3, Point3D Centroid, bool Reliable) MeshVolume(
|
||||||
|
System.Collections.Generic.IEnumerable<MeshGeometry3D> meshes)
|
||||||
|
{
|
||||||
|
double vol6 = 0; // Σ 6×有向體積
|
||||||
|
double absVol6 = 0; // Σ |6×有向體積|(封閉性 sanity check 用)
|
||||||
|
double cx = 0, cy = 0, cz = 0; // Σ 四面體質心 × 6×有向體積
|
||||||
|
foreach (MeshGeometry3D mesh in meshes)
|
||||||
|
{
|
||||||
|
Point3DCollection pos = mesh.Positions;
|
||||||
|
Int32Collection idx = mesh.TriangleIndices;
|
||||||
|
for (int i = 0; i + 2 < idx.Count; i += 3)
|
||||||
|
{
|
||||||
|
Point3D a = pos[idx[i]], b = pos[idx[i + 1]], c = pos[idx[i + 2]];
|
||||||
|
// 6V = a · (b × c)
|
||||||
|
double v = a.X * (b.Y * c.Z - b.Z * c.Y)
|
||||||
|
+ a.Y * (b.Z * c.X - b.X * c.Z)
|
||||||
|
+ a.Z * (b.X * c.Y - b.Y * c.X);
|
||||||
|
vol6 += v;
|
||||||
|
absVol6 += Math.Abs(v);
|
||||||
|
// 四面體(a,b,c,原點)質心 = (a+b+c)/4
|
||||||
|
cx += (a.X + b.X + c.X) / 4 * v;
|
||||||
|
cy += (a.Y + b.Y + c.Y) / 4 * v;
|
||||||
|
cz += (a.Z + b.Z + c.Z) / 4 * v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double volume = Math.Abs(vol6) / 6;
|
||||||
|
// 有向體積相對正負抵銷比例過高 → 可能是開放殼/法向不一致,數值不可靠
|
||||||
|
bool reliable = absVol6 > 0 && Math.Abs(vol6) / absVol6 > 0.01 && volume > 1e-9;
|
||||||
|
Point3D centroid = Math.Abs(vol6) > 1e-12
|
||||||
|
? new Point3D(cx / vol6, cy / vol6, cz / vol6)
|
||||||
|
: default;
|
||||||
|
return (volume, centroid, reliable);
|
||||||
|
}
|
||||||
|
|
||||||
// 點到三角形最近點:共用 InterferenceService.ClosestPointOnTriangle(v0.4.0 去重複)
|
// 點到三角形最近點:共用 InterferenceService.ClosestPointOnTriangle(v0.4.0 去重複)
|
||||||
|
|
||||||
// ─── overlay helpers ─────────────────────────────────────────
|
// ─── overlay helpers ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
|
/// <summary>使用者設定(%LOCALAPPDATA%\STPViewer\settings.json)</summary>
|
||||||
|
public class AppSettings
|
||||||
|
{
|
||||||
|
public double WindowWidth { get; set; } = double.NaN;
|
||||||
|
public double WindowHeight { get; set; } = double.NaN;
|
||||||
|
public double WindowLeft { get; set; } = double.NaN;
|
||||||
|
public double WindowTop { get; set; } = double.NaN;
|
||||||
|
public bool WindowMaximized { get; set; }
|
||||||
|
public bool UseInch { get; set; }
|
||||||
|
public List<string> RecentFiles { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>設定讀寫:壞檔/缺檔一律回預設值,儲存失敗靜默(設定不值得讓程式閃退)</summary>
|
||||||
|
public static class SettingsService
|
||||||
|
{
|
||||||
|
public const int MaxRecentFiles = 10;
|
||||||
|
|
||||||
|
private static readonly string SettingsPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"STPViewer", "settings.json");
|
||||||
|
|
||||||
|
public static AppSettings Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(SettingsPath))
|
||||||
|
return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(SettingsPath)) ?? new AppSettings();
|
||||||
|
}
|
||||||
|
catch { /* 壞檔回預設 */ }
|
||||||
|
return new AppSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save(AppSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
|
||||||
|
File.WriteAllText(SettingsPath,
|
||||||
|
JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
}
|
||||||
|
catch { /* 儲存失敗靜默 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
@@ -39,6 +40,48 @@ public partial class MainViewModel
|
|||||||
StatusText = $"已匯出 {Measurements.Count} 筆量測 → {Path.GetFileName(dlg.FileName)}";
|
StatusText = $"已匯出 {Measurements.Count} 筆量測 → {Path.GetFileName(dlg.FileName)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把可見檔案(目前位置,含對齊/旋轉/拖曳結果)匯出成「新的」STEP 檔 —
|
||||||
|
/// 對齊插合後的配合結果可交接給 CAD;不動任何原始檔(唯讀原則不變)。
|
||||||
|
/// 只含 B-rep Solid/Shell(STL 網格、DXF 線架構無法進 STEP B-rep)。
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsIdle))]
|
||||||
|
private void ExportStepFile()
|
||||||
|
{
|
||||||
|
var geos = Roots.Where(r => r.IsVisible)
|
||||||
|
.SelectMany(r => r.Leaves()).Where(l => l.IsVisible)
|
||||||
|
.SelectMany(l => l.SourceGeos)
|
||||||
|
.Where(g => g is CADability.GeoObject.Solid or CADability.GeoObject.Shell)
|
||||||
|
.ToList();
|
||||||
|
if (geos.Count == 0)
|
||||||
|
{
|
||||||
|
StatusText = "沒有可匯出的 B-rep 幾何(STEP 匯出僅含 Solid/Shell;請勾選要匯出的 STEP 檔案)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dlg = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Title = "匯出目前場景為 STEP(零件在目前對齊位置)",
|
||||||
|
Filter = "STEP 檔案 (*.stp)|*.stp|STEP 檔案 (*.step)|*.step",
|
||||||
|
FileName = $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stp",
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// B-rep 已含目前位置(TransformRoot 對 Solid/Shell 整體 Modify)→ 直接寫出
|
||||||
|
CADability.Project project = CADability.Project.CreateSimpleProject();
|
||||||
|
CADability.Model model = project.GetActiveModel();
|
||||||
|
foreach (CADability.GeoObject.IGeoObject g in geos) model.Add(g);
|
||||||
|
new CADability.ExportStep().WriteToFile(dlg.FileName, project);
|
||||||
|
StatusText = $"已匯出 STEP({geos.Count} 個實體,目前位置)→ {Path.GetFileName(dlg.FileName)}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"STEP 匯出失敗:{ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SaveScreenshot()
|
private void SaveScreenshot()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public partial class MainViewModel
|
|||||||
// 操作器疊圖層:另一個透明 Viewport3D 疊在主視窗上、相機同步、只放操作器 →
|
// 操作器疊圖層:另一個透明 Viewport3D 疊在主視窗上、相機同步、只放操作器 →
|
||||||
// 操作器不在主場景,永不被實體遮擋(always-on-top);空白處滑鼠穿透回主視窗
|
// 操作器不在主場景,永不被實體遮擋(always-on-top);空白處滑鼠穿透回主視窗
|
||||||
private System.Windows.Controls.Viewport3D? _overlayViewport;
|
private System.Windows.Controls.Viewport3D? _overlayViewport;
|
||||||
private PerspectiveCamera? _overlayCamera;
|
private ProjectionCamera? _overlayCamera; // 型別跟隨主相機(透視/正交切換,v0.5.0)
|
||||||
|
|
||||||
/// <summary>注入操作器疊圖層(透明 Viewport3D,疊在主視窗上、永遠最上層)</summary>
|
/// <summary>注入操作器疊圖層(透明 Viewport3D,疊在主視窗上、永遠最上層)</summary>
|
||||||
public void AttachOverlay(System.Windows.Controls.Viewport3D overlay)
|
public void AttachOverlay(System.Windows.Controls.Viewport3D overlay)
|
||||||
@@ -37,16 +37,41 @@ public partial class MainViewModel
|
|||||||
SyncOverlayCamera();
|
SyncOverlayCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>疊圖層相機跟隨主相機(每次主相機變更時呼叫,讓操作器疊在正確螢幕位置)</summary>
|
/// <summary>
|
||||||
|
/// 疊圖層相機跟隨主相機(每次主相機變更時呼叫,讓操作器疊在正確螢幕位置)。
|
||||||
|
/// 主相機可能在透視/正交間切換(Orthographic 屬性)→ 疊圖層相機「型別」也要跟著換,投影才一致。
|
||||||
|
/// </summary>
|
||||||
private void SyncOverlayCamera()
|
private void SyncOverlayCamera()
|
||||||
{
|
{
|
||||||
if (_overlayCamera is null || _viewport?.Camera is not ProjectionCamera src) return;
|
if (_overlayViewport is null || _viewport?.Camera is not ProjectionCamera src) return;
|
||||||
|
|
||||||
|
if (src is PerspectiveCamera p)
|
||||||
|
{
|
||||||
|
if (_overlayCamera is not PerspectiveCamera op)
|
||||||
|
{
|
||||||
|
op = new PerspectiveCamera();
|
||||||
|
_overlayCamera = op;
|
||||||
|
_overlayViewport.Camera = op;
|
||||||
|
}
|
||||||
|
op.FieldOfView = p.FieldOfView;
|
||||||
|
}
|
||||||
|
else if (src is OrthographicCamera o)
|
||||||
|
{
|
||||||
|
if (_overlayCamera is not OrthographicCamera oo)
|
||||||
|
{
|
||||||
|
oo = new OrthographicCamera();
|
||||||
|
_overlayCamera = oo;
|
||||||
|
_overlayViewport.Camera = oo;
|
||||||
|
}
|
||||||
|
oo.Width = o.Width;
|
||||||
|
}
|
||||||
|
if (_overlayCamera is null) return;
|
||||||
|
|
||||||
_overlayCamera.Position = src.Position;
|
_overlayCamera.Position = src.Position;
|
||||||
_overlayCamera.LookDirection = src.LookDirection;
|
_overlayCamera.LookDirection = src.LookDirection;
|
||||||
_overlayCamera.UpDirection = src.UpDirection;
|
_overlayCamera.UpDirection = src.UpDirection;
|
||||||
_overlayCamera.NearPlaneDistance = src.NearPlaneDistance;
|
_overlayCamera.NearPlaneDistance = src.NearPlaneDistance;
|
||||||
_overlayCamera.FarPlaneDistance = src.FarPlaneDistance;
|
_overlayCamera.FarPlaneDistance = src.FarPlaneDistance;
|
||||||
if (src is PerspectiveCamera p) _overlayCamera.FieldOfView = p.FieldOfView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnGizmoEnabledChanged(bool value) => UpdateGizmo();
|
partial void OnGizmoEnabledChanged(bool value) => UpdateGizmo();
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ public partial class MainViewModel
|
|||||||
{
|
{
|
||||||
if (_viewport is null) return;
|
if (_viewport is null) return;
|
||||||
var visibleRoots = Roots.Where(r => r.IsVisible).ToList();
|
var visibleRoots = Roots.Where(r => r.IsVisible).ToList();
|
||||||
if (visibleRoots.Count != 2)
|
if (visibleRoots.Count < 2)
|
||||||
{
|
{
|
||||||
StatusText = $"干涉檢查需要剛好 2 個可見檔案(目前 {visibleRoots.Count} 個)— 用樹面板勾選";
|
StatusText = $"干涉檢查需要至少 2 個可見檔案(目前 {visibleRoots.Count} 個)— 用樹面板勾選";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +33,27 @@ public partial class MainViewModel
|
|||||||
.Select(m => _faceMap[(GeometryModel3D)m].Mesh)
|
.Select(m => _faceMap[(GeometryModel3D)m].Mesh)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var a = MeshesOf(visibleRoots[0]);
|
// 網格快照(開跑後零件被改走也不會半途換料;v0.4.0 起運算期間指令已鎖,這裡是雙保險)
|
||||||
var b = MeshesOf(visibleRoots[1]);
|
var meshes = visibleRoots.ToDictionary(r => r, MeshesOf);
|
||||||
string nameA = visibleRoots[0].Name, nameB = visibleRoots[1].Name;
|
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
StatusText = $"干涉檢查中:{nameA} ⟷ {nameB} …";
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
int pairs = 0, intersecting = 0;
|
||||||
AddInterferenceResult(result, nameA, nameB);
|
for (int i = 0; i < visibleRoots.Count; i++)
|
||||||
|
for (int j = i + 1; j < visibleRoots.Count; j++)
|
||||||
|
{
|
||||||
|
pairs++;
|
||||||
|
string nameA = visibleRoots[i].Name, nameB = visibleRoots[j].Name;
|
||||||
|
StatusText = $"干涉檢查中(第 {pairs} 組):{nameA} ⟷ {nameB} …";
|
||||||
|
var a = meshes[visibleRoots[i]];
|
||||||
|
var b = meshes[visibleRoots[j]];
|
||||||
|
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
||||||
|
AddInterferenceResult(result, nameA, nameB);
|
||||||
|
if (result.Intersects) intersecting++;
|
||||||
|
}
|
||||||
|
if (pairs > 1)
|
||||||
|
StatusText = $"干涉檢查完成:{pairs} 組配對、{intersecting} 組相交";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,11 +38,116 @@ public partial class MainViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool sectionFlip;
|
private bool sectionFlip;
|
||||||
|
|
||||||
partial void OnSectionEnabledChanged(bool value) => ScheduleSection();
|
// ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)──
|
||||||
partial void OnSectionAxisIndexChanged(int value) => ScheduleSection();
|
private const int CustomAxisIndex = 3;
|
||||||
partial void OnSectionPositionChanged(double value) => ScheduleSection();
|
private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點)
|
||||||
|
private readonly List<Point3D> _sectionPicks = new();
|
||||||
|
private readonly List<Visual3D> _sectionPickOverlays = new();
|
||||||
|
|
||||||
|
partial void OnSectionEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
if (!value) CancelSectionPickIfActive(quiet: true);
|
||||||
|
ScheduleSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSectionAxisIndexChanged(int value)
|
||||||
|
{
|
||||||
|
if (value == CustomAxisIndex)
|
||||||
|
{
|
||||||
|
if (_customNormal is null)
|
||||||
|
StatusText = "3點剖面:請在模型上點 3 個點定義剖切平面(Esc 取消)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 離開自訂平面 → 丟棄舊平面(再選「3點」即重新定義)
|
||||||
|
_customNormal = null;
|
||||||
|
ClearSectionPicks();
|
||||||
|
}
|
||||||
|
ScheduleSection();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSectionPositionChanged(double value)
|
||||||
|
{
|
||||||
|
if (value < 0) { SectionPosition = 0; return; } // 位置數值輸入框防呆
|
||||||
|
if (value > 100) { SectionPosition = 100; return; }
|
||||||
|
ScheduleSection();
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnSectionFlipChanged(bool value) => ScheduleSection();
|
partial void OnSectionFlipChanged(bool value) => ScheduleSection();
|
||||||
|
|
||||||
|
/// <summary>3點剖面定義模式中的點擊(OnViewportClick 轉入)。回傳 true = 已消費此點擊。</summary>
|
||||||
|
public bool HandleSectionPlanePick(System.Windows.Point position)
|
||||||
|
{
|
||||||
|
if (!SectionEnabled || SectionAxisIndex != CustomAxisIndex || _customNormal is not null) return false;
|
||||||
|
if (_viewport is null) return true;
|
||||||
|
|
||||||
|
Point3D? hit = null;
|
||||||
|
foreach (var h in _viewport.Viewport.FindHits(position))
|
||||||
|
{
|
||||||
|
if (h.Model is null) continue;
|
||||||
|
if (_faceMap.ContainsKey(h.Model) || _mergedFaceRanges.ContainsKey(h.Model))
|
||||||
|
{
|
||||||
|
hit = h.Position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hit is null)
|
||||||
|
{
|
||||||
|
StatusText = $"3點剖面:未命中模型表面(已選 {_sectionPicks.Count}/3 點,Esc 取消)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_sectionPicks.Count == 2 &&
|
||||||
|
Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], hit.Value - _sectionPicks[0]).LengthSquared < 1e-12)
|
||||||
|
{
|
||||||
|
StatusText = "3點剖面:三點共線,請改選不共線的第 3 點";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sectionPicks.Add(hit.Value);
|
||||||
|
double markerR = Math.Clamp(SceneDiagonal() * 0.004, 0.01, 5.0);
|
||||||
|
var marker = new SphereVisual3D { Center = hit.Value, Radius = markerR, Fill = System.Windows.Media.Brushes.DodgerBlue };
|
||||||
|
_sectionPickOverlays.Add(marker);
|
||||||
|
_viewport.Children.Add(marker);
|
||||||
|
|
||||||
|
if (_sectionPicks.Count < 3)
|
||||||
|
{
|
||||||
|
StatusText = $"3點剖面:已選 {_sectionPicks.Count}/3 點";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3D n = Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], _sectionPicks[2] - _sectionPicks[0]);
|
||||||
|
ClearSectionPicks();
|
||||||
|
if (n.LengthSquared < 1e-12)
|
||||||
|
{
|
||||||
|
StatusText = "3點剖面:三點退化(共線),請重新點選";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
n.Normalize();
|
||||||
|
_customNormal = n;
|
||||||
|
StatusText = $"3點剖面:平面已定義(法向 {n.X:F2}, {n.Y:F2}, {n.Z:F2})— 用位置滑桿掃剖面";
|
||||||
|
ApplySection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>取消 3 點平面定義(Esc / 關剖面)。回傳 true = 有取消動作。</summary>
|
||||||
|
private bool CancelSectionPickIfActive(bool quiet = false)
|
||||||
|
{
|
||||||
|
bool picking = SectionEnabled && SectionAxisIndex == CustomAxisIndex && _customNormal is null;
|
||||||
|
if (_sectionPicks.Count == 0 && !picking) return false;
|
||||||
|
ClearSectionPicks();
|
||||||
|
if (SectionAxisIndex == CustomAxisIndex && _customNormal is null)
|
||||||
|
SectionAxisIndex = 0; // 回 X 軸
|
||||||
|
if (!quiet) StatusText = "已取消 3 點剖面定義";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSectionPicks()
|
||||||
|
{
|
||||||
|
foreach (Visual3D v in _sectionPickOverlays) _viewport?.Children.Remove(v);
|
||||||
|
_sectionPickOverlays.Clear();
|
||||||
|
_sectionPicks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
private void ScheduleSection()
|
private void ScheduleSection()
|
||||||
{
|
{
|
||||||
_sectionTimer.Stop();
|
_sectionTimer.Stop();
|
||||||
@@ -82,28 +187,40 @@ public partial class MainViewModel
|
|||||||
Rect3D bounds = UnionBounds();
|
Rect3D bounds = UnionBounds();
|
||||||
if (bounds.IsEmpty) return;
|
if (bounds.IsEmpty) return;
|
||||||
|
|
||||||
Vector3D axis = SectionAxisIndex switch
|
Vector3D axis;
|
||||||
|
if (SectionAxisIndex == CustomAxisIndex)
|
||||||
{
|
{
|
||||||
0 => new Vector3D(1, 0, 0),
|
if (_customNormal is null) return; // 平面尚未定義(等使用者點 3 點)
|
||||||
1 => new Vector3D(0, 1, 0),
|
axis = _customNormal.Value;
|
||||||
_ => new Vector3D(0, 0, 1),
|
}
|
||||||
};
|
else
|
||||||
|
{
|
||||||
|
axis = SectionAxisIndex switch
|
||||||
|
{
|
||||||
|
0 => new Vector3D(1, 0, 0),
|
||||||
|
1 => new Vector3D(0, 1, 0),
|
||||||
|
_ => new Vector3D(0, 0, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
double t = SectionPosition / 100.0;
|
double t = SectionPosition / 100.0;
|
||||||
Point3D min = bounds.Location;
|
Point3D min = bounds.Location;
|
||||||
var size = new Vector3D(bounds.SizeX, bounds.SizeY, bounds.SizeZ);
|
var size = new Vector3D(bounds.SizeX, bounds.SizeY, bounds.SizeZ);
|
||||||
double planeCoord = SectionAxisIndex switch
|
|
||||||
{
|
|
||||||
0 => min.X + size.X * t,
|
|
||||||
1 => min.Y + size.Y * t,
|
|
||||||
_ => min.Z + size.Z * t,
|
|
||||||
};
|
|
||||||
Point3D center = min + size / 2;
|
Point3D center = min + size / 2;
|
||||||
Point3D planePoint = SectionAxisIndex switch
|
// 平面位置:場景 AABB 8 個角投影到法向的 [dmin, dmax],滑桿 t 內插
|
||||||
|
//(軸向法向 = 與舊版逐軸計算等價;任意法向也成立)
|
||||||
|
double dmin = double.MaxValue, dmax = double.MinValue;
|
||||||
|
for (int ci = 0; ci < 8; ci++)
|
||||||
{
|
{
|
||||||
0 => new Point3D(planeCoord, center.Y, center.Z),
|
var corner = new Point3D(
|
||||||
1 => new Point3D(center.X, planeCoord, center.Z),
|
(ci & 1) == 0 ? min.X : min.X + size.X,
|
||||||
_ => new Point3D(center.X, center.Y, planeCoord),
|
(ci & 2) == 0 ? min.Y : min.Y + size.Y,
|
||||||
};
|
(ci & 4) == 0 ? min.Z : min.Z + size.Z);
|
||||||
|
double d = Vector3D.DotProduct((Vector3D)corner, axis);
|
||||||
|
if (d < dmin) dmin = d;
|
||||||
|
if (d > dmax) dmax = d;
|
||||||
|
}
|
||||||
|
double dStar = dmin + (dmax - dmin) * t;
|
||||||
|
Point3D planePoint = center + axis * (dStar - Vector3D.DotProduct((Vector3D)center, axis));
|
||||||
Vector3D normal = SectionFlip ? -axis : axis;
|
Vector3D normal = SectionFlip ? -axis : axis;
|
||||||
|
|
||||||
// UI 執行緒快照(原始 mesh 皆已 Freeze,可跨執行緒),背景平行裁切,回 UI 一次換上。
|
// UI 執行緒快照(原始 mesh 皆已 Freeze,可跨執行緒),背景平行裁切,回 UI 一次換上。
|
||||||
@@ -137,7 +254,8 @@ public partial class MainViewModel
|
|||||||
|
|
||||||
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
||||||
UpdateSectionPlaneVisual(planePoint, axis, size);
|
UpdateSectionPlaneVisual(planePoint, axis, size);
|
||||||
StatusText = $"剖面:{"XYZ"[SectionAxisIndex]} 軸 {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : "");
|
string axisName = SectionAxisIndex == CustomAxisIndex ? "自訂平面" : $"{"XYZ"[SectionAxisIndex]} 軸";
|
||||||
|
StatusText = $"剖面:{axisName} {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : "");
|
||||||
} while (_sectionReapply);
|
} while (_sectionReapply);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -155,8 +273,10 @@ public partial class MainViewModel
|
|||||||
{
|
{
|
||||||
0 => (new Vector3D(0, 1, 0), size.Y, size.Z),
|
0 => (new Vector3D(0, 1, 0), size.Y, size.Z),
|
||||||
1 => (new Vector3D(1, 0, 0), size.X, size.Z),
|
1 => (new Vector3D(1, 0, 0), size.X, size.Z),
|
||||||
_ => (new Vector3D(1, 0, 0), size.X, size.Y),
|
2 => (new Vector3D(1, 0, 0), size.X, size.Y),
|
||||||
|
_ => (AnyPerpendicular(axis), size.Length * 0.8, size.Length * 0.8), // 自訂平面
|
||||||
};
|
};
|
||||||
|
lenDir.Normalize();
|
||||||
_sectionPlaneVisual = new RectangleVisual3D
|
_sectionPlaneVisual = new RectangleVisual3D
|
||||||
{
|
{
|
||||||
Origin = planePoint,
|
Origin = planePoint,
|
||||||
@@ -168,4 +288,14 @@ public partial class MainViewModel
|
|||||||
};
|
};
|
||||||
_viewport!.Children.Add(_sectionPlaneVisual);
|
_viewport!.Children.Add(_sectionPlaneVisual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>取任一與 n 垂直的單位向量(自訂平面的指示矩形方向用)</summary>
|
||||||
|
private static Vector3D AnyPerpendicular(Vector3D n)
|
||||||
|
{
|
||||||
|
Vector3D v = Math.Abs(n.Z) < 0.9
|
||||||
|
? Vector3D.CrossProduct(n, new Vector3D(0, 0, 1))
|
||||||
|
: Vector3D.CrossProduct(n, new Vector3D(1, 0, 0));
|
||||||
|
v.Normalize();
|
||||||
|
return v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,16 +61,55 @@ public partial class MainViewModel : ObservableObject
|
|||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ImportCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ImportCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ImportRecentCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(RotateRootCommand))]
|
[NotifyCanExecuteChangedFor(nameof(RotateRootCommand))]
|
||||||
[NotifyCanExecuteChangedFor(nameof(CheckInterferenceCommand))]
|
[NotifyCanExecuteChangedFor(nameof(CheckInterferenceCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ExportStepFileCommand))]
|
||||||
private bool isBusy;
|
private bool isBusy;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool useInch;
|
private bool useInch;
|
||||||
|
|
||||||
|
/// <summary>樹面板名稱過濾字串(只影響樹顯示,不影響 3D 可見性)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? treeFilter;
|
||||||
|
|
||||||
|
partial void OnTreeFilterChanged(string? value)
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel root in Roots) root.ApplyFilter(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>最近開啟檔案(MRU,最多 10 筆;隨 settings.json 保存)</summary>
|
||||||
|
public ObservableCollection<string> RecentFiles { get; } = new();
|
||||||
|
|
||||||
public ObservableCollection<ModelNodeViewModel> Roots { get; } = new();
|
public ObservableCollection<ModelNodeViewModel> Roots { get; } = new();
|
||||||
public ObservableCollection<MeasurementResult> Measurements { get; } = new();
|
public ObservableCollection<MeasurementResult> Measurements { get; } = new();
|
||||||
|
|
||||||
|
// ─── 使用者設定(視窗由 MainWindow 處理;VM 管單位 + MRU)─────
|
||||||
|
|
||||||
|
public void LoadSettings(AppSettings s)
|
||||||
|
{
|
||||||
|
UseInch = s.UseInch;
|
||||||
|
RecentFiles.Clear();
|
||||||
|
foreach (string f in s.RecentFiles.Where(File.Exists).Take(SettingsService.MaxRecentFiles))
|
||||||
|
RecentFiles.Add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSettingsInto(AppSettings s)
|
||||||
|
{
|
||||||
|
s.UseInch = UseInch;
|
||||||
|
s.RecentFiles = RecentFiles.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TouchRecent(string path)
|
||||||
|
{
|
||||||
|
string full = Path.GetFullPath(path);
|
||||||
|
RecentFiles.Remove(full);
|
||||||
|
RecentFiles.Insert(0, full);
|
||||||
|
while (RecentFiles.Count > SettingsService.MaxRecentFiles)
|
||||||
|
RecentFiles.RemoveAt(RecentFiles.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
{
|
{
|
||||||
_sectionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
_sectionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
||||||
@@ -140,6 +179,10 @@ public partial class MainViewModel : ObservableObject
|
|||||||
await ImportFilesAsync(dlg.FileNames);
|
await ImportFilesAsync(dlg.FileNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>MRU 下拉點選(檔案可能已被移走 → ImportFilesAsync 會報匯入失敗)</summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsIdle))]
|
||||||
|
private Task ImportRecentAsync(string path) => ImportFilesAsync(new[] { path });
|
||||||
|
|
||||||
public async Task ImportFilesAsync(IEnumerable<string> paths)
|
public async Task ImportFilesAsync(IEnumerable<string> paths)
|
||||||
{
|
{
|
||||||
if (_viewport is null) return;
|
if (_viewport is null) return;
|
||||||
@@ -162,6 +205,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
ImportedFileData data = await Task.Run(() => _importService.Import(path));
|
ImportedFileData data = await Task.Run(() => _importService.Import(path));
|
||||||
BuildRoot(data);
|
BuildRoot(data);
|
||||||
|
TouchRecent(path);
|
||||||
ok++;
|
ok++;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -200,6 +244,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
root.ShowEdges = false;
|
root.ShowEdges = false;
|
||||||
|
|
||||||
Roots.Add(root);
|
Roots.Add(root);
|
||||||
|
if (!string.IsNullOrWhiteSpace(TreeFilter)) root.ApplyFilter(TreeFilter); // 新檔套用目前過濾
|
||||||
foreach (ModelNodeViewModel leaf in root.Leaves())
|
foreach (ModelNodeViewModel leaf in root.Leaves())
|
||||||
{
|
{
|
||||||
leaf.VisualStateChanged += _ => SyncLeafVisuals(root);
|
leaf.VisualStateChanged += _ => SyncLeafVisuals(root);
|
||||||
@@ -410,6 +455,44 @@ public partial class MainViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ZoomAll() => _viewport?.ZoomExtents(400);
|
private void ZoomAll() => _viewport?.ZoomExtents(400);
|
||||||
|
|
||||||
|
/// <summary>標準視圖(機構慣例 Z 向上):Iso / Front / Back / Top / Bottom / Left / Right</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void SetStandardView(string name)
|
||||||
|
{
|
||||||
|
if (_viewport?.Camera is not ProjectionCamera cam) return;
|
||||||
|
(Vector3D dir, Vector3D up) = name switch
|
||||||
|
{
|
||||||
|
"Front" => (new Vector3D(0, 1, 0), new Vector3D(0, 0, 1)), // 從 -Y 往 +Y 看
|
||||||
|
"Back" => (new Vector3D(0, -1, 0), new Vector3D(0, 0, 1)),
|
||||||
|
"Top" => (new Vector3D(0, 0, -1), new Vector3D(0, 1, 0)),
|
||||||
|
"Bottom" => (new Vector3D(0, 0, 1), new Vector3D(0, -1, 0)),
|
||||||
|
"Right" => (new Vector3D(-1, 0, 0), new Vector3D(0, 0, 1)),
|
||||||
|
"Left" => (new Vector3D(1, 0, 0), new Vector3D(0, 0, 1)),
|
||||||
|
_ => (new Vector3D(-1, -1, -1), new Vector3D(0, 0, 1)), // Iso
|
||||||
|
};
|
||||||
|
dir.Normalize();
|
||||||
|
Rect3D b = UnionBounds();
|
||||||
|
Point3D center = b.IsEmpty ? new Point3D()
|
||||||
|
: new Point3D(b.X + b.SizeX / 2, b.Y + b.SizeY / 2, b.Z + b.SizeZ / 2);
|
||||||
|
double dist = Math.Max(SceneDiagonal() * 1.8, 10);
|
||||||
|
cam.Position = center - dir * dist;
|
||||||
|
cam.LookDirection = dir * dist; // 目標點 = Position + LookDirection = 場景中心
|
||||||
|
cam.UpDirection = up;
|
||||||
|
_viewport.ZoomExtents(400);
|
||||||
|
StatusText = $"視圖:{name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>正交投影切換(平行投影,量尺寸不受透視變形;Helix 保持視角換相機型別)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool orthographicView;
|
||||||
|
|
||||||
|
partial void OnOrthographicViewChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_viewport is null) return;
|
||||||
|
_viewport.Orthographic = value;
|
||||||
|
StatusText = value ? "正交投影(平行投影,適合尺寸確認)" : "透視投影";
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void RemoveRoot(ModelNodeViewModel root)
|
private void RemoveRoot(ModelNodeViewModel root)
|
||||||
{
|
{
|
||||||
@@ -440,6 +523,92 @@ public partial class MainViewModel : ObservableObject
|
|||||||
StatusText = $"已移除:{root.Name}";
|
StatusText = $"已移除:{root.Name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 顯示控制 / 體積(樹右鍵選單)────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>只顯示此節點(其餘全部隱藏;「全部顯示」復原)</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void IsolateNode(ModelNodeViewModel node)
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, false);
|
||||||
|
SetVisibleRecursive(node, true);
|
||||||
|
StatusText = $"只顯示「{node.Name}」(樹右鍵 → 全部顯示 復原)";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void InvertVisibility()
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel l in Roots.SelectMany(r => r.Leaves()))
|
||||||
|
l.IsVisible = !l.IsVisible;
|
||||||
|
StatusText = "已反轉顯示";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowAll()
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, true);
|
||||||
|
StatusText = "已全部顯示";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>逐節點顯式遞迴設定可見性。不能只設 root(setter 同值不觸發 cascade,
|
||||||
|
/// 之前的隔離操作可能留下「父關子開」的混合狀態)</summary>
|
||||||
|
private static void SetVisibleRecursive(ModelNodeViewModel node, bool visible)
|
||||||
|
{
|
||||||
|
node.IsVisible = visible;
|
||||||
|
foreach (ModelNodeViewModel c in node.Children) SetVisibleRecursive(c, visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>體積/質心(網格 signed volume,封閉實體才可靠)→ 加進量測結果 + 質心標記</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void MeasureVolume(ModelNodeViewModel node)
|
||||||
|
{
|
||||||
|
var meshes = node.Leaves()
|
||||||
|
.Where(l => l.FacesContent is not null)
|
||||||
|
.SelectMany(l => l.FacesContent!.Children)
|
||||||
|
.OfType<GeometryModel3D>()
|
||||||
|
.Where(g => _faceMap.ContainsKey(g))
|
||||||
|
.Select(g => _faceMap[g].Mesh)
|
||||||
|
.ToList();
|
||||||
|
if (meshes.Count == 0)
|
||||||
|
{
|
||||||
|
StatusText = $"「{node.Name}」沒有可計算的面網格(線架構無體積)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(double vol, Point3D centroid, bool reliable) = MeasurementService.MeshVolume(meshes);
|
||||||
|
if (!reliable)
|
||||||
|
{
|
||||||
|
StatusText = $"「{node.Name}」非封閉網格(開放殼),無法可靠計算體積";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double diag = SceneDiagonal();
|
||||||
|
double markerR = Math.Clamp(diag * 0.004, 0.01, 5.0);
|
||||||
|
string label = NextLabel(MeasureMode.Volume, "V");
|
||||||
|
string name = node.Name;
|
||||||
|
Rect3D b = node.Bounds;
|
||||||
|
var m = new MeasurementResult
|
||||||
|
{
|
||||||
|
Kind = MeasureMode.Volume,
|
||||||
|
TitleFor = u => $"{label} {name} V ≈ {Units.V(vol, u)}",
|
||||||
|
DetailFor = u => $"體積 ≈ {Units.V(vol, u)}(網格近似)\n質心 {Units.P(centroid, u)}\n" +
|
||||||
|
$"外形(AABB){Units.C(b.SizeX, u)} × {Units.C(b.SizeY, u)} × {Units.C(b.SizeZ, u)} " +
|
||||||
|
(u == UnitSystem.Millimeter ? "mm" : "in"),
|
||||||
|
};
|
||||||
|
m.Overlays.Add(new SphereVisual3D { Center = centroid, Radius = markerR, Fill = Brushes.OrangeRed });
|
||||||
|
var volLabel = new BillboardTextVisual3D
|
||||||
|
{
|
||||||
|
Position = centroid + new Vector3D(0, 0, markerR * 2),
|
||||||
|
Text = $"{label} {Units.V(vol, UnitSystem.Millimeter)}",
|
||||||
|
Foreground = Brushes.Black,
|
||||||
|
Background = new SolidColorBrush(Color.FromArgb(200, 255, 255, 210)),
|
||||||
|
Padding = new Thickness(4, 2, 4, 2),
|
||||||
|
FontSize = 14,
|
||||||
|
};
|
||||||
|
m.Overlays.Add(volLabel);
|
||||||
|
m.DynamicLabels.Add((volLabel, u => $"{label} {Units.V(vol, u)}"));
|
||||||
|
AddMeasurement(m);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 量測 ────────────────────────────────────────────────────
|
// ─── 量測 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
partial void OnCurrentModeChanged(MeasureMode value)
|
partial void OnCurrentModeChanged(MeasureMode value)
|
||||||
@@ -470,10 +639,52 @@ public partial class MainViewModel : ObservableObject
|
|||||||
StatusText = value ? "顯示單位:inch" : "顯示單位:mm";
|
StatusText = value ? "顯示單位:inch" : "顯示單位:mm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Esc(MainWindow 轉發):先取消進行中的多段量測(保留模式),再按退回瀏覽模式;再退操作器</summary>
|
||||||
|
public void OnEscape()
|
||||||
|
{
|
||||||
|
if (CancelSectionPickIfActive()) return; // 3點剖面定義中 → 先取消定義
|
||||||
|
bool hadPending = _pendingPoint is not null || _pendingDirection is not null ||
|
||||||
|
_pendingFace is not null || _pendingAlign is not null || _align3.Count > 0;
|
||||||
|
if (hadPending)
|
||||||
|
{
|
||||||
|
CancelPending();
|
||||||
|
StatusText = "已取消目前量測起點(再按 Esc 退出量測模式)";
|
||||||
|
}
|
||||||
|
else if (CurrentMode != MeasureMode.None)
|
||||||
|
{
|
||||||
|
CurrentMode = MeasureMode.None;
|
||||||
|
}
|
||||||
|
else if (GizmoEnabled)
|
||||||
|
{
|
||||||
|
GizmoEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>量測模式快速鍵(MainWindow 轉發;無修飾鍵)。同鍵再按 = 退回瀏覽。回傳 false 表示非快速鍵。</summary>
|
||||||
|
public bool OnHotKey(System.Windows.Input.Key key)
|
||||||
|
{
|
||||||
|
MeasureMode? m = key switch
|
||||||
|
{
|
||||||
|
System.Windows.Input.Key.P => MeasureMode.Point,
|
||||||
|
System.Windows.Input.Key.D => MeasureMode.Distance,
|
||||||
|
System.Windows.Input.Key.E => MeasureMode.Edge,
|
||||||
|
System.Windows.Input.Key.F => MeasureMode.Face,
|
||||||
|
System.Windows.Input.Key.C => MeasureMode.Circle,
|
||||||
|
System.Windows.Input.Key.A => MeasureMode.Angle,
|
||||||
|
System.Windows.Input.Key.M => MeasureMode.FaceDistance,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (m is null) return false;
|
||||||
|
CurrentMode = CurrentMode == m ? MeasureMode.None : m.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>MainWindow 滑鼠左鍵點擊轉發進來</summary>
|
/// <summary>MainWindow 滑鼠左鍵點擊轉發進來</summary>
|
||||||
public void OnViewportClick(Point position)
|
public void OnViewportClick(Point position)
|
||||||
{
|
{
|
||||||
if (_viewport is null || IsBusy || CurrentMode is MeasureMode.None or MeasureMode.Drag) return;
|
if (_viewport is null || IsBusy) return;
|
||||||
|
if (HandleSectionPlanePick(position)) return; // 3點剖面定義模式優先吃點擊(v0.5.0)
|
||||||
|
if (CurrentMode is MeasureMode.None or MeasureMode.Drag) return;
|
||||||
|
|
||||||
var hits = _viewport.Viewport.FindHits(position);
|
var hits = _viewport.Viewport.FindHits(position);
|
||||||
FaceInfo? fi = null;
|
FaceInfo? fi = null;
|
||||||
|
|||||||
@@ -57,8 +57,24 @@ public partial class ModelNodeViewModel : ObservableObject
|
|||||||
public int FaceCount { get; init; }
|
public int FaceCount { get; init; }
|
||||||
public int TriangleCount { get; init; }
|
public int TriangleCount { get; init; }
|
||||||
|
|
||||||
/// <summary>世界座標邊界(零件平移後同步位移)</summary>
|
/// <summary>世界座標邊界(零件平移/旋轉後由 RecomputeBounds 更新;連動 Stats/Tooltip 的外形尺寸)</summary>
|
||||||
public Rect3D Bounds { get; set; }
|
public Rect3D Bounds
|
||||||
|
{
|
||||||
|
get => _bounds;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_bounds == value) return;
|
||||||
|
_bounds = value;
|
||||||
|
OnPropertyChanged(nameof(Stats));
|
||||||
|
OnPropertyChanged(nameof(ToolTipText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private Rect3D _bounds = Rect3D.Empty;
|
||||||
|
|
||||||
|
/// <summary>外形尺寸 L×W×H(AABB,mm)</summary>
|
||||||
|
public string SizeText => Bounds.IsEmpty
|
||||||
|
? ""
|
||||||
|
: $"{Bounds.SizeX:F1} × {Bounds.SizeY:F1} × {Bounds.SizeZ:F1} mm";
|
||||||
|
|
||||||
/// <summary>來源 B-rep 物件(零件平移時整體 Modify,避免共用邊被重複位移)</summary>
|
/// <summary>來源 B-rep 物件(零件平移時整體 Modify,避免共用邊被重複位移)</summary>
|
||||||
public List<CADability.GeoObject.IGeoObject> SourceGeos { get; } = new();
|
public List<CADability.GeoObject.IGeoObject> SourceGeos { get; } = new();
|
||||||
@@ -67,10 +83,32 @@ public partial class ModelNodeViewModel : ObservableObject
|
|||||||
public bool HasBrep { get; init; } = true;
|
public bool HasBrep { get; init; } = true;
|
||||||
|
|
||||||
public string Stats =>
|
public string Stats =>
|
||||||
$"{SolidCount} 實體 · {FaceCount} 面 · {TriangleCount:N0} △" + (HasBrep ? "" : " · 無B-rep");
|
$"{SolidCount} 實體 · {FaceCount} 面 · {TriangleCount:N0} △" + (HasBrep ? "" : " · 無B-rep") +
|
||||||
|
(Bounds.IsEmpty ? "" : $"\n外形 {SizeText}");
|
||||||
|
|
||||||
public string ToolTipText => (FilePath is not null ? FilePath + "\n" : "") + Stats;
|
public string ToolTipText => (FilePath is not null ? FilePath + "\n" : "") + Stats;
|
||||||
|
|
||||||
|
// ── 樹搜尋/過濾(只影響樹面板顯示,不影響 3D 可見性)──
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isFilterVisible = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 套用名稱過濾:符合者 + 其祖先 + 其子樹 顯示。空字串 = 全顯。
|
||||||
|
/// 過濾中有符合的子孫時自動展開。回傳此節點是否顯示。
|
||||||
|
/// </summary>
|
||||||
|
public bool ApplyFilter(string? filter, bool ancestorMatched = false)
|
||||||
|
{
|
||||||
|
bool empty = string.IsNullOrWhiteSpace(filter);
|
||||||
|
bool selfMatch = !empty && Name.Contains(filter!, StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool anyChild = false;
|
||||||
|
foreach (ModelNodeViewModel c in Children)
|
||||||
|
anyChild |= c.ApplyFilter(filter, ancestorMatched || selfMatch);
|
||||||
|
IsFilterVisible = empty || selfMatch || ancestorMatched || anyChild;
|
||||||
|
if (!empty && anyChild) IsExpanded = true;
|
||||||
|
return IsFilterVisible;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 視覺物件 ──
|
// ── 視覺物件 ──
|
||||||
public ModelVisual3D? BodyVisual { get; set; } // leaf:面網格容器(內容在兩種模式間切換)
|
public ModelVisual3D? BodyVisual { get; set; } // leaf:面網格容器(內容在兩種模式間切換)
|
||||||
public Model3DGroup? FacesContent { get; set; } // leaf:逐面 GeometryModel3D(量測拾取用)
|
public Model3DGroup? FacesContent { get; set; } // leaf:逐面 GeometryModel3D(量測拾取用)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using CADability;
|
||||||
|
using CADability.GeoObject;
|
||||||
|
using STPViewer.Services;
|
||||||
|
using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
|
||||||
|
|
||||||
|
namespace SmokeTest;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// STEP 匯出往返驗證(v0.5.0 匯出 STEP 功能的無 UI 驗證):
|
||||||
|
/// 匯入來源 STEP → 走與 MainViewModel.ExportStepFile 相同路徑寫出新檔 → 重新匯入比對實體數。
|
||||||
|
/// </summary>
|
||||||
|
public static class ExportTest
|
||||||
|
{
|
||||||
|
public static int Run(string sourcePath, string outPath)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"=== ExportTest: {IOPath.GetFileName(sourcePath)} → {IOPath.GetFileName(outPath)} ===");
|
||||||
|
ImportedFileData data = new StepImportService().Import(sourcePath);
|
||||||
|
var geos = Collect(data.Root).Where(g => g is Solid or Shell).ToList();
|
||||||
|
Console.WriteLine($" 來源 Solid/Shell 物件 = {geos.Count}(Solids = {data.Root.SolidCount})");
|
||||||
|
|
||||||
|
Project project = Project.CreateSimpleProject();
|
||||||
|
Model model = project.GetActiveModel();
|
||||||
|
foreach (IGeoObject g in geos) model.Add(g);
|
||||||
|
new ExportStep().WriteToFile(outPath, project);
|
||||||
|
var fi = new FileInfo(outPath);
|
||||||
|
Console.WriteLine($" 已寫出({fi.Length:N0} bytes)");
|
||||||
|
if (fi.Length < 100)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ExportTest: FAIL(輸出檔近乎空白)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportedFileData back = new StepImportService().Import(outPath);
|
||||||
|
Console.WriteLine($" 回讀 Solids = {back.Root.SolidCount}, Faces = {back.Root.FaceCount}");
|
||||||
|
bool ok = back.Root.SolidCount == data.Root.SolidCount && back.Root.FaceCount > 0;
|
||||||
|
Console.WriteLine(ok
|
||||||
|
? "ExportTest: PASS(實體數一致)"
|
||||||
|
: $"ExportTest: FAIL(來源 Solids {data.Root.SolidCount} ↔ 回讀 {back.Root.SolidCount})");
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<IGeoObject> Collect(ImportedNode n)
|
||||||
|
{
|
||||||
|
foreach (IGeoObject g in n.SourceGeos) yield return g;
|
||||||
|
foreach (ImportedNode c in n.Children)
|
||||||
|
foreach (IGeoObject g in Collect(c))
|
||||||
|
yield return g;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,9 @@ if (args[0] == "--align-test")
|
|||||||
if (args[0] == "--bbox")
|
if (args[0] == "--bbox")
|
||||||
return SmokeTest.BBoxDump.Run(args[1]);
|
return SmokeTest.BBoxDump.Run(args[1]);
|
||||||
|
|
||||||
|
if (args[0] == "--export-test")
|
||||||
|
return SmokeTest.ExportTest.Run(args[1], args[2]) == 0 ? 0 : 2;
|
||||||
|
|
||||||
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
|
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user