Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2caa95a69d | ||
|
|
fb46da29cf | ||
|
|
c65fc4e67f | ||
|
|
6d48d7f852 | ||
|
|
39ed6d7bba | ||
|
|
89ed1270a8 | ||
|
|
060ca4ecd0 | ||
|
|
45b27d51cb | ||
|
|
3d124782d5 | ||
|
|
e4485667e2 | ||
|
|
f5ccc13e9c | ||
|
|
38eb6d92d9 | ||
|
|
0093e92b26 | ||
|
|
9b40d4c36d | ||
|
|
c657417d6b |
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
publish/
|
publish/
|
||||||
Publish/ # publish-winapp 打包產物(大小寫明確,Linux/CI 也忽略)
|
Publish/ # publish-winapp output on case-sensitive systems
|
||||||
*.user
|
*.user
|
||||||
.vs/
|
.vs/
|
||||||
|
|
||||||
|
|||||||
+153
-51
@@ -12,12 +12,16 @@ 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 檔**、
|
||||||
|
**STL 網格檔**(參數對話框:合併/每檔、binary/ASCII、mm/inch/自訂縮放、精度,確認再匯出)
|
||||||
|
- 單機離線執行,無網路相依;視窗/單位/最近檔案自動保存(settings.json)
|
||||||
|
|
||||||
使用者:單一桌面使用者(無登入 / 角色系統)。
|
使用者:單一桌面使用者(無登入 / 角色系統)。
|
||||||
|
|
||||||
@@ -96,19 +100,30 @@ STPViewer/
|
|||||||
├── STPViewer.csproj # net8.0-windows, UseWPF
|
├── STPViewer.csproj # net8.0-windows, UseWPF
|
||||||
├── App.xaml / App.xaml.cs # 全域例外處理(log + 訊息框存活;%LOCALAPPDATA%\STPViewer\error.log)
|
├── App.xaml / App.xaml.cs # 全域例外處理(log + 訊息框存活;%LOCALAPPDATA%\STPViewer\error.log)
|
||||||
├── MainWindow.xaml / .cs # 版面 + 滑鼠拾取事件
|
├── MainWindow.xaml / .cs # 版面 + 滑鼠拾取事件
|
||||||
|
├── i18n/ # 語言包 en / zh_tw / zh_cn.json(輸出到 exe 旁;啟動時掃描,免重編)
|
||||||
|
├── Localization/
|
||||||
|
│ └── LocExtension.cs # XAML {loc:Loc Key} → 綁定 LocalizationService[Key](切語言即時更新)
|
||||||
│
|
│
|
||||||
├── Models/
|
├── Models/
|
||||||
|
│ ├── LanguageDefinition.cs # 一個語言包(Id = 檔名、Key = 顯示名、Data = 翻譯表)
|
||||||
│ ├── 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 格式化
|
||||||
│
|
│
|
||||||
|
├── StlExportDialog.xaml / .cs # STL 匯出參數對話框(範圍/格式/單位/精度 + 預估,確認再匯出)
|
||||||
|
├── AboutDialog.xaml / .cs # 關於視窗(程式/開發者/技術棧 + 使用者版 changelog)
|
||||||
|
│
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
||||||
│ ├── MeasurementService.cs # 點/線/面/圓/角度/面距 幾何計算
|
│ ├── StlMeshReader.cs # STL 直接解析(binary/ASCII,不經 CADability;v0.7.1)
|
||||||
|
│ ├── LocalizationService.cs # 語言包載入、目前語言、[key] 查詢(缺字回退英文)、LanguageChanged
|
||||||
|
│ ├── 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)
|
||||||
|
│ └── StlExportService.cs # STL 匯出:binary/ASCII 寫檔 + B-rep 重新三角化(精細)
|
||||||
│
|
│
|
||||||
└── ViewModels/ # MainViewModel 為 partial class,依職責分檔
|
└── ViewModels/ # MainViewModel 為 partial class,依職責分檔
|
||||||
├── MainViewModel.cs # 核心:匯入、裝配樹、量測、剛體變換、邊線、單位
|
├── MainViewModel.cs # 核心:匯入、裝配樹、量測、剛體變換、邊線、單位
|
||||||
@@ -116,7 +131,10 @@ STPViewer/
|
|||||||
├── MainViewModel.Gizmo.cs # 三軸操作器 + 疊圖層相機
|
├── MainViewModel.Gizmo.cs # 三軸操作器 + 疊圖層相機
|
||||||
├── MainViewModel.Section.cs # 剖面(v0.4.0 起背景平行裁切)
|
├── MainViewModel.Section.cs # 剖面(v0.4.0 起背景平行裁切)
|
||||||
├── MainViewModel.Interference.cs # 干涉檢查指令
|
├── MainViewModel.Interference.cs # 干涉檢查指令
|
||||||
├── MainViewModel.Export.cs # CSV / 截圖匯出
|
├── MainViewModel.Export.cs # CSV / 截圖 / STEP / STL 匯出
|
||||||
|
├── MainViewModel.Localization.cs # 語言切換指令 + 切換後刷新量測/樹/狀態列文字
|
||||||
|
├── QuickBarViewModel.cs # 快速列按鈕註冊表(key/文字 key/預設值)
|
||||||
|
├── StlExportViewModel.cs # STL 匯出對話框 VM(選項 + 即時摘要)
|
||||||
└── ModelNodeViewModel.cs # 裝配樹節點(可見性/顏色 cascade)
|
└── ModelNodeViewModel.cs # 裝配樹節點(可見性/顏色 cascade)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -127,29 +145,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 +201,16 @@ 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 / 📐 STL | UTF-8 BOM CSV;2x PNG;可見檔案目前位置寫成**新** STEP 檔(對齊結果交接 CAD);STL 先開參數對話框(範圍/格式/單位/精度 + 三角形數與大小預估)確認再匯出 |
|
||||||
| 視圖 | 滑鼠右鍵旋轉/滾輪縮放/中鍵平移(Helix 預設)、ViewCube | |
|
| 視圖 | 滑鼠右鍵旋轉/滾輪縮放/中鍵平移(Helix 預設)、ViewCube、工具列 等角/前/上/右、正交⇄透視 | |
|
||||||
|
| 快速鍵 | P 點 / D 距離 / E 邊 / F 面 / C 圓 / A 角度 / M 面距;同鍵再按=退出;Esc=取消進行中量測→退出模式 | |
|
||||||
|
| 樹操作 | 搜尋框過濾節點名稱;右鍵:只顯示此節點 / 反轉顯示 / 全部顯示 / 量體積質心 | tooltip 含外形尺寸 L×W×H |
|
||||||
|
| 體積/質心 | 樹節點右鍵「⚖ 量體積/質心」 | signed volume(封閉實體)+ 質心標記 + AABB 外形尺寸 |
|
||||||
|
| 語言 | 視圖 → 語言 / 快速列下拉:English、繁體中文、简体中文(v0.8.0) | 即時切換(含既有量測文字)並記住;`i18n/*.json` 加檔即新增語言,缺字回退英文 |
|
||||||
|
| 設定/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 +417,113 @@ 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 警告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Phases — 第七輪(STL 匯出 v0.6.0,全部完成)
|
||||||
|
|
||||||
|
### Phase 15 — STL 匯出(工作量:M)
|
||||||
|
|
||||||
|
- [x] `StlExportService` — binary(80B header + placeholder 補三角形數)/ ASCII 寫檔,
|
||||||
|
退化(零面積)三角形濾除、逐三角形自算法向量;`Tessellate` 由 B-rep 重新三角化
|
||||||
|
- [x] 參數對話框(`StlExportDialog` + `StlExportViewModel`)— 範圍(合併單檔/每檔一個)、
|
||||||
|
格式(binary/ASCII)、單位縮放(mm / inch ÷25.4 / 自訂)、網格精度(目前網格/精細)、
|
||||||
|
即時摘要(檔案數/三角形數/預估大小),**確認才選路徑匯出**
|
||||||
|
- [x] 背景執行緒寫檔 — UI 先快照 frozen mesh(`GeometryModel3D` 是 DispatcherObject
|
||||||
|
不可跨執行緒,v0.6.0 修正),背景只碰 frozen `MeshGeometry3D` + CADability B-rep
|
||||||
|
- [x] SmokeTest `--stl-export-test [file.stp]`:binary 往返(含 inch 縮放)、ASCII 結構、
|
||||||
|
退化三角形濾除、真檔精細重算三角形數 > 匯入預設
|
||||||
|
|
||||||
|
**CADability 三角化兩個實測特性(設計依據):**
|
||||||
|
1. `GetTriangulation` 對「比快取粗」的精度要求直接回傳既有較細快取 → 「較粗」選項無效,誠實不提供
|
||||||
|
2. 三角形數對精度**非單調**(0.4× 重算反而比 1× 少 — 重算網格較有效率),
|
||||||
|
要明顯更細需 ≤0.15×(test.stp:1× = 30,784 → 0.15× = 35,228)→ 精細係數定 0.15
|
||||||
|
|
||||||
|
**驗收:** StlExportTest 5 項全過;既有 ClipTest/AlignTest/InterferenceTest 回歸全過;建置 0 警告
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Phases — 第八輪(UI 重整 v0.7.0)
|
||||||
|
|
||||||
|
### Phase 16 — 選單列 + 可自訂快速列(工作量:M)
|
||||||
|
|
||||||
|
**問題:** 功能累積後單一工具列 30+ 顆按鈕擠在一排,視窗不夠寬就溢出到「▾」,難找難按。
|
||||||
|
|
||||||
|
- [x] **選單列**(Menu):全部功能分類下拉 — 檔案(匯入/MRU/四種匯出)、視圖(全覽/標準視圖/正交/單位)、
|
||||||
|
量測(7 種模式 checkable + 快速鍵提示 + 清除)、變換(對齊/三點/拖曳/操作器/旋轉XYZ)、
|
||||||
|
工具(干涉/剖面)、快速列(自訂按鈕勾選 + 恢復預設)
|
||||||
|
- [x] **快速工具列**(`QuickBarViewModel`):只顯示使用者勾選的常用按鈕;
|
||||||
|
註冊表定義全部按鈕(key/選單名/預設值),XAML 以索引子綁定
|
||||||
|
`Visibility="{Binding QuickBar[Key].IsChecked, Converter=BoolToVis}"`(按鈕本體與綁定保持靜態)
|
||||||
|
- [x] 預設常用:匯入、最近、全覽、等角、正交、點、距離、圓、角度、干涉、剖面、清除、mm⇄in;
|
||||||
|
其餘(邊/面/面距/對齊/三點/旋轉/拖曳/操作器/四種匯出/前上右視圖)預設收進選單
|
||||||
|
- [x] 勾選清單隨 settings.json(`QuickBarKeys`)保存;null/新按鈕用註冊表預設值
|
||||||
|
- [x] **剖面參數列**獨立成第三列,只在 `SectionEnabled` 時顯示(含「✕ 關閉」);
|
||||||
|
平常整條隱藏,不再常駐佔位
|
||||||
|
- [x] **關於視窗**(`AboutDialog`,選單「說明 → 關於 STPViewer」)— 程式資訊/開發者/技術棧 +
|
||||||
|
給一般使用者看的版本紀錄(樣式參考 ETTerms AboutView);發新版在 Changelog 陣列頂端加一筆
|
||||||
|
|
||||||
|
**驗收:** 選單所有功能可用(checkable 與工具列 toggle 同步);快速列勾選即時增減、重開保留;
|
||||||
|
既有快速鍵 P/D/E/F/C/A/M + Esc 不受影響
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Phases — 第九輪(多語系 v0.8.0)
|
||||||
|
|
||||||
|
### Phase 17 — 執行期語言包 + 即時切換(工作量:L;外部貢獻 PR #1 by @CarmJos)
|
||||||
|
|
||||||
|
**問題:** 介面字串全部寫死繁中,非中文使用者無法使用;新增語言要改程式重編。
|
||||||
|
|
||||||
|
- [x] **語言包**:`i18n/*.json`(`key` = 選單顯示名、`data` = key → 譯文)複製到 exe 旁;
|
||||||
|
啟動時 `LocalizationService` 掃描,檔名即語言 Id(`zh_cn.json` → `zh_cn`);壞檔/重複 Id 略過記 Trace
|
||||||
|
- [x] **查詢與回退**:`LocalizationService[key]` → 目前語言 → 英文 → key 本身;`Format` 格式錯誤時回退英文格式
|
||||||
|
- [x] **XAML**:`{loc:Loc Key}`(`LocExtension` = 綁 `LocalizationService.Instance[Key]`,`Item[]` 變更即時刷新)
|
||||||
|
- [x] **程式內文字**:狀態列 `SetStatus(() => Get/Format(...))`、量測/樹節點文字延後產生,
|
||||||
|
`LanguageChanged` → `RefreshLocalizedText()` 重算(與 mm⇄inch 同一套延後產生模式)
|
||||||
|
- [x] **初始語言**:settings.json `Language`,null → 依系統 UI 語系(zh-Hant/TW/HK/MO → zh_tw,其他 zh → zh_cn)
|
||||||
|
- [x] 關於視窗版本紀錄改為 key(`Ui.About.ReleaseXYZ.Title/ChangeN`),三語各一份
|
||||||
|
- [x] SmokeTest `--localization-test`:語系解析(zh-Hans/Hant/TW…)、缺字/格式錯回退英文、壞檔容錯、
|
||||||
|
內建語言包格式參數一致、VM 切語言後既有量測/3D 標籤/快速列即時更新
|
||||||
|
|
||||||
|
**驗收:** 三語切換即時生效(含已存在的量測結果與狀態列)、重開保留;刪掉某語言檔中的 key 時顯示英文不崩潰
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 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/STL 匯出(STL 有參數對話框)、設定與 MRU 保存。
|
||||||
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||||
|
|
||||||
## 技術棧
|
## 技術棧
|
||||||
@@ -41,8 +42,36 @@ 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**
|
||||||
|
(樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理
|
||||||
|
- UI 三層(v0.7.0):**選單列**(全部功能,分類下拉)+ **快速列**(`QuickBarViewModel` 註冊表,
|
||||||
|
使用者勾選常用按鈕,XAML 索引子綁定 `QuickBar[Key].IsChecked` 控 Visibility,按鈕本體維持靜態綁定)+
|
||||||
|
**剖面參數列**(只在 SectionEnabled 顯示)。新增功能要四處都接:選單分類、快速列註冊表
|
||||||
|
(含預設值)+ 對應 XAML 按鈕、**三個語言包的文字 key**;勾選清單存 settings.json `QuickBarKeys`(null = 預設)
|
||||||
|
- 使用者設定 `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` 往返驗證
|
||||||
|
- 匯出 STL(`StlExportService` + 參數對話框):**背景寫檔前必須在 UI 執行緒快照 mesh** —
|
||||||
|
`GeometryModel3D`/`Model3DGroup` 是 DispatcherObject 跨執行緒會炸(v0.6.0 踩過),
|
||||||
|
背景只能碰 frozen `MeshGeometry3D` 與 CADability B-rep。精度只有「目前網格/精細」兩檔:
|
||||||
|
CADability `GetTriangulation` 對比快取粗的精度回傳既有快取(粗化無效),且三角形數對精度
|
||||||
|
**非單調**(0.4× 反而比 1× 少),精細 = 0.15×(匯入 clamp 後再乘,保證嚴格更細)。
|
||||||
|
改動要跑 `SmokeTest --stl-export-test For_AI/test.stp`
|
||||||
|
- 3點剖面(SectionAxisIndex==3):`_customNormal` null = 拾取中(`HandleSectionPlanePick` 在
|
||||||
|
OnViewportClick 最前面攔點擊);換軸/關剖面/Esc 會重置。平面位置一律用「AABB 8 角投影到法向」內插,
|
||||||
|
軸向與任意法向共用同一段程式,不要改回逐軸特化
|
||||||
- 量測文字一律 `Func<UnitSystem,string>` 延後產生(mm⇄inch 即時切換);內部數值永遠存 mm
|
- 量測文字一律 `Func<UnitSystem,string>` 延後產生(mm⇄inch 即時切換);內部數值永遠存 mm
|
||||||
|
- 🌐 **多語系(v0.8.0,PR #1 by @CarmJos)**:介面文字**一律走 key,不可寫死字串**。語言包 `src/STPViewer/i18n/{en,zh_tw,zh_cn}.json`
|
||||||
|
(`key` = 選單顯示名、`data` = key → 譯文;建置時複製到 exe 旁,啟動掃描,加檔即新增語言免重編)。
|
||||||
|
XAML 用 `{loc:Loc Ui.Xxx}`;程式用 `LocalizationService.Get(key)` / `Format(key, args)`;
|
||||||
|
狀態列用 `SetStatus(() => Get(...))`(延後產生,切語言時 `RefreshLocalizedText()` 重算 — 直接設 `StatusText` 切語言不會更新)。
|
||||||
|
新增文字 key **三個 json 都要加**(`en.json` 是 key 清單正本;缺字執行期回退英文、再缺顯示 key 本身)。
|
||||||
|
key 命名:`Ui.Main.*`(主視窗)/ `Ui.StlExport.*` / `Ui.About.*` / `QuickBar.*` / `Runtime.*`(狀態列訊息)/ `App.*`。
|
||||||
|
選用語言存 settings.json `Language`(null = 依系統 UI 語系)。**程式碼註解維持中文**(設計理由與「不要改回 X」都寫在註解,勿精簡掉)
|
||||||
- 裝配樹節點(`ModelNodeViewModel`)的可見性/邊線/顏色向下 cascade
|
- 裝配樹節點(`ModelNodeViewModel`)的可見性/邊線/顏色向下 cascade
|
||||||
- 剖面只換 `GeometryModel3D.Geometry`(`FaceInfo.Mesh` 保留原始 frozen mesh 供還原與量測)
|
- 剖面只換 `GeometryModel3D.Geometry`(`FaceInfo.Mesh` 保留原始 frozen mesh 供還原與量測)
|
||||||
- ⚡ 剖面裁切是**背景平行**(v0.4.0):`ApplySection` 在 UI 快照 (model, frozen mesh) → `Task.Run`+`Parallel.For`
|
- ⚡ 剖面裁切是**背景平行**(v0.4.0):`ApplySection` 在 UI 快照 (model, frozen mesh) → `Task.Run`+`Parallel.For`
|
||||||
@@ -75,6 +104,10 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
放開事件靠 overlay 的 `AddHandler(MouseLeftButtonUp, handledEventsToo:true)`(manipulator 會標 handled),`_gizmoBakePending` 防同次重複烘焙
|
放開事件靠 overlay 的 `AddHandler(MouseLeftButtonUp, handledEventsToo:true)`(manipulator 會標 handled),`_gizmoBakePending` 防同次重複烘焙
|
||||||
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
|
- 發新版:升 csproj `<Version>` 之外,記得在 `AboutDialog.xaml.cs` 的 `Changelog` 陣列**頂端**加一筆
|
||||||
|
(口吻寫給一般使用者:「你會感覺到什麼」,不是技術 changelog)。v0.8.0 起內容是 key
|
||||||
|
(`Ui.About.ReleaseXYZ.Title` / `.ChangeN`),**三個語言包都要寫**。
|
||||||
|
⚠️ WPF **頂層 MenuItem 直接掛 Click 不會觸發**(實測滑鼠/鍵盤都沒反應),要開視窗的入口一律放子選單(如 說明 → 關於)
|
||||||
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
||||||
|
|
||||||
## 注意事項 / 已知限制
|
## 注意事項 / 已知限制
|
||||||
@@ -94,11 +127,23 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
`_edgesSuspended` 為真時 `RefreshRootEdges` 不把線掛回。轉動/縮放/平移時不付邊線重建成本。
|
`_edgesSuspended` 為真時 `RefreshRootEdges` 不把線掛回。轉動/縮放/平移時不付邊線重建成本。
|
||||||
**只訂 `Camera.Changed` 不夠**:`Attach` 在建構式呼叫,相機實例若被 Helix 換掉訂閱會孤兒化 → 暫停永不觸發;故加訂控制項層級事件保底(重複觸發 `OnCameraMoved` 無害,有 guard)
|
**只訂 `Camera.Changed` 不夠**:`Attach` 在建構式呼叫,相機實例若被 Helix 換掉訂閱會孤兒化 → 暫停永不觸發;故加訂控制項層級事件保底(重複觸發 `OnCameraMoved` 無害,有 guard)
|
||||||
- CADability `ImportStep` 對少數 AP242 檔案支援不完整;匯入失敗要 catch 顯示訊息,不可閃退
|
- CADability `ImportStep` 對少數 AP242 檔案支援不完整;匯入失敗要 catch 顯示訊息,不可閃退
|
||||||
|
- ⚡ **STL 直接解析(`StlMeshReader`,v0.7.1),不經過 CADability `ImportSTL`**。CADability 會為 STL 每個三角形
|
||||||
|
建一個 B-rep Face(含 Edge/Vertex 拓樸)→ 百萬三角形要數十分鐘且吃光記憶體(實測 100k 三角形:CADability ~41 s
|
||||||
|
vs 直接解析 ~11 ms;75 MB/150 萬三角形整條匯入管線 ~0.77 s)。binary/ASCII 皆自解;binary 判定用
|
||||||
|
**檔案大小 == 84 + 50×count**(header 常也以 "solid" 開頭,不能只看字串)。頂點不焊接(STL 原生就無共用頂點)。
|
||||||
|
**後果**:STL leaf **沒有 `SourceGeos`(CADability B-rep)** → (1) 不進 STEP 匯出(STL 本就非 B-rep 幾何,符合原設計意圖;
|
||||||
|
v0.7.0 以前會把 STL shell 當 Shell 混進 STEP,現移除);(2) 剛體變換照常 — `TransformRoot` 的 SourceGeos 迴圈空轉、
|
||||||
|
網格層照樣變換;(3) 量測走網格近似不受影響。**不要為了「STL 也要進 STEP」而改回 CADability ImportSTL**(會退回龜速)
|
||||||
- IGES 無 reader;STL 無 B-rep(FaceInfo.BrepFace == null 的分支要保留)
|
- IGES 無 reader;STL 無 B-rep(FaceInfo.BrepFace == null 的分支要保留)
|
||||||
- 不寫入原始檔(唯讀工具);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 匯出往返:寫出→回讀比對實體數)、
|
||||||
|
`--stl-export-test [file.stp]`(STL binary/ASCII 往返 + 退化濾除;給 stp 加測 B-rep 精細重算)、
|
||||||
|
`--make-stl <out.stl> [triCount]`(產測試用 binary STL,預設 150 萬三角形,量 STL 匯入效能用)、
|
||||||
|
`--localization-test`(語言包載入/回退/壞檔容錯 + VM 即時切換;改 i18n 或加文字要跑)
|
||||||
|
- Linux 也能編 Windows 版:`~/.dotnet/dotnet build/publish ... -p:EnableWindowsTargeting=true`(SmokeTest 是 net8.0-windows,只能在 Windows 跑)
|
||||||
- **絕不要用 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/
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# STPViewer
|
# STPViewer
|
||||||
|
|
||||||
[](README.md) [](README.zh-TW.md)
|
[](README.md) [](README.zh-CN.md) [](README.zh-TW.md)
|
||||||
|
|
||||||
> 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,34 @@
|
|||||||
|
|
||||||
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
|
- **Language** — choose a language from **View → Language**; available languages come from the `i18n/` JSON files at startup, and your choice is remembered
|
||||||
- **View** — right-button orbit, wheel zoom, middle-button pan, ViewCube
|
- **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; 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 +75,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 +106,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 +119,8 @@ 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 -- --localization-test # language resources and live switching
|
||||||
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP export round-trip
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -131,6 +137,24 @@ NuGet dependencies (restored automatically): `CADability`, `HelixToolkit.Wpf`, `
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🌐 Adding a Language
|
||||||
|
|
||||||
|
The application scans `i18n/*.json` beside the executable at startup. Each file name without `.json` is the language ID (for example, `en.json` → `en`, `zh_cn.json` → `zh_cn`). The JSON root has a `key` for the name shown in the language menu and a `data` object that maps resource keys to translated text:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "Français",
|
||||||
|
"data": {
|
||||||
|
"Ui.Language": "Langue",
|
||||||
|
"Ui.Main.MenuImport": "📂 Importer…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `src/STPViewer/i18n/en.json` as the list of text keys, translate its `data` values, and save the new file as UTF-8 in `i18n/` beside the executable. Restart STPViewer to see the new language in the menu and Quick Bar. No recompilation is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -141,9 +165,10 @@ STPViewer/
|
|||||||
├── src/STPViewer/
|
├── src/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
|
||||||
|
│ ├── i18n/ # Language JSON files copied beside the executable
|
||||||
│ ├── 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 +180,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 +197,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.
|
||||||
|
|||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
# STPViewer
|
||||||
|
|
||||||
|
[](README.md) [](README.zh-CN.md) [](README.zh-TW.md)
|
||||||
|
|
||||||
|
> STP/STEP 3D 查看器(Windows 桌面程序,C# .NET 8 WPF)— 多文件导入、装配树、点/距离/边/面/圆测量。
|
||||||
|
|
||||||
|
   
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 目录
|
||||||
|
|
||||||
|
- [✨ 功能特性](#-功能特性)
|
||||||
|
- [💻 系统需求](#-系统需求)
|
||||||
|
- [📥 安装](#-安装)
|
||||||
|
- [🚀 快速开始](#-快速开始)
|
||||||
|
- [📚 使用指南](#-使用指南)
|
||||||
|
- [🔨 从源代码构建](#-从源代码构建)
|
||||||
|
- [📁 项目结构](#-项目结构)
|
||||||
|
- [⚠️ 已知限制](#️-已知限制)
|
||||||
|
- [🤝 贡献](#-贡献)
|
||||||
|
- [📜 版本记录](#-版本记录)
|
||||||
|
- [🙏 致谢](#-致谢)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
快速打开机械零件 STEP 文件进行查看和测量,无需安装 SolidWorks / Creo 等大型 CAD 软件。
|
||||||
|
|
||||||
|
- **多文件导入** — STEP / STL / DXF,工具栏导入(可多选)、拖放到窗口、命令行 `STPViewer.exe a.stp b.stl`,或最近文件下拉菜单(▾,最近 10 项)
|
||||||
|
- **装配树** — STEP product structure 还原为树(组件→零件);逐节点显示/隐藏、更改颜色(作用于子节点)、缩放至节点;文件层级可移除、开关轮廓边线;**按名称搜索过滤**;右键 **只显示此节点 / 反转显示 / 全部显示 / 计算体积与质心**;提示信息显示外形尺寸 L×W×H
|
||||||
|
- **测量**(工具栏切换模式或快捷键,点击模型):
|
||||||
|
|
||||||
|
| 模式 | 快捷键 | 输出 |
|
||||||
|
|---|---|---|
|
||||||
|
| 📍 点 | P | XYZ 坐标(自动吸附邻近 B-rep 顶点**或圆心**) |
|
||||||
|
| 📏 距离 | D | 两点直线距离 + ΔX/ΔY/ΔZ — 点击两个孔缘自动吸附**圆心**,可直接测量孔中心距 |
|
||||||
|
| 📐 边 | E | 直线长 / 曲线长 / 圆弧长 + 半径 |
|
||||||
|
| ⬛ 面 | F | 面积(网格近似)+ 曲面类型(平面法向量、圆柱半径/轴向) |
|
||||||
|
| ⭕ 圆 | C | 圆心 / 半径 / 直径 / 周长 |
|
||||||
|
| ∠ 角度 | A | 两面(法向量)/ 两条直线边的夹角 + 补角 |
|
||||||
|
| ⇔ 面距 | M | 面到面最短距离(网格近似)+ 最近点对 |
|
||||||
|
| ⚖ 体积 | (树右键) | 网格有符号体积 + 质心标记 + 外形尺寸(封闭实体) |
|
||||||
|
| ⤚ 对齐(两点) | | 点「要移动零件」一点 + 目标点 → 纯平移使两点贴合 |
|
||||||
|
| 🎯 对齐(三点) | | 源文件 3 点 + 目标文件 3 对应点 → 旋转+平移一次贴合 |
|
||||||
|
|
||||||
|
再次按下同一快捷键退出模式;**Esc** 先取消当前选择、再按退出模式。
|
||||||
|
- **旋转** — 树面板选文件 + 工具栏 ↻X/↻Y/↻Z 绕中心 +90°(用于调整朝向,可重复旋转)
|
||||||
|
- **拖动** 🖐 — 手形光标模式,左键按住零件沿屏幕平面拖动、放开定位(右键转视角不受影响)
|
||||||
|
- **操作器** ⊹ — 树面板选文件 → XYZ 三色箭头 + 旋转环(Fusion 360 风格);拖箭头沿该轴移动(与视角无关)、拖环绕轴转。永远浮在最上层、不被实体遮挡
|
||||||
|
- **干涉检查** 🧩 — 2 个以上可见文件**两两配对**检查:相交时显示红色干涉交线 + 相交的三角形对数;无相交时报告最小间隙(gap≈0 即为配合,共面贴合不算干涉)
|
||||||
|
- **剖面** ✂ — X/Y/Z 轴或**在模型上点 3 点定义任意平面**;位置滑块 + 数值输入 + 反向;CPU 网格裁切(背景平行运算),原始几何保留(测量仍精确)
|
||||||
|
- **单位** — mm ⇄ inch 一键切换,已有测量(列表与 3D 标签)实时换算
|
||||||
|
- **语言** — 在 **视图 → 语言** 中选择语言;启动时从 `i18n/` 的 JSON 文件读取可用语言,并记住你的选择
|
||||||
|
- **导出** — 测量结果 CSV(UTF-8 BOM,Excel 中文不乱码)、3D 视图 2× PNG 截图、📤 **STEP 导出**:可见零件以当前对齐位置写成新 STEP 文件(源文件不动)
|
||||||
|
- **视角** — 右键旋转、滚轮缩放、中键平移、ViewCube;一键 **等角/前/上/右** 标准视图与 **正交⇄透视** 切换
|
||||||
|
- **设置保存** — 窗口位置和大小、单位、最近文件自动保存;左右面板可拖动调宽
|
||||||
|
|
||||||
|
测量原则:边长、圆半径、角度取 **B-rep 精确值**;面积为三角网格求和近似(三角化精度根据模型尺寸自适应 0.02–0.5 mm)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 系统需求
|
||||||
|
|
||||||
|
| 项目 | 需求 |
|
||||||
|
|------|------|
|
||||||
|
| 操作系统 | Windows 10 / 11(x64) |
|
||||||
|
| 运行环境 | .NET 8 Desktop Runtime(依赖框架版)— Portable 版则免装 |
|
||||||
|
| 构建 SDK | .NET 8 SDK(仅从源代码构建时需要) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 安装
|
||||||
|
|
||||||
|
下载发布版直接执行,免安装。
|
||||||
|
|
||||||
|
- **依赖框架版**(文件较小):需 .NET 8 Desktop Runtime,执行 `STPViewer v0.5.0.exe`。
|
||||||
|
- **Portable 版**(self-contained):内置运行环境,免安装 / 无需管理员权限,执行 `STPViewer v0.5.0.exe`。
|
||||||
|
|
||||||
|
或从源代码构建(见下)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/ETWen/STPViewer.git
|
||||||
|
cd STPViewer
|
||||||
|
dotnet build STPViewer.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建并执行
|
||||||
|
dotnet build STPViewer.sln
|
||||||
|
dotnet run --project src/STPViewer
|
||||||
|
|
||||||
|
# 发布免安装文件夹
|
||||||
|
dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
||||||
|
```
|
||||||
|
|
||||||
|
然后导入 `.stp` 文件(工具栏「导入」、拖放或命令行传入文件路径),选一个测量模式,点击模型即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 使用指南
|
||||||
|
|
||||||
|
1. **导入** 一个或多个 CAD 文件,每个文件成为装配树的一个根节点,视角自动缩放至适合窗口。
|
||||||
|
2. **操作树** — 按名称搜索、显示/隐藏(或右键隔离显示)、换色、缩放至某节点,或移除文件。
|
||||||
|
3. **测量** — 在工具栏选择模式或按快捷键(P/D/E/F/C/A/M),点击模型;点击孔缘自动吸附圆心,点击两次即可测得孔中心距;Esc 取消。结果显示在右侧面板,可逐条删除或全部清除。
|
||||||
|
4. **装配** — 使用对齐(两点/三点)、旋转、拖动、操作器摆位,再用干涉检查对所有可见文件两两检查,验证装配配合。
|
||||||
|
5. **剖面** — 打开 ✂、选轴向或点 3 点定义任意平面、滑动(或输入 %)剖切;测量仍以原始几何精确计算。
|
||||||
|
6. **导出** — 测量结果保存为 CSV、截取 2× PNG,或把对齐后的场景写成新 STEP 文件交接给 CAD。
|
||||||
|
|
||||||
|
无需启动界面的导入流程与几何运算验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project tools/SmokeTest -- "path\to\model.stp" # 导入 + 装配树
|
||||||
|
dotnet run --project tools/SmokeTest -- --clip-test # 剖切裁切数学
|
||||||
|
dotnet run --project tools/SmokeTest -- --interference-test # 干涉 相交/分离/贴合
|
||||||
|
dotnet run --project tools/SmokeTest -- --align-test # 三点对齐刚体变换数学
|
||||||
|
dotnet run --project tools/SmokeTest -- --localization-test # 语言资源与实时切换
|
||||||
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP 导出往返验证
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 从源代码构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build STPViewer.sln -c Debug
|
||||||
|
dotnet run --project src/STPViewer
|
||||||
|
dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
||||||
|
```
|
||||||
|
|
||||||
|
NuGet 依赖(自动还原):`CADability`、`HelixToolkit.Wpf`、`CommunityToolkit.Mvvm`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 添加语言
|
||||||
|
|
||||||
|
程序启动时扫描可执行文件旁的 `i18n/*.json`。文件名去掉 `.json` 后就是语言 ID,例如 `en.json` 对应 `en`,`zh_cn.json` 对应 `zh_cn`。JSON 顶层的 `key` 是语言菜单中的显示名,`data` 是资源键到译文的映射:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "Français",
|
||||||
|
"data": {
|
||||||
|
"Ui.Language": "Langue",
|
||||||
|
"Ui.Main.MenuImport": "📂 Importer…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
参考 `src/STPViewer/i18n/en.json` 中的资源键,翻译其 `data` 内容,将新文件以 UTF-8 编码放入可执行文件旁的 `i18n/`。重启 STPViewer 后,新语言便会出现在菜单和快捷栏中,无需重新编译。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
STPViewer/
|
||||||
|
├── ARCHITECTURE.md # 设计、数据流、开发 阶段
|
||||||
|
├── CLAUDE.md # 项目记忆 & 开发规范
|
||||||
|
├── STPViewer.sln
|
||||||
|
├── src/STPViewer/
|
||||||
|
│ ├── STPViewer.csproj # net8.0-windows、UseWPF、统一的版本号来源 <Version>
|
||||||
|
│ ├── MainWindow.xaml / .cs # 布局 + 鼠标拾取转发
|
||||||
|
│ ├── i18n/ # 复制到可执行文件旁的语言 JSON 文件
|
||||||
|
│ ├── Models/ # FaceInfo、MeasureMode、MeasurementResult、UnitSystem
|
||||||
|
│ ├── Services/ # StepImport、Measurement、Interference、Section、RigidAlign、Settings
|
||||||
|
│ └── ViewModels/ # MainViewModel(partial:Drag/Gizmo/Section/Interference/Export)、ModelNodeViewModel
|
||||||
|
└── tools/SmokeTest/ # 无需 UI 导入 + 几何数学验证
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 已知限制
|
||||||
|
|
||||||
|
- 大型 STEP(数千面)导入需数十秒(CADability 解析成本),导入期间有进度提示、UI 不会卡住。
|
||||||
|
- 轮廓边线超过 30,000 线段的文件会默认关闭边线(WPF `LinesVisual3D` 转动视角时性能限制),可在树面板手动打开。
|
||||||
|
- **IGES 不支持**(CADability 无 IGES reader);STL 无 B-rep,仅支持 点/距离/角度/面距 测量;DXF 为线框查看。
|
||||||
|
- 剖切面无封口(cap),剖开处显示内部背面材质(深灰)。
|
||||||
|
- STEP 导出为「当前位置的 Solid/Shell 扁平列表」,不保留原始装配层级与 product 名称;STL 网格、DXF 线框无 B-rep 不导出。
|
||||||
|
- 面积、面距、体积/质心为三角网格近似值;边长/圆半径/角度为 B-rep 精确值。
|
||||||
|
- 少数 AP242 文件 CADability 支持不完整,导入失败会提示消息(不会崩溃)。
|
||||||
|
- 只读查看器,不写入/修改原始文件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
1. Fork 并建立 feature 分支:`git checkout -b feature/your-feature`
|
||||||
|
2. 遵循 [Conventional Commits](https://www.conventionalcommits.org/):`feat(scope): summary`
|
||||||
|
3. Push 后开 Pull Request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 版本记录
|
||||||
|
|
||||||
|
### v0.5.0
|
||||||
|
|
||||||
|
- **功能扩展:** 圆心吸附(点击两次测量孔中心距)、测量快捷键(P/D/E/F/C/A/M)+ Esc、标准视图(等角/前/上/右)+ 正交投影、装配树搜索/隔离显示/反转、体积与质心、3 点任意剖切平面 + 位置数值输入、干涉检查支持 3 个以上文件(两两配对)、对齐后场景导出 STEP、面板可调宽、窗口/单位/最近文件自动保存。
|
||||||
|
|
||||||
|
### v0.4.0
|
||||||
|
|
||||||
|
- **性能:** 剖面裁切改为后台并行运算(大文件拖动滑块时不再卡住 UI);零件变换烘焙(拖动/操作器放开)并行化;干涉间隙精修以 AABB 快速排除加速。
|
||||||
|
- **稳定性:** 全域例外处理(记 log 至 `%LOCALAPPDATA%\STPViewer\error.log` + 消息框,不会崩溃);背景运算期间停用会改动几何的指令;导入防重入。
|
||||||
|
|
||||||
|
### v0.3.2
|
||||||
|
|
||||||
|
- **性能:** 大组件测量模式下转动视角不再卡顿。测量模式改为渲染合并网格(每个文件 1 个 model),并用命中三角形的顶点 index 反查回是哪个面,不再挂数万个逐面 model;逐面渲染只保留给剖面模式。
|
||||||
|
- 相机交互暂停事件额外监听 `HelixViewport3D.CameraChanged`,避免相机实例被换掉时失去订阅。
|
||||||
|
|
||||||
|
### v0.3.1
|
||||||
|
|
||||||
|
- 操作器 always-on-top 叠图层(永远浮在零件上、不被实体遮挡)。
|
||||||
|
|
||||||
|
### v0.3.0
|
||||||
|
|
||||||
|
- 旋转对齐:轴向旋转(↻X/↻Y/↻Z)、三点对齐,以及统一的 `TransformRoot` 刚体变换路径。
|
||||||
|
|
||||||
|
### v0.2.x
|
||||||
|
|
||||||
|
- 拖动模式、两点对齐、干涉检查、剖面、角度/面距测量、装配树、STL/DXF 支持、mm ⇄ inch。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
|
- [CADability](https://github.com/SOFAgh/CADability) — 纯 C# CAD kernel:STEP 导入、B-rep 几何、面三角化
|
||||||
|
- [HelixToolkit.Wpf](https://github.com/helix-toolkit/helix-toolkit) — 3D viewport、相机操作、HitTest
|
||||||
|
- [CommunityToolkit.Mvvm](https://github.com/CommunityToolkit/dotnet) — MVVM
|
||||||
|
|
||||||
|
详细设计与开发 阶段 见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||||
+65
-30
@@ -1,10 +1,10 @@
|
|||||||
# STPViewer
|
# STPViewer
|
||||||
|
|
||||||
[](README.md) [](README.zh-TW.md)
|
[](README.md) [](README.zh-CN.md) [](README.zh-TW.md)
|
||||||
|
|
||||||
> STP/STEP 3D 檢視器(Windows 桌面程式,C# .NET 8 WPF)— 多檔匯入、裝配樹、點/距離/邊/面/圓量測。
|
> STP/STEP 3D 檢視器(Windows 桌面程式,C# .NET 8 WPF)— 多檔匯入、裝配樹、點/距離/邊/面/圓量測。
|
||||||
|
|
||||||
   
|
   
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,30 +28,34 @@
|
|||||||
|
|
||||||
快速開啟機構件 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 截圖
|
- **語言** — 從 **視圖 → 語言** 選擇語言;啟動時從 `i18n/` 的 JSON 檔案讀取可用語言,並記住選擇
|
||||||
- **視角** — 右鍵旋轉、滾輪縮放、中鍵平移、ViewCube
|
- **匯出** — 量測結果 CSV(UTF-8 BOM,Excel 中文不亂碼)、3D 視圖 2× PNG 截圖、📤 **STEP 匯出**:可見零件以目前對齊位置寫成新 STEP 檔(原始檔不動)
|
||||||
|
- **視角** — 右鍵旋轉、滾輪縮放、中鍵平移、ViewCube;一鍵 **等角/前/上/右** 標準視圖與 **正交⇄透視** 切換
|
||||||
|
- **記住設定** — 視窗位置大小、單位、最近檔案自動保存;左右面板可拖曳調寬
|
||||||
|
|
||||||
量測原則:邊長、圓半徑、角度取 **B-rep 精確值**;面積為三角網格加總近似(三角化精度依模型尺寸自適應 0.02–0.5 mm)。
|
量測原則:邊長、圓半徑、角度取 **B-rep 精確值**;面積為三角網格加總近似(三角化精度依模型尺寸自適應 0.02–0.5 mm)。
|
||||||
|
|
||||||
@@ -71,8 +75,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 +106,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 +119,8 @@ 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 -- --localization-test # 語言資源與即時切換
|
||||||
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP 匯出往返驗證
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -131,6 +137,24 @@ NuGet 相依(自動還原):`CADability`、`HelixToolkit.Wpf`、`CommunityT
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🌐 新增語言
|
||||||
|
|
||||||
|
程式啟動時掃描執行檔旁的 `i18n/*.json`。檔名去掉 `.json` 後就是語言 ID,例如 `en.json` 對應 `en`,`zh_tw.json` 對應 `zh_tw`。JSON 頂層的 `key` 是語言選單中的顯示名稱,`data` 是資源鍵與譯文的對應:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "Français",
|
||||||
|
"data": {
|
||||||
|
"Ui.Language": "Langue",
|
||||||
|
"Ui.Main.MenuImport": "📂 Importer…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
參考 `src/STPViewer/i18n/en.json` 的資源鍵,翻譯其 `data` 內容,將新檔案以 UTF-8 編碼放入執行檔旁的 `i18n/`。重啟 STPViewer 後,新語言便會出現在選單與快速列中,不需重新編譯。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 專案結構
|
## 📁 專案結構
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -141,9 +165,10 @@ STPViewer/
|
|||||||
├── src/STPViewer/
|
├── src/STPViewer/
|
||||||
│ ├── STPViewer.csproj # net8.0-windows、UseWPF、單一版號來源 <Version>
|
│ ├── STPViewer.csproj # net8.0-windows、UseWPF、單一版號來源 <Version>
|
||||||
│ ├── MainWindow.xaml / .cs # 版面 + 滑鼠拾取轉發
|
│ ├── MainWindow.xaml / .cs # 版面 + 滑鼠拾取轉發
|
||||||
|
│ ├── i18n/ # 複製到執行檔旁的語言 JSON 檔
|
||||||
│ ├── 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 +180,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 +197,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,26 @@
|
|||||||
|
# v0.1.0 — First release: STEP viewer with measurement & assembly checks
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Open your CAD files**
|
||||||
|
* Import STEP, STL and DXF — several files at once. STEP assemblies show up as a tree where you can hide/show, recolor or zoom to any part.
|
||||||
|
|
||||||
|
**Measure**
|
||||||
|
* Point, distance, edge length, face, circle (radius/diameter), angle and face-to-face distance. Values come from the exact CAD geometry wherever it's available.
|
||||||
|
|
||||||
|
**Check how parts fit**
|
||||||
|
* Two-point align (move), three-point align (rotate + move), rotate 90° about an axis, and an interference check that reports overlaps or the minimum gap between parts.
|
||||||
|
|
||||||
|
**More tools**
|
||||||
|
* Section view, mm ⇄ inch switch, export measurements to CSV, and save a high-resolution screenshot.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [commit history](https://github.com/ETWen/STPViewer/commits/v0.1.0)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# v0.2.0 — Drag parts into place
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Drag mode**
|
||||||
|
* Press and hold a part with the left mouse button and drag it where you want; release to drop it. Smooth even on very large assemblies, and measurement accuracy is unaffected.
|
||||||
|
* Right-drag rotate, wheel zoom and Shift+drag pan still work as before.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.1.0...v0.2.0](https://github.com/ETWen/STPViewer/compare/v0.1.0...v0.2.0)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# v0.2.1 — Fix crash after dragging a part
|
||||||
|
|
||||||
|
## 🐛 Bug fixes
|
||||||
|
|
||||||
|
**No more crash after using drag mode**
|
||||||
|
* Clicking anywhere in the view after dragging a part could crash the app. Fixed — you can drag, then measure or select as usual.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.2.0...v0.2.1](https://github.com/ETWen/STPViewer/compare/v0.2.0...v0.2.1)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# v0.3.0 — 3-axis gizmo for precise move & rotate
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Gizmo manipulator**
|
||||||
|
* Select a file in the tree and a gizmo appears at its center: drag the X/Y/Z arrows to move exactly along one axis, or the rotation rings to spin it by any angle — from any viewing angle.
|
||||||
|
* Selecting another file moves the gizmo to it.
|
||||||
|
|
||||||
|
## ⚠️ Known limitation
|
||||||
|
|
||||||
|
* The gizmo can be hidden inside solid parts. (Fixed in v0.3.1 — the gizmo now always stays on top.)
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.2.1...v0.3.0](https://github.com/ETWen/STPViewer/compare/v0.2.1...v0.3.0)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# v0.4.0 — Faster and more stable
|
||||||
|
|
||||||
|
## ✨ Improvements
|
||||||
|
|
||||||
|
**Section slider stays responsive on big files**
|
||||||
|
* Dragging the section slider on large assemblies no longer freezes the screen — the cut is computed in the background and shown as soon as it's ready.
|
||||||
|
|
||||||
|
**Faster placement after moving parts**
|
||||||
|
* After dragging or using the gizmo, the part settles into its new position much faster. The interference check's gap calculation is faster too.
|
||||||
|
|
||||||
|
## 🐛 Bug fixes
|
||||||
|
|
||||||
|
**Errors no longer close the app**
|
||||||
|
* If something goes wrong, you now get a message and the app keeps running instead of shutting down. Details are logged to `%LOCALAPPDATA%\STPViewer\error.log`.
|
||||||
|
* Import, rotate and interference buttons are disabled while a long task is running, so they can't be triggered twice by accident.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.3.2...v0.4.0](https://github.com/ETWen/STPViewer/compare/v0.3.2...v0.4.0)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# v0.6.0 — Export to STL
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**STL export with a settings dialog**
|
||||||
|
* Save the parts on screen — including any alignment or rotation you applied — as an STL mesh for 3D printing or other software.
|
||||||
|
* Before exporting you choose: one merged file or one file per input, Binary or ASCII, mm / inch / custom scale, and mesh quality (current or fine).
|
||||||
|
* The dialog shows the triangle count and estimated file size live, so you know what you'll get before you hit export.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.5.0...v0.6.0](https://github.com/ETWen/STPViewer/compare/v0.5.0...v0.6.0)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# v0.7.0 — Menu bar & customizable quick toolbar
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Menu bar**
|
||||||
|
* Every feature is now in a menu, grouped by File / View / Measure / Transform / Tools — no more hunting through one long row of buttons.
|
||||||
|
|
||||||
|
**Quick toolbar you can customize**
|
||||||
|
* The toolbar now holds only the buttons you use most. Pick which ones under **Quick Bar → Customize**; your choice is remembered next time.
|
||||||
|
|
||||||
|
**Tidier section controls**
|
||||||
|
* The section axis/position bar only appears while section view is on, so it no longer takes up space.
|
||||||
|
|
||||||
|
**About dialog**
|
||||||
|
* **Help → About** shows the version and a plain-language history of what each release brought.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
Source only — no prebuilt binaries were published for this version. For a ready-to-run build, grab the [latest release](https://github.com/ETWen/STPViewer/releases/latest).
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.6.0...v0.7.0](https://github.com/ETWen/STPViewer/compare/v0.6.0...v0.7.0)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# v0.7.1 — Large STL files open in a blink
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Large STL files open in a blink**
|
||||||
|
* Opening big STL meshes (tens of MB, millions of triangles) is now near-instant instead of a multi-minute wait. A 75 MB / 1.5-million-triangle STL that used to take ages now shows in **under a second**.
|
||||||
|
* STL is now parsed directly instead of going through the CAD kernel's importer, which used to build a full B-rep face per triangle (100k triangles: **~41 s → ~11 ms**). Both binary and ASCII STL are detected and read automatically.
|
||||||
|
|
||||||
|
**Customizable menu bar & quick toolbar**
|
||||||
|
* A full menu bar groups every feature into categorized dropdowns, and a quick toolbar lets you tick just the buttons you use most — your selection is remembered between sessions.
|
||||||
|
|
||||||
|
**About dialog with a changelog**
|
||||||
|
* Help → About now shows a per-version, plain-language changelog so you can see what each release brought.
|
||||||
|
|
||||||
|
## 🐛 Bug fixes
|
||||||
|
|
||||||
|
**STL geometry no longer leaks into STEP export**
|
||||||
|
* STL is pure mesh (no B-rep), so STL parts are now correctly excluded from STEP export — earlier versions wrongly mixed STL shells into the exported STEP. Rigid transforms and measurement on STL parts are unaffected.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
| Build | Needs .NET Runtime? | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| **STPViewer_v0.7.1** (Standard) | ✅ Requires .NET 8 Desktop Runtime | Smaller download |
|
||||||
|
| **STPViewer_v0.7.1_portable** (Portable) | ❌ Runtime included | Unzip and run anywhere, no admin |
|
||||||
|
|
||||||
|
Run `STPViewer v0.7.1.exe`.
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.6.0...v0.7.1](https://github.com/ETWen/STPViewer/compare/v0.6.0...v0.7.1)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# v0.8.0 — Switch languages: English, 繁體中文, 简体中文
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Language switching**
|
||||||
|
* Pick **English**, **繁體中文** or **简体中文** from **View → Language** or the language box on the quick toolbar. The whole interface switches instantly — menus, dialogs, status messages, and even measurements already on screen — and your choice is remembered next time.
|
||||||
|
* On first launch STPViewer follows your Windows display language (Traditional Chinese regions → 繁體中文, other Chinese → 简体中文, everything else → English).
|
||||||
|
|
||||||
|
**Add your own language — no update needed**
|
||||||
|
* Languages are plain JSON files in the `i18n` folder next to `STPViewer.exe`. Copy `en.json`, translate it, save it under a new name, and restart — the new language appears in the menu. Anything not yet translated falls back to English. See [Adding a Language](https://github.com/ETWen/STPViewer#-adding-a-language) in the README.
|
||||||
|
|
||||||
|
**Simplified Chinese README**
|
||||||
|
* The README is now also available in [简体中文](https://github.com/ETWen/STPViewer/blob/main/README.zh-CN.md).
|
||||||
|
|
||||||
|
## 🙏 Thanks
|
||||||
|
|
||||||
|
* Multi-language support was contributed by **@CarmJos** in [#1](https://github.com/ETWen/STPViewer/pull/1) — thank you!
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
| Build | Needs .NET Runtime? | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| **STPViewer_v0.8.0** (Standard) | ✅ Requires .NET 8 Desktop Runtime | Smaller download |
|
||||||
|
| **STPViewer_v0.8.0_portable** (Portable) | ❌ Runtime included | Unzip and run anywhere, no admin |
|
||||||
|
|
||||||
|
Run `STPViewer v0.8.0.exe`. Keep the `i18n` folder next to it — that's where the languages live.
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.7.1...v0.8.0](https://github.com/ETWen/STPViewer/compare/v0.7.1...v0.8.0)
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<Window x:Class="STPViewer.AboutDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:loc="clr-namespace:STPViewer.Localization"
|
||||||
|
Title="{loc:Loc Ui.About.Title}"
|
||||||
|
Width="860" Height="600"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="CanResize" MinWidth="700" MinHeight="420"
|
||||||
|
ShowInTaskbar="False">
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#F0F4F8" />
|
||||||
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="Padding" Value="14,10" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,10" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="CardTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,6" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="DimText" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#667788" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="14">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="300" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- ─── 左:程式 / 開發者 / 技術棧 ─── -->
|
||||||
|
<StackPanel Grid.Column="0" Grid.Row="0" Margin="0,0,12,0">
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<TextBlock Text="STPViewer" FontSize="22" FontWeight="Bold"
|
||||||
|
Foreground="#1565C0" HorizontalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="VersionText" Text="v0.0.0" Style="{StaticResource DimText}"
|
||||||
|
HorizontalAlignment="Center" Margin="0,2,0,8" />
|
||||||
|
<TextBlock Text="{loc:Loc Ui.About.Description}" TextAlignment="Center"
|
||||||
|
TextWrapping="Wrap" Width="260" FontSize="12" LineHeight="18" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{loc:Loc Ui.About.Developer}" Style="{StaticResource CardTitle}" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="60" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition /><RowDefinition /><RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{loc:Loc Ui.About.Name}" Style="{StaticResource DimText}" />
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="ET Wen" FontSize="12" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{loc:Loc Ui.About.Email}" Style="{StaticResource DimText}" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="eric441151893@gmail.com" FontSize="12" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="GitHub" Style="{StaticResource DimText}" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="github.com/ETWen" FontSize="12" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{loc:Loc Ui.About.TechnologyTitle}" Style="{StaticResource CardTitle}" />
|
||||||
|
<TextBlock Text="{loc:Loc Ui.About.Technology}" Style="{StaticResource DimText}"
|
||||||
|
TextWrapping="Wrap" Width="260" LineHeight="18" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ─── 右:Change log ─── -->
|
||||||
|
<Border Grid.Column="1" Grid.Row="0" Background="#FAFBFC" CornerRadius="6" Padding="4">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="{loc:Loc Ui.About.Changelog}" FontWeight="Bold" FontSize="14"
|
||||||
|
Margin="12,8,0,4" />
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="12,0,12,8">
|
||||||
|
<ItemsControl x:Name="ChangelogList">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="0,8,0,4">
|
||||||
|
<TextBlock FontWeight="Bold" Foreground="#1565C0" FontSize="13" TextWrapping="Wrap">
|
||||||
|
<Run Text="●" /> <Run Text="{Binding Header, Mode=OneWay}" />
|
||||||
|
</TextBlock>
|
||||||
|
<ItemsControl ItemsSource="{Binding Changes}" Margin="16,3,0,0">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" TextWrapping="Wrap" FontSize="12"
|
||||||
|
LineHeight="17" Margin="0,1,0,1" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button Grid.Column="1" Grid.Row="1" Content="{loc:Loc Ui.About.Close}" Width="90" Margin="0,10,0,0"
|
||||||
|
HorizontalAlignment="Right" IsCancel="True" IsDefault="True" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Windows;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
|
namespace STPViewer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 關於視窗:左 = 程式資訊 / 開發者 / 技術棧,右 = 版本紀錄(寫給一般使用者看,非技術 changelog)。
|
||||||
|
/// 樣式參考 ETTerms 的 AboutView。發新版時在 Changelog 陣列最上面加一筆。
|
||||||
|
/// </summary>
|
||||||
|
public partial class AboutDialog : Window
|
||||||
|
{
|
||||||
|
public AboutDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Version v = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
VersionText.Text = $"v{v.Major}.{v.Minor}.{v.Build}";
|
||||||
|
ChangelogList.ItemsSource = Changelog.Select(e => new
|
||||||
|
{
|
||||||
|
Header = $"v{e.Version} · {LocalizationService.Get(e.Title)} ({e.Date:yyyy-MM-dd})",
|
||||||
|
Changes = e.Changes.Select(LocalizationService.Get).ToArray(),
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChangelogEntry(string Version, DateOnly Date, string Title, string[] Changes);
|
||||||
|
|
||||||
|
// ── 版本紀錄(新版加最上面;口吻:一般使用者看得懂,講「你會感覺到什麼」而非實作細節)──
|
||||||
|
private static readonly ChangelogEntry[] Changelog =
|
||||||
|
{
|
||||||
|
new("0.8.0", new DateOnly(2026, 10, 20), "Ui.About.Release080.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release080.Change1",
|
||||||
|
"Ui.About.Release080.Change2",
|
||||||
|
"Ui.About.Release080.Change3",
|
||||||
|
}),
|
||||||
|
new("0.7.1", new DateOnly(2026, 7, 12), "Ui.About.Release071.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release071.Change1",
|
||||||
|
"Ui.About.Release071.Change2",
|
||||||
|
}),
|
||||||
|
new("0.7.0", new DateOnly(2026, 7, 4), "Ui.About.Release070.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release070.Change1",
|
||||||
|
"Ui.About.Release070.Change2",
|
||||||
|
"Ui.About.Release070.Change3",
|
||||||
|
"Ui.About.Release070.Change4",
|
||||||
|
}),
|
||||||
|
new("0.6.0", new DateOnly(2026, 7, 4), "Ui.About.Release060.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release060.Change1",
|
||||||
|
"Ui.About.Release060.Change2",
|
||||||
|
"Ui.About.Release060.Change3",
|
||||||
|
}),
|
||||||
|
new("0.5.0", new DateOnly(2026, 7, 3), "Ui.About.Release050.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release050.Change1",
|
||||||
|
"Ui.About.Release050.Change2",
|
||||||
|
"Ui.About.Release050.Change3",
|
||||||
|
"Ui.About.Release050.Change4",
|
||||||
|
"Ui.About.Release050.Change5",
|
||||||
|
"Ui.About.Release050.Change6",
|
||||||
|
"Ui.About.Release050.Change7",
|
||||||
|
}),
|
||||||
|
new("0.4.0", new DateOnly(2026, 7, 2), "Ui.About.Release040.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release040.Change1",
|
||||||
|
"Ui.About.Release040.Change2",
|
||||||
|
"Ui.About.Release040.Change3",
|
||||||
|
}),
|
||||||
|
new("0.3.2", new DateOnly(2026, 6, 18), "Ui.About.Release032.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release032.Change1",
|
||||||
|
}),
|
||||||
|
new("0.3.1", new DateOnly(2026, 6, 13), "Ui.About.Release031.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release031.Change1",
|
||||||
|
}),
|
||||||
|
new("0.3.0", new DateOnly(2026, 6, 13), "Ui.About.Release030.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release030.Change1",
|
||||||
|
}),
|
||||||
|
new("0.2.0", new DateOnly(2026, 6, 13), "Ui.About.Release020.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release020.Change1",
|
||||||
|
"Ui.About.Release020.Change2",
|
||||||
|
}),
|
||||||
|
new("0.1.0", new DateOnly(2026, 6, 13), "Ui.About.Release010.Title",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"Ui.About.Release010.Change1",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
namespace STPViewer;
|
namespace STPViewer;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ public partial class App : Application
|
|||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
|
LocalizationService.Instance.SetLanguage(SettingsService.Load().Language);
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
// UI 執行緒(含 async void 回拋)的未攔截例外:記 log、提示、標記 Handled 讓程式存活。
|
// UI 執行緒(含 async void 回拋)的未攔截例外:記 log、提示、標記 Handled 讓程式存活。
|
||||||
@@ -26,9 +28,8 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
LogException("DispatcherUnhandledException", args.Exception);
|
LogException("DispatcherUnhandledException", args.Exception);
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
$"發生未預期的錯誤,操作已中止(程式將嘗試繼續運作)。\n\n{args.Exception.Message}\n\n" +
|
LocalizationService.Format("App.UnexpectedError", args.Exception.Message, LogPath),
|
||||||
$"詳細記錄:{LogPath}",
|
LocalizationService.Get("App.ErrorTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
"STPViewer 錯誤", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,6 +56,6 @@ public partial class App : Application
|
|||||||
sb.AppendLine(new string('-', 80));
|
sb.AppendLine(new string('-', 80));
|
||||||
File.AppendAllText(LogPath, sb.ToString(), Encoding.UTF8);
|
File.AppendAllText(LogPath, sb.ToString(), Encoding.UTF8);
|
||||||
}
|
}
|
||||||
catch { /* logging 本身失敗不能再拋 */ }
|
catch { /* Logging must not throw from an exception handler. */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace STPViewer.Converters;
|
||||||
|
|
||||||
|
/// <summary>完整路徑 → 檔名(選單「最近檔案」顯示用;tooltip 仍給完整路徑)</summary>
|
||||||
|
public class FileNameConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
value is string s ? System.IO.Path.GetFileName(s) : "";
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
|
namespace STPViewer.Localization;
|
||||||
|
|
||||||
|
[MarkupExtensionReturnType(typeof(string))]
|
||||||
|
public class LocExtension : MarkupExtension
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
public LocExtension(string key) => Key = key;
|
||||||
|
|
||||||
|
public override object ProvideValue(IServiceProvider serviceProvider) =>
|
||||||
|
new Binding($"[{Key}]")
|
||||||
|
{
|
||||||
|
Source = LocalizationService.Instance,
|
||||||
|
Mode = BindingMode.OneWay,
|
||||||
|
}.ProvideValue(serviceProvider);
|
||||||
|
}
|
||||||
+296
-80
@@ -4,97 +4,279 @@
|
|||||||
xmlns:h="http://helix-toolkit.org/wpf"
|
xmlns:h="http://helix-toolkit.org/wpf"
|
||||||
xmlns:conv="clr-namespace:STPViewer.Converters"
|
xmlns:conv="clr-namespace:STPViewer.Converters"
|
||||||
xmlns:vm="clr-namespace:STPViewer.ViewModels"
|
xmlns:vm="clr-namespace:STPViewer.ViewModels"
|
||||||
Title="STPViewer — STP/STEP 3D 檢視器"
|
xmlns:loc="clr-namespace:STPViewer.Localization"
|
||||||
|
Title="{loc:Loc Ui.Main.WindowTitle}"
|
||||||
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" />
|
||||||
|
<conv:FileNameConverter x:Key="FileNameConv" />
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||||
<Style x:Key="ModeButton" TargetType="ToggleButton">
|
<Style x:Key="ModeButton" TargetType="ToggleButton">
|
||||||
<Setter Property="Padding" Value="8,4" />
|
<Setter Property="Padding" Value="8,4" />
|
||||||
<Setter Property="Margin" Value="2,0" />
|
<Setter Property="Margin" Value="2,0" />
|
||||||
<Setter Property="FontSize" Value="13" />
|
<Setter Property="FontSize" Value="13" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style x:Key="QuickButton" TargetType="Button">
|
||||||
|
<Setter Property="Padding" Value="8,4" />
|
||||||
|
<Setter Property="Margin" Value="2,0" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<!-- ─── 工具列 ─── -->
|
<!-- ─── 選單列(全部功能,分類下拉)─── -->
|
||||||
|
<Menu DockPanel.Dock="Top">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.FileF}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.MenuImport}" Command="{Binding ImportCommand}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ImportSTPSTEPSTLDXFFiles}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.RecentFiles}" ItemsSource="{Binding RecentFiles}">
|
||||||
|
<MenuItem.ItemContainerStyle>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Header" Value="{Binding Converter={StaticResource FileNameConv}}" />
|
||||||
|
<Setter Property="ToolTip" Value="{Binding}" />
|
||||||
|
<Setter Property="Command"
|
||||||
|
Value="{Binding DataContext.ImportRecentCommand, RelativeSource={RelativeSource AncestorType=Menu}}" />
|
||||||
|
<Setter Property="CommandParameter" Value="{Binding}" />
|
||||||
|
</Style>
|
||||||
|
</MenuItem.ItemContainerStyle>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ExportMeasurementsCSV}" Command="{Binding ExportCsvCommand}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.SaveViewPNG}" Command="{Binding SaveScreenshotCommand}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.SavePNGScreenshot3DView2}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ExportSTEP}" Command="{Binding ExportStepFileCommand}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ExportStepTooltip}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ExportSTL}" Command="{Binding ExportStlFileCommand}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ChooseScopeFormatUnitsMeshQuality}" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ViewV}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ZoomFit}" Command="{Binding ZoomAllCommand}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.IsometricView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.FrontView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.LookAlongYZUp}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.TopView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.LookDownFromZ}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.RightView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.LookAlongXZUp}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.OrthographicProjection}" IsCheckable="True" IsChecked="{Binding OrthographicView}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ParallelProjectionAvoidsPerspectiveDistortionUncheck}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.DisplayInchesMm}" IsCheckable="True" IsChecked="{Binding UseInch}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ExistingMeasurementsUpdateImmediatelyValuesStored}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Language}" ItemsSource="{Binding Languages}">
|
||||||
|
<MenuItem.ItemContainerStyle>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Header" Value="{Binding Key}" />
|
||||||
|
<Setter Property="IsCheckable" Value="True" />
|
||||||
|
<Setter Property="IsChecked" Value="{Binding IsSelected, Mode=OneWay}" />
|
||||||
|
<Setter Property="Command"
|
||||||
|
Value="{Binding DataContext.ChangeLanguageCommand, RelativeSource={RelativeSource AncestorType=Menu}}" />
|
||||||
|
<Setter Property="CommandParameter" Value="{Binding Id}" />
|
||||||
|
</Style>
|
||||||
|
</MenuItem.ItemContainerStyle>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.MeasureM}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.PointCoordinates}" IsCheckable="True" InputGestureText="P"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.MeasurePointCoordinatesSnapsVerticesCircle}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.TwoPointDistance}" IsCheckable="True" InputGestureText="D"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.DistanceXYZCircularEdgesSnapCenters}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.EdgeLength}" IsCheckable="True" InputGestureText="E"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.MeasureStraightCurvedCircularEdgeLengths}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.Face}" IsCheckable="True" InputGestureText="F"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.AreaSurfaceTypeNormalVector}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.Circle}" IsCheckable="True" InputGestureText="C"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.CenterRadiusDiameterCircumference}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.Angle}" IsCheckable="True" InputGestureText="A"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.AngleBetweenTwoFacesStraightEdges}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.FaceDistance}" IsCheckable="True" InputGestureText="M"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ShortestDistanceBetweenFacesMeshApproximation}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ClearAllMeasurements}" Command="{Binding ClearMeasurementsCommand}"
|
||||||
|
InputGestureText="Esc" ToolTip="{loc:Loc Ui.Main.EscCancelsCurrentMeasurementExitsMode}" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.TransformT}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.TwoPointAlignmentTranslate}" IsCheckable="True"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.PickPointPartMoveThenPick}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ThreePointAlignmentRotateTranslate}" IsCheckable="True"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.PickThreePointsMovingModelThen}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.DragMode}" IsCheckable="True"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.HoldLeftMouseButtonMovePart}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.3AxisGizmo}" IsCheckable="True" IsChecked="{Binding GizmoEnabled}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ShowXYZArrowsRotationRingsSelected}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.Rotate90AroundX}" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.RotateModelSelectedTreeAroundIts}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.Rotate90AroundY}" Command="{Binding RotateRootCommand}" CommandParameter="Y" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.Rotate90AroundZ}" Command="{Binding RotateRootCommand}" CommandParameter="Z" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ToolsL}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.InterferenceCheck}" Command="{Binding CheckInterferenceCommand}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.CheckVisibleModelPairsIntersectionsReport}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.SectionView}" IsCheckable="True" IsChecked="{Binding SectionEnabled}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ShowSectionControlsAxisPositionDirection}" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.QuickBarQ}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.CustomizeButtons}" ItemsSource="{Binding QuickBar.Items}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.CheckButtonsShowQuickBarSaved}">
|
||||||
|
<MenuItem.ItemContainerStyle>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Header" Value="{Binding Label}" />
|
||||||
|
<Setter Property="IsCheckable" Value="True" />
|
||||||
|
<Setter Property="IsChecked" Value="{Binding IsChecked}" />
|
||||||
|
<Setter Property="StaysOpenOnClick" Value="True" />
|
||||||
|
</Style>
|
||||||
|
</MenuItem.ItemContainerStyle>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.RestoreDefaultButtons}" Command="{Binding QuickBar.ResetDefaultsCommand}" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.HelpH}">
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.IAboutSTPViewer}" Click="About_Click"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ApplicationInformationReleaseHistory}" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<!-- ─── 快速工具列(可自訂:選單「快速列 → 自訂按鈕」勾選)─── -->
|
||||||
<ToolBarTray DockPanel.Dock="Top">
|
<ToolBarTray DockPanel.Dock="Top">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Command="{Binding ImportCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ImportCommand}"
|
||||||
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
Visibility="{Binding QuickBar[Import].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<Button Command="{Binding ZoomAllCommand}" Padding="8,4" FontSize="13"
|
ToolTip="{loc:Loc Ui.Main.ImportSTPSTEPSTLDXFFiles}" Content="{loc:Loc Ui.Main.ToolbarImport}" />
|
||||||
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
<Button Style="{StaticResource QuickButton}" Click="Recent_Click" Padding="4,4"
|
||||||
<Separator />
|
Visibility="{Binding QuickBar[Recent].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<TextBlock Text="量測:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
ToolTip="{loc:Loc Ui.Main.RecentlyOpenedFiles}">▾</Button>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點)"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ZoomAllCommand}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}">📍 點</ToggleButton>
|
Visibility="{Binding QuickBar[ZoomAll].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ"
|
ToolTip="{loc:Loc Ui.Main.FitAllModelsView}" Content="{loc:Loc Ui.Main.Fit}" />
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}">📏 距離</ToggleButton>
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso"
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)"
|
Visibility="{Binding QuickBar[ViewIso].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}">📐 邊</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.IsometricView}" Content="{loc:Loc Ui.Main.Iso}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}">⬛ 面</ToggleButton>
|
Visibility="{Binding QuickBar[ViewFront].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長"
|
ToolTip="{loc:Loc Ui.Main.FrontViewLookAlongYZ}" Content="{loc:Loc Ui.Main.Front}" />
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}">⭕ 圓</ToggleButton>
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)"
|
Visibility="{Binding QuickBar[ViewTop].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}">∠ 角度</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.TopViewLookDownFromZ}" Content="{loc:Loc Ui.Main.Top}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}">⇔ 面距</ToggleButton>
|
Visibility="{Binding QuickBar[ViewRight].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<Separator />
|
ToolTip="{loc:Loc Ui.Main.RightViewLookAlongXZ}" Content="{loc:Loc Ui.Main.Right}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding OrthographicView}"
|
||||||
|
Visibility="{Binding QuickBar[Ortho].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.OrthographicProjectionAvoidsPerspectiveDistortionClick}" Content="{loc:Loc Ui.Main.Ortho}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.PointCoordinatesVertexCircleCenterSnapping}"
|
||||||
|
Visibility="{Binding QuickBar[Point].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}" Content="{loc:Loc Ui.Main.Point}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.DistanceXYZCircleCenterSnappingHole}"
|
||||||
|
Visibility="{Binding QuickBar[Distance].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}" Content="{loc:Loc Ui.Main.Distance}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.StraightCurvedCircularEdgeLengthShortcut}"
|
||||||
|
Visibility="{Binding QuickBar[Edge].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}" Content="{loc:Loc Ui.Main.Edge}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.AreaSurfaceTypeNormalVectorShortcut}"
|
||||||
|
Visibility="{Binding QuickBar[Face].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}" Content="{loc:Loc Ui.Main.Face}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.CenterRadiusDiameterCircumferenceShortcutC}"
|
||||||
|
Visibility="{Binding QuickBar[Circle].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}" Content="{loc:Loc Ui.Main.Circle}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.AngleBetweenFacesStraightEdgesShortcut}"
|
||||||
|
Visibility="{Binding QuickBar[Angle].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}" Content="{loc:Loc Ui.Main.Angle}" />
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.ShortestFaceDistanceMeshApproximationShortcut}"
|
||||||
|
Visibility="{Binding QuickBar[FaceDistance].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}" Content="{loc:Loc Ui.Main.FaceDistance}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="兩點對齊(平移零件):先點要移動零件上的點,再點目標點"
|
ToolTip="{loc:Loc Ui.Main.TwoPointAlignmentPickPointMoving}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}">🎯 對齊</ToggleButton>
|
Visibility="{Binding QuickBar[Align].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}" Content="{loc:Loc Ui.Main.Align}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="三點對齊(旋轉+平移):先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)"
|
ToolTip="{loc:Loc Ui.Main.ThreePointAlignmentPickThreePoints}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}">🎯 三點</ToggleButton>
|
Visibility="{Binding QuickBar[Align3].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<Separator />
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}" Content="{loc:Loc Ui.Main.3Point}" />
|
||||||
<TextBlock Text="旋轉:" VerticalAlignment="Center" Margin="4,0,2,0"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
||||||
ToolTip="對樹面板選取的檔案,繞其中心旋轉 +90°" />
|
Visibility="{Binding QuickBar[RotateX].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="X" Padding="6,4" FontSize="13"
|
ToolTip="{loc:Loc Ui.Main.RotateModelSelectedTree90Around}">↻X</Button>
|
||||||
ToolTip="選取的檔案繞 X 軸 +90°(先在樹面板點選檔案)">↻X</Button>
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Y"
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="Y" Padding="6,4" FontSize="13"
|
Visibility="{Binding QuickBar[RotateY].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="選取的檔案繞 Y 軸 +90°">↻Y</Button>
|
ToolTip="{loc:Loc Ui.Main.RotateSelectedModel90AroundY}">↻Y</Button>
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="Z" Padding="6,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Z"
|
||||||
ToolTip="選取的檔案繞 Z 軸 +90°">↻Z</Button>
|
Visibility="{Binding QuickBar[RotateZ].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.RotateSelectedModel90AroundZ}">↻Z</Button>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="拖曳模式:左鍵按住零件沿螢幕平面拖動,放開定位(深度方向先轉視角再拖)"
|
ToolTip="{loc:Loc Ui.Main.DragModeHoldLeftMouseButton}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}">🖐 拖曳</ToggleButton>
|
Visibility="{Binding QuickBar[Drag].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}" Content="{loc:Loc Ui.Main.Drag}" />
|
||||||
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding GizmoEnabled}"
|
||||||
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[Gizmo].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="干涉檢查:檢查 2 個可見檔案是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
ToolTip="{loc:Loc Ui.Main.3AxisGizmoUseXYZArrows}" Content="{loc:Loc Ui.Main.Gizmo}" />
|
||||||
<Separator />
|
<Button Style="{StaticResource QuickButton}" Command="{Binding CheckInterferenceCommand}"
|
||||||
<Button Command="{Binding ClearMeasurementsCommand}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[Interference].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="清除全部量測與標記">🧹 清除</Button>
|
ToolTip="{loc:Loc Ui.Main.InterferenceCheckTestVisibleModelPairs}" Content="{loc:Loc Ui.Main.Interference}" />
|
||||||
<Separator />
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding SectionEnabled}"
|
||||||
<ToggleButton IsChecked="{Binding UseInch}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[Section].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.ToggleSectionViewItsControls}" Content="{loc:Loc Ui.Main.ToolbarSection}" />
|
||||||
<Separator />
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ClearMeasurementsCommand}"
|
||||||
<Button Command="{Binding ExportCsvCommand}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[Clear].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
ToolTip="{loc:Loc Ui.Main.ClearAllMeasurementsMarkersEscCancels}" Content="{loc:Loc Ui.Main.Clear}" />
|
||||||
<Button Command="{Binding SaveScreenshotCommand}" Padding="8,4" FontSize="13"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding UseInch}"
|
||||||
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
Visibility="{Binding QuickBar[Unit].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.SwitchDisplayUnitsBetweenMmInches}">mm ⇄ in</ToggleButton>
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportCsvCommand}"
|
||||||
|
Visibility="{Binding QuickBar[ExportCsv].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ExportMeasurementResultsCSV}">💾 CSV</Button>
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SaveScreenshotCommand}"
|
||||||
|
Visibility="{Binding QuickBar[Screenshot].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.SavePNGScreenshot3DView2}" Content="{loc:Loc Ui.Main.Capture}" />
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStepFileCommand}"
|
||||||
|
Visibility="{Binding QuickBar[ExportStep].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ToolbarExportStepTooltip}">📤 STEP</Button>
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStlFileCommand}"
|
||||||
|
Visibility="{Binding QuickBar[ExportStl].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.ToolbarExportStlTooltip}">📐 STL</Button>
|
||||||
|
<ComboBox ItemsSource="{Binding Languages}" DisplayMemberPath="Key"
|
||||||
|
SelectedValue="{Binding SelectedLanguage}" SelectedValuePath="Id"
|
||||||
|
Visibility="{Binding QuickBar[Language].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.Language}" Margin="2,0" />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
|
</ToolBarTray>
|
||||||
|
|
||||||
|
<!-- ─── 剖面參數列(只在剖面開啟時出現)─── -->
|
||||||
|
<ToolBarTray DockPanel.Dock="Top"
|
||||||
|
Visibility="{Binding SectionEnabled, Converter={StaticResource BoolToVis}}">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<ToggleButton IsChecked="{Binding SectionEnabled}" Padding="8,4" FontSize="13"
|
<TextBlock Text="{loc:Loc Ui.Main.SectionLabel}" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
ToolTip="開啟/關閉剖面檢視">✂ 剖面</ToggleButton>
|
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="60" Margin="4,0"
|
||||||
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="52" Margin="4,0"
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.SectionAxis3PointsDefineAn}">
|
||||||
VerticalAlignment="Center" ToolTip="剖切軸向"
|
|
||||||
IsEnabled="{Binding SectionEnabled}">
|
|
||||||
<ComboBoxItem>X</ComboBoxItem>
|
<ComboBoxItem>X</ComboBoxItem>
|
||||||
<ComboBoxItem>Y</ComboBoxItem>
|
<ComboBoxItem>Y</ComboBoxItem>
|
||||||
<ComboBoxItem>Z</ComboBoxItem>
|
<ComboBoxItem>Z</ComboBoxItem>
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.Main.3Points}" />
|
||||||
</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="{loc:Loc Ui.Main.SectionPositionAlongNormal}" />
|
||||||
IsEnabled="{Binding SectionEnabled}" />
|
<TextBox Text="{Binding SectionPosition, StringFormat={}{0:F0}}" Width="34"
|
||||||
<TextBlock Text="{Binding SectionPosition, StringFormat={}{0:F0}%}" Width="36"
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.SectionPosition0100EnterValue}" />
|
||||||
VerticalAlignment="Center" />
|
<TextBlock Text="%" VerticalAlignment="Center" Margin="1,0,0,0" />
|
||||||
<CheckBox IsChecked="{Binding SectionFlip}" Content="反向" VerticalAlignment="Center"
|
<CheckBox IsChecked="{Binding SectionFlip}" Content="{loc:Loc Ui.Main.Flip}" VerticalAlignment="Center"
|
||||||
Margin="4,0" ToolTip="切換保留側" IsEnabled="{Binding SectionEnabled}" />
|
Margin="4,0" ToolTip="{loc:Loc Ui.Main.SwitchRetainedSide}" />
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding CloseSectionCommand}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.CloseSectionView}" Content="{loc:Loc Ui.Main.Close}" />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
</ToolBarTray>
|
</ToolBarTray>
|
||||||
|
|
||||||
@@ -111,13 +293,19 @@
|
|||||||
|
|
||||||
<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="{loc:Loc Ui.Main.ModelsAssemblyTree}" Margin="4">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBox DockPanel.Dock="Top" Margin="0,2,0,4"
|
||||||
|
Text="{Binding TreeFilter, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.FilterAssemblyTreeByNameDoes}" />
|
||||||
<TreeView ItemsSource="{Binding Roots}"
|
<TreeView ItemsSource="{Binding Roots}"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
VirtualizingPanel.IsVirtualizing="True"
|
VirtualizingPanel.IsVirtualizing="True"
|
||||||
@@ -125,17 +313,36 @@
|
|||||||
<TreeView.ItemContainerStyle>
|
<TreeView.ItemContainerStyle>
|
||||||
<Style TargetType="TreeViewItem">
|
<Style TargetType="TreeViewItem">
|
||||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||||
|
<Setter Property="Visibility"
|
||||||
|
Value="{Binding IsFilterVisible, Converter={StaticResource BoolToVis}}" />
|
||||||
</Style>
|
</Style>
|
||||||
</TreeView.ItemContainerStyle>
|
</TreeView.ItemContainerStyle>
|
||||||
<TreeView.ItemTemplate>
|
<TreeView.ItemTemplate>
|
||||||
<HierarchicalDataTemplate DataType="{x:Type vm:ModelNodeViewModel}"
|
<HierarchicalDataTemplate DataType="{x:Type vm:ModelNodeViewModel}"
|
||||||
ItemsSource="{Binding Children}">
|
ItemsSource="{Binding Children}">
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,1"
|
<StackPanel Orientation="Horizontal" Margin="0,1"
|
||||||
ToolTip="{Binding ToolTipText}">
|
Background="Transparent"
|
||||||
|
ToolTip="{Binding ToolTipText}"
|
||||||
|
Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||||
|
<StackPanel.ContextMenu>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ShowOnlyThisNode}"
|
||||||
|
Command="{Binding PlacementTarget.Tag.IsolateNodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.InvertVisibility}"
|
||||||
|
Command="{Binding PlacementTarget.Tag.InvertVisibilityCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.ShowAll}"
|
||||||
|
Command="{Binding PlacementTarget.Tag.ShowAllCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="{loc:Loc Ui.Main.MeasureVolumeCentroid}"
|
||||||
|
Command="{Binding PlacementTarget.Tag.MeasureVolumeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
|
CommandParameter="{Binding}" />
|
||||||
|
</ContextMenu>
|
||||||
|
</StackPanel.ContextMenu>
|
||||||
<CheckBox IsChecked="{Binding IsVisible}" VerticalAlignment="Center"
|
<CheckBox IsChecked="{Binding IsVisible}" VerticalAlignment="Center"
|
||||||
ToolTip="顯示 / 隱藏(含子節點)" />
|
ToolTip="{loc:Loc Ui.Main.ShowHideIncludingChildNodes}" />
|
||||||
<Button Width="14" Height="14" Margin="3,0"
|
<Button Width="14" Height="14" Margin="3,0"
|
||||||
VerticalAlignment="Center" ToolTip="點擊換色(含子節點)"
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.ClickChangeColorIncludingChildNodes}"
|
||||||
Command="{Binding CycleColorCommand}">
|
Command="{Binding CycleColorCommand}">
|
||||||
<Button.Template>
|
<Button.Template>
|
||||||
<ControlTemplate TargetType="Button">
|
<ControlTemplate TargetType="Button">
|
||||||
@@ -159,16 +366,16 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</TextBlock.Style>
|
</TextBlock.Style>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<CheckBox IsChecked="{Binding ShowEdges}" Content="邊" FontSize="10"
|
<CheckBox IsChecked="{Binding ShowEdges}" Content="{loc:Loc Ui.Main.Edges}" FontSize="10"
|
||||||
VerticalAlignment="Center" Margin="4,0,0,0"
|
VerticalAlignment="Center" Margin="4,0,0,0"
|
||||||
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="顯示輪廓邊線(邊線量大時轉動視角會變慢)" />
|
ToolTip="{loc:Loc Ui.Main.ShowOutlineEdgesManyEdgesCan}" />
|
||||||
<Button Content="🔍" Width="20" Margin="3,0,0,0" Padding="0"
|
<Button Content="🔍" Width="20" Margin="3,0,0,0" Padding="0"
|
||||||
VerticalAlignment="Center" ToolTip="縮放至此節點"
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.ZoomThisNode}"
|
||||||
Command="{Binding DataContext.ZoomNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding DataContext.ZoomNodeCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="{Binding}" />
|
CommandParameter="{Binding}" />
|
||||||
<Button Content="✕" Width="20" Padding="0" Margin="2,0,0,0"
|
<Button Content="✕" Width="20" Padding="0" Margin="2,0,0,0"
|
||||||
VerticalAlignment="Center" ToolTip="移除此檔案"
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.RemoveThisFile}"
|
||||||
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding IsRoot, Converter={StaticResource BoolToVis}}"
|
||||||
Command="{Binding DataContext.RemoveRootCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding DataContext.RemoveRootCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="{Binding}" />
|
CommandParameter="{Binding}" />
|
||||||
@@ -176,10 +383,15 @@
|
|||||||
</HierarchicalDataTemplate>
|
</HierarchicalDataTemplate>
|
||||||
</TreeView.ItemTemplate>
|
</TreeView.ItemTemplate>
|
||||||
</TreeView>
|
</TreeView>
|
||||||
|
</DockPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
|
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
|
ToolTip="{loc:Loc Ui.Main.DragResizePanel}" />
|
||||||
|
|
||||||
<!-- ─── 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 +420,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="{loc:Loc Ui.Main.DragResizePanel}" />
|
||||||
|
|
||||||
<!-- ─── 量測結果面板 ─── -->
|
<!-- ─── 量測結果面板 ─── -->
|
||||||
<GroupBox Grid.Column="2" Header="量測結果" Margin="4">
|
<GroupBox Grid.Column="4" Header="{loc:Loc Ui.Main.Measurements}" Margin="4">
|
||||||
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
@@ -228,7 +444,7 @@
|
|||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1" Content="✕" Width="22" Height="22"
|
<Button Grid.Column="1" Content="✕" Width="22" Height="22"
|
||||||
VerticalAlignment="Top" ToolTip="刪除此筆量測"
|
VerticalAlignment="Top" ToolTip="{loc:Loc Ui.Main.DeleteThisMeasurement}"
|
||||||
Command="{Binding DataContext.RemoveMeasurementCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
Command="{Binding DataContext.RemoveMeasurementCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
CommandParameter="{Binding}" />
|
CommandParameter="{Binding}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -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,23 +13,80 @@ 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()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
// 版號單一來源 = csproj <Version>;標題執行時讀,避免寫死
|
// 版號單一來源 = csproj <Version>;標題執行時讀,避免寫死
|
||||||
var v = Assembly.GetExecutingAssembly().GetName().Version!;
|
UpdateTitle();
|
||||||
Title = $"STPViewer V{v.Major}.{v.Minor}.{v.Build} — STP/STEP 3D 檢視器";
|
|
||||||
DataContext = _vm;
|
DataContext = _vm;
|
||||||
_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);
|
||||||
|
_vm.InitializeLocalization();
|
||||||
|
LocalizationService.Instance.LanguageChanged += LanguageChanged;
|
||||||
|
Closed += (_, _) =>
|
||||||
|
{
|
||||||
|
LocalizationService.Instance.LanguageChanged -= LanguageChanged;
|
||||||
|
_vm.DisposeLocalization();
|
||||||
|
};
|
||||||
|
|
||||||
// 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 LanguageChanged(object? sender, System.EventArgs e) => UpdateTitle();
|
||||||
|
|
||||||
|
private void UpdateTitle()
|
||||||
|
{
|
||||||
|
var v = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
Title = LocalizationService.Format("App.WindowTitle", $"{v.Major}.{v.Minor}.{v.Build}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +96,50 @@ 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 = LocalizationService.Get("App.NoRecentFiles"), 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 About_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
new AboutDialog { Owner = this }.ShowDialog();
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace STPViewer.Models;
|
||||||
|
|
||||||
|
public class LanguageDefinition : ObservableObject
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
public string Key { get; }
|
||||||
|
public IReadOnlyDictionary<string, string> Data { get; }
|
||||||
|
public bool IsSelected { get; private set; }
|
||||||
|
|
||||||
|
internal LanguageDefinition(string id, string key, IReadOnlyDictionary<string, string> data)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Key = key;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetSelected(bool selected)
|
||||||
|
{
|
||||||
|
IsSelected = selected;
|
||||||
|
OnPropertyChanged(nameof(IsSelected));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,4 +15,5 @@ public enum MeasureMode
|
|||||||
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
||||||
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
||||||
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
||||||
|
Volume, // 體積/質心(樹右鍵選單觸發,非點擊模式,僅作為結果類型)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ public class MeasurementResult : ObservableObject
|
|||||||
{
|
{
|
||||||
if (_unit == unit) return;
|
if (_unit == unit) return;
|
||||||
_unit = unit;
|
_unit = unit;
|
||||||
|
RefreshText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshText()
|
||||||
|
{
|
||||||
OnPropertyChanged(nameof(Title));
|
OnPropertyChanged(nameof(Title));
|
||||||
OnPropertyChanged(nameof(Detail));
|
OnPropertyChanged(nameof(Detail));
|
||||||
foreach ((BillboardTextVisual3D label, Func<UnitSystem, string> textFor) in DynamicLabels)
|
foreach ((BillboardTextVisual3D label, Func<UnitSystem, string> textFor) in DynamicLabels)
|
||||||
label.Text = textFor(unit);
|
label.Text = textFor(_unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.8.0</Version> <!-- Shared by the window title and publish output. -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -15,4 +15,8 @@
|
|||||||
<PackageReference Include="HelixToolkit.Wpf" Version="3.1.2" />
|
<PackageReference Include="HelixToolkit.Wpf" Version="3.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="i18n\*.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using STPViewer.Models;
|
||||||
|
|
||||||
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
|
public class LocalizationService : ObservableObject
|
||||||
|
{
|
||||||
|
public static LocalizationService Instance { get; } = new(Path.Combine(AppContext.BaseDirectory, "i18n"));
|
||||||
|
|
||||||
|
private LanguageDefinition? _selected;
|
||||||
|
private readonly LanguageDefinition? _english;
|
||||||
|
|
||||||
|
public IReadOnlyList<LanguageDefinition> Languages { get; }
|
||||||
|
public string Language => _selected?.Id ?? "";
|
||||||
|
public event EventHandler? LanguageChanged;
|
||||||
|
|
||||||
|
public LocalizationService(string directory)
|
||||||
|
{
|
||||||
|
Languages = LoadLanguages(directory);
|
||||||
|
_english = Languages.FirstOrDefault(language => language.Id == "en");
|
||||||
|
_selected = Languages.FirstOrDefault(language => language.Id == ResolveLanguage(null));
|
||||||
|
foreach (LanguageDefinition language in Languages)
|
||||||
|
language.SetSelected(ReferenceEquals(language, _selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[string key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_selected?.Data.TryGetValue(key, out string? value) == true) return value;
|
||||||
|
if (_english?.Data.TryGetValue(key, out value) == true) return value;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Get(string key) => Instance[key];
|
||||||
|
|
||||||
|
public static string Format(string key, params object[] args) => Instance.FormatText(key, args);
|
||||||
|
|
||||||
|
public string FormatText(string key, params object[] args)
|
||||||
|
{
|
||||||
|
string text = this[key];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, text, args);
|
||||||
|
}
|
||||||
|
catch (FormatException ex)
|
||||||
|
{
|
||||||
|
Trace.TraceWarning($"Invalid translation format '{key}' in '{Language}': {ex.Message}");
|
||||||
|
if (_english?.Data.TryGetValue(key, out string? fallback) == true && fallback != text)
|
||||||
|
{
|
||||||
|
try { return string.Format(CultureInfo.CurrentCulture, fallback, args); }
|
||||||
|
catch (FormatException) { }
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ResolveLanguage(string? language)
|
||||||
|
{
|
||||||
|
string id = NormalizeId(language ?? CultureInfo.CurrentUICulture.Name);
|
||||||
|
if (Languages.Any(item => item.Id == id)) return id;
|
||||||
|
|
||||||
|
string[] parts = id.Split('_');
|
||||||
|
if (parts[0] == "zh")
|
||||||
|
{
|
||||||
|
bool traditional = parts.Any(part => part is "hant" or "tw" or "hk" or "mo");
|
||||||
|
string[] candidates = traditional ? ["zh_tw", "zh_hant", "zh"] : ["zh_cn", "zh_hans", "zh"];
|
||||||
|
foreach (string candidate in candidates)
|
||||||
|
if (Languages.Any(item => item.Id == candidate)) return candidate;
|
||||||
|
return _english?.Id ?? Languages.FirstOrDefault()?.Id ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
LanguageDefinition? match = Languages.FirstOrDefault(item => item.Id == parts[0]);
|
||||||
|
return match?.Id ?? _english?.Id ?? Languages.FirstOrDefault()?.Id ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLanguage(string? language)
|
||||||
|
{
|
||||||
|
string id = ResolveLanguage(language);
|
||||||
|
LanguageDefinition? selected = Languages.FirstOrDefault(item => item.Id == id);
|
||||||
|
bool changed = !ReferenceEquals(_selected, selected);
|
||||||
|
_selected = selected;
|
||||||
|
|
||||||
|
foreach (LanguageDefinition item in Languages)
|
||||||
|
item.SetSelected(ReferenceEquals(item, selected));
|
||||||
|
|
||||||
|
if (selected is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CultureInfo culture = CultureInfo.GetCultureInfo(selected.Id.Replace('_', '-'));
|
||||||
|
CultureInfo.CurrentUICulture = culture;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||||
|
}
|
||||||
|
catch (CultureNotFoundException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
OnPropertyChanged(nameof(Language));
|
||||||
|
OnPropertyChanged("Item[]");
|
||||||
|
LanguageChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeId(string id) => id.Trim().Replace('-', '_').ToLowerInvariant();
|
||||||
|
|
||||||
|
private static IReadOnlyList<LanguageDefinition> LoadLanguages(string directory)
|
||||||
|
{
|
||||||
|
var languages = new List<LanguageDefinition>();
|
||||||
|
if (!Directory.Exists(directory)) return languages;
|
||||||
|
|
||||||
|
string[] paths;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
paths = Directory.GetFiles(directory, "*.json");
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
Trace.TraceWarning($"Cannot read language directory '{directory}': {ex.Message}");
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string path in paths.OrderBy(path => path, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LanguageFile? file = JsonSerializer.Deserialize<LanguageFile>(File.ReadAllText(path));
|
||||||
|
string id = NormalizeId(Path.GetFileNameWithoutExtension(path));
|
||||||
|
if (string.IsNullOrWhiteSpace(file?.Key) || file.Data is null || id.Length == 0 ||
|
||||||
|
file.Data.Any(pair => string.IsNullOrWhiteSpace(pair.Key) || pair.Value is null))
|
||||||
|
throw new JsonException("Expected a display key and a data object containing string translations.");
|
||||||
|
if (languages.Any(language => language.Id == id))
|
||||||
|
throw new JsonException($"Duplicate language identifier '{id}'.");
|
||||||
|
|
||||||
|
languages.Add(new LanguageDefinition(id, file.Key, file.Data));
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
||||||
|
{
|
||||||
|
Trace.TraceWarning($"Cannot load language file '{path}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LanguageFile
|
||||||
|
{
|
||||||
|
[JsonPropertyName("key")]
|
||||||
|
public string? Key { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public Dictionary<string, string>? Data { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using CADability;
|
|||||||
using CADability.GeoObject;
|
using CADability.GeoObject;
|
||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.Services;
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
@@ -31,7 +32,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;
|
||||||
@@ -46,7 +52,23 @@ public class MeasurementService
|
|||||||
if (d < bestD) { bestD = d; best = p; }
|
if (d < bestD) { bestD = d; best = p; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +92,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Vector3D dir = ToP3(ln.EndPoint) - ToP3(ln.StartPoint);
|
Vector3D dir = ToP3(ln.EndPoint) - ToP3(ln.StartPoint);
|
||||||
dir.Normalize();
|
dir.Normalize();
|
||||||
return new DirectionPick(p, dir, "邊方向");
|
return new DirectionPick(p, dir, "Runtime.DirectionEdge");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,14 +103,14 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
GeoPoint2D uv = fi.BrepFace.Surface.PositionOf(ToGeo(hit));
|
GeoPoint2D uv = fi.BrepFace.Surface.PositionOf(ToGeo(hit));
|
||||||
GeoVector n = fi.BrepFace.Surface.GetNormal(uv).Normalized;
|
GeoVector n = fi.BrepFace.Surface.GetNormal(uv).Normalized;
|
||||||
return new DirectionPick(hit, new Vector3D(n.x, n.y, n.z), "面法向量");
|
return new DirectionPick(hit, new Vector3D(n.x, n.y, n.z), "Runtime.DirectionFace");
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
Vector3D mn = meshNormal;
|
Vector3D mn = meshNormal;
|
||||||
if (mn.LengthSquared < 1e-12) mn = new Vector3D(0, 0, 1);
|
if (mn.LengthSquared < 1e-12) mn = new Vector3D(0, 0, 1);
|
||||||
mn.Normalize();
|
mn.Normalize();
|
||||||
return new DirectionPick(hit, mn, "面法向量(網格)");
|
return new DirectionPick(hit, mn, "Runtime.DirectionMeshFace");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 點 / 距離 ───────────────────────────────────────────────
|
// ─── 點 / 距離 ───────────────────────────────────────────────
|
||||||
@@ -113,8 +135,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.Distance,
|
Kind = MeasureMode.Distance,
|
||||||
TitleFor = u => $"{label} {Units.L(d.Length, u)}",
|
TitleFor = u => $"{label} {Units.L(d.Length, u)}",
|
||||||
DetailFor = u => $"距離 = {Units.L(d.Length, u)}\nΔX = {Units.L(d.X, u)}\nΔY = {Units.L(d.Y, u)}\nΔZ = {Units.L(d.Z, u)}\n" +
|
DetailFor = u => Format("Runtime.DistanceDetail", Units.L(d.Length, u), Units.L(d.X, u), Units.L(d.Y, u), Units.L(d.Z, u), Units.P(p1, u), Units.P(p2, u)),
|
||||||
$"P1 {Units.P(p1, u)}\nP2 {Units.P(p2, u)}",
|
|
||||||
};
|
};
|
||||||
r.Overlays.Add(Sphere(p1, markerR));
|
r.Overlays.Add(Sphere(p1, markerR));
|
||||||
r.Overlays.Add(Sphere(p2, markerR));
|
r.Overlays.Add(Sphere(p2, markerR));
|
||||||
@@ -172,9 +193,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.Circle,
|
Kind = MeasureMode.Circle,
|
||||||
TitleFor = u => $"{label} ⌀{Units.L(radius * 2, u)}",
|
TitleFor = u => $"{label} ⌀{Units.L(radius * 2, u)}",
|
||||||
DetailFor = u => $"直徑 = {Units.L(radius * 2, u)}\n半徑 = {Units.L(radius, u)}\n" +
|
DetailFor = u => Format("Runtime.CircleDetail", Units.L(radius * 2, u), Units.L(radius, u), Get(full ? "Runtime.Circumference" : "Runtime.ArcLength"), Units.L(curveLen, u), Units.P(center, u)),
|
||||||
$"{(full ? "周長" : "弧長")} = {Units.L(curveLen, u)}\n" +
|
|
||||||
$"圓心 {Units.P(center, u)}",
|
|
||||||
};
|
};
|
||||||
r.Overlays.Add(Polyline(curve, HighlightColor, 3));
|
r.Overlays.Add(Polyline(curve, HighlightColor, 3));
|
||||||
r.Overlays.Add(Sphere(center, markerR));
|
r.Overlays.Add(Sphere(center, markerR));
|
||||||
@@ -198,8 +217,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.Edge,
|
Kind = MeasureMode.Edge,
|
||||||
TitleFor = u => $"{label} L = {Units.L(len, u)}",
|
TitleFor = u => $"{label} L = {Units.L(len, u)}",
|
||||||
DetailFor = u => $"長度 = {Units.L(len, u)}\nΔX = {Units.C(d.X, u)} ΔY = {Units.C(d.Y, u)} ΔZ = {Units.C(d.Z, u)}\n" +
|
DetailFor = u => Format("Runtime.LineDetail", Units.L(len, u), Units.C(d.X, u), Units.C(d.Y, u), Units.C(d.Z, u), Units.P(s, u), Units.P(e, u)),
|
||||||
$"起點 {Units.P(s, u)}\n終點 {Units.P(e, u)}",
|
|
||||||
};
|
};
|
||||||
r.Overlays.Add(new LinesVisual3D
|
r.Overlays.Add(new LinesVisual3D
|
||||||
{
|
{
|
||||||
@@ -220,7 +238,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.Edge,
|
Kind = MeasureMode.Edge,
|
||||||
TitleFor = u => $"{label} L = {Units.L(len, u)}",
|
TitleFor = u => $"{label} L = {Units.L(len, u)}",
|
||||||
DetailFor = u => $"曲線長 = {Units.L(len, u)}\n類型 = {typeName}",
|
DetailFor = u => Format("Runtime.CurveDetail", Units.L(len, u), typeName),
|
||||||
};
|
};
|
||||||
r.Overlays.Add(Polyline(curve, HighlightColor, 3));
|
r.Overlays.Add(Polyline(curve, HighlightColor, 3));
|
||||||
AddLabel(r, ToP3(curve.PointAt(0.5)) + new Vector3D(0, 0, markerR * 2), u => $"L={Units.L(len, u)}");
|
AddLabel(r, ToP3(curve.PointAt(0.5)) + new Vector3D(0, 0, markerR * 2), u => $"L={Units.L(len, u)}");
|
||||||
@@ -236,13 +254,13 @@ public class MeasurementService
|
|||||||
int triCount = fi.Mesh.TriangleIndices.Count / 3;
|
int triCount = fi.Mesh.TriangleIndices.Count / 3;
|
||||||
Func<UnitSystem, string> surfaceDesc = fi.BrepFace is not null
|
Func<UnitSystem, string> surfaceDesc = fi.BrepFace is not null
|
||||||
? DescribeSurface(fi.BrepFace.Surface)
|
? DescribeSurface(fi.BrepFace.Surface)
|
||||||
: _ => "類型 = 網格(無 B-rep)";
|
: _ => Get("Runtime.MeshSurface");
|
||||||
|
|
||||||
var r = new MeasurementResult
|
var r = new MeasurementResult
|
||||||
{
|
{
|
||||||
Kind = MeasureMode.Face,
|
Kind = MeasureMode.Face,
|
||||||
TitleFor = u => $"{label} A ≈ {Units.A(area, u)}",
|
TitleFor = u => $"{label} A ≈ {Units.A(area, u)}",
|
||||||
DetailFor = u => $"面積 ≈ {Units.A(area, u)}(網格近似)\n{surfaceDesc(u)}\n三角形數 = {triCount:N0}",
|
DetailFor = u => Format("Runtime.FaceDetail", Units.A(area, u), surfaceDesc(u), triCount),
|
||||||
};
|
};
|
||||||
|
|
||||||
// overlay:外輪廓 highlight + 標籤
|
// overlay:外輪廓 highlight + 標籤
|
||||||
@@ -281,31 +299,29 @@ public class MeasurementService
|
|||||||
case PlaneSurface ps:
|
case PlaneSurface ps:
|
||||||
{
|
{
|
||||||
GeoVector n = ps.Normal.Normalized;
|
GeoVector n = ps.Normal.Normalized;
|
||||||
string desc = $"類型 = 平面\n法向量 ({n.x:F3}, {n.y:F3}, {n.z:F3})";
|
return _ => Format("Runtime.PlaneSurface", n.x, n.y, n.z);
|
||||||
return _ => desc;
|
|
||||||
}
|
}
|
||||||
case CylindricalSurface cs:
|
case CylindricalSurface cs:
|
||||||
{
|
{
|
||||||
GeoVector ax = cs.Axis.Normalized;
|
GeoVector ax = cs.Axis.Normalized;
|
||||||
double radius = cs.RadiusX;
|
double radius = cs.RadiusX;
|
||||||
return u => $"類型 = 圓柱面\n半徑 = {Units.L(radius, u)}\n軸向 ({ax.x:F3}, {ax.y:F3}, {ax.z:F3})";
|
return u => Format("Runtime.CylinderSurface", Units.L(radius, u), ax.x, ax.y, ax.z);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
string desc = $"類型 = {SurfaceTypeName(surface)}";
|
return _ => Format("Runtime.SurfaceType", SurfaceTypeName(surface));
|
||||||
return _ => desc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SurfaceTypeName(ISurface s) => s.GetType().Name switch
|
private static string SurfaceTypeName(ISurface s) => s.GetType().Name switch
|
||||||
{
|
{
|
||||||
"SphericalSurface" => "球面",
|
"SphericalSurface" => Get("Runtime.SphericalSurface"),
|
||||||
"ToroidalSurface" => "環面",
|
"ToroidalSurface" => Get("Runtime.ToroidalSurface"),
|
||||||
"ConicalSurface" => "圓錐面",
|
"ConicalSurface" => Get("Runtime.ConicalSurface"),
|
||||||
"NurbsSurface" => "NURBS 曲面",
|
"NurbsSurface" => Get("Runtime.NurbsSurface"),
|
||||||
"SurfaceOfRevolution" => "旋轉曲面",
|
"SurfaceOfRevolution" => Get("Runtime.RevolutionSurface"),
|
||||||
"SurfaceOfLinearExtrusion" => "拉伸曲面",
|
"SurfaceOfLinearExtrusion" => Get("Runtime.ExtrusionSurface"),
|
||||||
var other => other,
|
var other => other,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -322,10 +338,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.Angle,
|
Kind = MeasureMode.Angle,
|
||||||
TitleFor = _ => $"{label} ∠ {deg:F2}°",
|
TitleFor = _ => $"{label} ∠ {deg:F2}°",
|
||||||
DetailFor = _ => $"夾角 = {deg:F2}°(補角 {comp:F2}°)\n" +
|
DetailFor = _ => Format("Runtime.AngleDetail", deg, comp, Get(a.Desc), a.Direction.X, a.Direction.Y, a.Direction.Z, Get(b.Desc), b.Direction.X, b.Direction.Y, b.Direction.Z),
|
||||||
$"方向 1:{a.Desc} ({a.Direction.X:F3}, {a.Direction.Y:F3}, {a.Direction.Z:F3})\n" +
|
|
||||||
$"方向 2:{b.Desc} ({b.Direction.X:F3}, {b.Direction.Y:F3}, {b.Direction.Z:F3})\n" +
|
|
||||||
"(兩面夾角以法向量計,互補角請參考補角值)",
|
|
||||||
};
|
};
|
||||||
r.Overlays.Add(Sphere(a.At, markerR));
|
r.Overlays.Add(Sphere(a.At, markerR));
|
||||||
r.Overlays.Add(Sphere(b.At, markerR));
|
r.Overlays.Add(Sphere(b.At, markerR));
|
||||||
@@ -357,9 +370,7 @@ public class MeasurementService
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.FaceDistance,
|
Kind = MeasureMode.FaceDistance,
|
||||||
TitleFor = u => $"{label} min = {Units.L(d.Length, u)}",
|
TitleFor = u => $"{label} min = {Units.L(d.Length, u)}",
|
||||||
DetailFor = u => $"最短距離 ≈ {Units.L(d.Length, u)}(網格近似)\n" +
|
DetailFor = u => Format("Runtime.FaceDistanceDetail", Units.L(d.Length, u), Units.L(d.X, u), Units.L(d.Y, u), Units.L(d.Z, u), Units.P(pa, u), Units.P(pb, u)),
|
||||||
$"ΔX = {Units.L(d.X, u)}\nΔY = {Units.L(d.Y, u)}\nΔZ = {Units.L(d.Z, u)}\n" +
|
|
||||||
$"點 1 {Units.P(pa, u)}\n點 2 {Units.P(pb, u)}",
|
|
||||||
};
|
};
|
||||||
r.Overlays.Add(Sphere(pa, markerR));
|
r.Overlays.Add(Sphere(pa, markerR));
|
||||||
r.Overlays.Add(Sphere(pb, markerR));
|
r.Overlays.Add(Sphere(pb, markerR));
|
||||||
@@ -406,6 +417,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,54 @@
|
|||||||
|
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 string? Language { get; set; }
|
||||||
|
public List<string> RecentFiles { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>快速工具列顯示的按鈕 key 清單;null = 用預設(QuickBarViewModel.Registry)</summary>
|
||||||
|
public List<string>? QuickBarKeys { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using System.Windows.Media.Media3D;
|
|||||||
using CADability;
|
using CADability;
|
||||||
using CADability.GeoObject;
|
using CADability.GeoObject;
|
||||||
using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
|
using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.Services;
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ public record ImportedFace(Face? BrepFace, MeshGeometry3D Mesh);
|
|||||||
public class ImportedNode
|
public class ImportedNode
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
|
public string? NameKey { get; set; }
|
||||||
|
public int NameNumber { get; set; }
|
||||||
public List<ImportedNode> Children { get; } = new();
|
public List<ImportedNode> Children { get; } = new();
|
||||||
|
|
||||||
// ── leaf 幾何 ──
|
// ── leaf 幾何 ──
|
||||||
@@ -46,6 +49,13 @@ public class StepImportService
|
|||||||
{
|
{
|
||||||
/// <summary>匯入階段回報(耗時分解),UI 可顯示於狀態列;null 則靜默</summary>
|
/// <summary>匯入階段回報(耗時分解),UI 可顯示於狀態列;null 則靜默</summary>
|
||||||
public Action<string>? Progress { get; set; }
|
public Action<string>? Progress { get; set; }
|
||||||
|
public Action<Func<string>>? ProgressTextFor { get; set; }
|
||||||
|
|
||||||
|
private void ReportProgress(Func<string> textFor)
|
||||||
|
{
|
||||||
|
Progress?.Invoke(textFor());
|
||||||
|
ProgressTextFor?.Invoke(textFor);
|
||||||
|
}
|
||||||
|
|
||||||
private long _triTicks, _edgeTicks; // 三角化 / 邊取樣 累計耗時(TimeSpan ticks,避免逐面 ms 截斷歸零)
|
private long _triTicks, _edgeTicks; // 三角化 / 邊取樣 累計耗時(TimeSpan ticks,避免逐面 ms 截斷歸零)
|
||||||
private double TriMs => new TimeSpan(_triTicks).TotalMilliseconds;
|
private double TriMs => new TimeSpan(_triTicks).TotalMilliseconds;
|
||||||
@@ -61,7 +71,7 @@ public class StepImportService
|
|||||||
public ImportedFileData Import(string filePath)
|
public ImportedFileData Import(string filePath)
|
||||||
{
|
{
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
throw new FileNotFoundException("找不到檔案", filePath);
|
throw new FileNotFoundException(Get("Runtime.FileNotFound"), filePath);
|
||||||
|
|
||||||
_triTicks = _edgeTicks = 0;
|
_triTicks = _edgeTicks = 0;
|
||||||
_leafWork.Clear();
|
_leafWork.Clear();
|
||||||
@@ -73,17 +83,18 @@ public class StepImportService
|
|||||||
case ".stl": ImportStlFile(filePath, root); break;
|
case ".stl": ImportStlFile(filePath, root); break;
|
||||||
case ".dxf": ImportDxfFile(filePath, root); break;
|
case ".dxf": ImportDxfFile(filePath, root); break;
|
||||||
default:
|
default:
|
||||||
throw new InvalidDataException("不支援的格式(支援 .stp / .step / .stl / .dxf;CADability 無 IGES reader)");
|
throw new InvalidDataException(Get("Runtime.UnsupportedFormat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_leafWork.Count > 0)
|
if (_leafWork.Count > 0)
|
||||||
{
|
{
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
Progress?.Invoke($"三角化 {_leafWork.Count} 個零件(平行)…");
|
int leafCount = _leafWork.Count;
|
||||||
|
ReportProgress(() => Format("Runtime.Triangulating", leafCount));
|
||||||
System.Threading.Tasks.Parallel.ForEach(_leafWork, w =>
|
System.Threading.Tasks.Parallel.ForEach(_leafWork, w =>
|
||||||
{
|
{
|
||||||
try { w(); }
|
try { w(); }
|
||||||
catch { /* 個別零件失敗不讓整檔失敗 */ }
|
catch { }
|
||||||
});
|
});
|
||||||
_leafWork.Clear();
|
_leafWork.Clear();
|
||||||
|
|
||||||
@@ -96,14 +107,16 @@ public class StepImportService
|
|||||||
FinishLeaf(leaf, edgeSet);
|
FinishLeaf(leaf, edgeSet);
|
||||||
}
|
}
|
||||||
_retry.Clear();
|
_retry.Clear();
|
||||||
Progress?.Invoke($"幾何處理 {sw.ElapsedMilliseconds:N0} ms" +
|
long elapsed = sw.ElapsedMilliseconds;
|
||||||
$"(三角化 {TriMs:N0} ms、邊取樣 {EdgeMs:N0} ms{(retried > 0 ? $"、重試補回 {retried} 面" : "")})");
|
int retryCount = retried;
|
||||||
|
ReportProgress(() => Format("Runtime.GeometryProcessed", elapsed, TriMs, EdgeMs,
|
||||||
|
retryCount > 0 ? Format("Runtime.RetriedFaces", retryCount) : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
Prune(root);
|
Prune(root);
|
||||||
Aggregate(root);
|
Aggregate(root);
|
||||||
if (root.SolidCount == 0 && root.FaceCount == 0 && CountSegments(root) == 0)
|
if (root.SolidCount == 0 && root.FaceCount == 0 && CountSegments(root) == 0)
|
||||||
throw new InvalidDataException("檔案內沒有可顯示的幾何");
|
throw new InvalidDataException(Get("Runtime.NoGeometry"));
|
||||||
return new ImportedFileData(filePath, root);
|
return new ImportedFileData(filePath, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,55 +127,56 @@ public class StepImportService
|
|||||||
|
|
||||||
private void ImportStepFile(string path, ImportedNode root)
|
private void ImportStepFile(string path, ImportedNode root)
|
||||||
{
|
{
|
||||||
Progress?.Invoke("解析 STEP(大檔可能需數分鐘)…");
|
ReportProgress(() => Get("Runtime.ParsingStep"));
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
var importer = new ImportStep { HierarchyToBlocks = true };
|
var importer = new ImportStep { HierarchyToBlocks = true };
|
||||||
GeoObjectList list = importer.Read(path)
|
GeoObjectList list = importer.Read(path)
|
||||||
?? throw new InvalidDataException("CADability 無法解析此 STEP 檔");
|
?? throw new InvalidDataException(Get("Runtime.StepParseFailed"));
|
||||||
Progress?.Invoke($"STEP 解析 {sw.ElapsedMilliseconds:N0} ms");
|
long elapsed = sw.ElapsedMilliseconds;
|
||||||
|
ReportProgress(() => Format("Runtime.StepParsed", elapsed));
|
||||||
BuildChildren(root, EnumerateGeo(list));
|
BuildChildren(root, EnumerateGeo(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── STL(純網格,無 B-rep;整個 shell 合併成單一 mesh 避免模型數爆炸)──
|
// ─── STL(純網格,無 B-rep)──
|
||||||
|
// 直接解析三角形(StlMeshReader),**不經過 CADability**。CADability 的 ImportSTL 會為每個
|
||||||
|
// 三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),百萬三角形需數十分鐘且吃光記憶體
|
||||||
|
// (實測 100k 三角形:CADability ~41 s,直接解析 ~11 ms)。STL 本就無 B-rep,讀三角形即可。
|
||||||
|
// ⚠️ 因此 STL leaf 沒有 SourceGeos(CADability B-rep)→ 不進 STEP 匯出(本就非 STEP B-rep 幾何);
|
||||||
|
// 剛體變換仍走網格層(TransformRoot 的 SourceGeos 迴圈空轉、mesh 照樣變換),量測走網格近似不受影響。
|
||||||
|
|
||||||
private void ImportStlFile(string path, ImportedNode root)
|
private void ImportStlFile(string path, ImportedNode root)
|
||||||
{
|
{
|
||||||
Shell[] shells = new ImportSTL().Read(path)
|
ReportProgress(() => Get("Runtime.ParsingStl"));
|
||||||
?? throw new InvalidDataException("無法解析 STL 檔");
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
int i = 1;
|
List<StlMeshReader.MeshGroup> groups = StlMeshReader.Read(path);
|
||||||
foreach (Shell sh in shells)
|
if (groups.Count == 0)
|
||||||
{
|
throw new InvalidDataException(Get("Runtime.StlNoTriangles"));
|
||||||
var positions = new Point3DCollection();
|
|
||||||
var indices = new Int32Collection();
|
|
||||||
double precision = PrecisionFor(SafeBounds(() => sh.GetBoundingCube()));
|
|
||||||
foreach (Face f in sh.Faces)
|
|
||||||
{
|
|
||||||
GeoPoint[] pts; int[] ind;
|
|
||||||
try { f.GetTriangulation(precision, out pts, out _, out ind, out _); }
|
|
||||||
catch { continue; }
|
|
||||||
if (pts is null || ind is null) continue;
|
|
||||||
int offset = positions.Count;
|
|
||||||
foreach (GeoPoint p in pts) positions.Add(new Point3D(p.x, p.y, p.z));
|
|
||||||
foreach (int k in ind) indices.Add(offset + k);
|
|
||||||
}
|
|
||||||
if (positions.Count == 0) continue;
|
|
||||||
|
|
||||||
var mesh = new MeshGeometry3D { Positions = positions, TriangleIndices = indices };
|
foreach (StlMeshReader.MeshGroup g in groups)
|
||||||
|
{
|
||||||
|
if (g.Positions.Count == 0) continue;
|
||||||
|
g.Positions.Freeze();
|
||||||
|
g.Indices.Freeze();
|
||||||
|
var mesh = new MeshGeometry3D { Positions = g.Positions, TriangleIndices = g.Indices };
|
||||||
mesh.Freeze();
|
mesh.Freeze();
|
||||||
|
int tris = g.Indices.Count / 3;
|
||||||
var leaf = new ImportedNode
|
var leaf = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = shells.Length == 1 ? "網格" : $"網格 {i}",
|
Name = g.Name,
|
||||||
|
NameKey = g.NameKey,
|
||||||
|
NameNumber = g.NameNumber,
|
||||||
HasBrep = false,
|
HasBrep = false,
|
||||||
SolidCount = 1,
|
SolidCount = 1,
|
||||||
FaceCount = sh.Faces.Length,
|
FaceCount = tris, // STL 無 B-rep 面 → 以三角形數代表
|
||||||
TriangleCount = indices.Count / 3,
|
TriangleCount = tris,
|
||||||
Bounds = BoundsOfMesh(mesh),
|
Bounds = BoundsOfMesh(mesh),
|
||||||
};
|
};
|
||||||
leaf.SourceGeos.Add(sh);
|
|
||||||
leaf.Faces.Add(new ImportedFace(null, mesh));
|
leaf.Faces.Add(new ImportedFace(null, mesh));
|
||||||
root.Children.Add(leaf);
|
root.Children.Add(leaf);
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
long elapsed = sw.ElapsedMilliseconds;
|
||||||
|
int meshCount = root.Children.Count;
|
||||||
|
ReportProgress(() => Format("Runtime.StlParsed", elapsed, meshCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
||||||
@@ -171,7 +185,7 @@ public class StepImportService
|
|||||||
{
|
{
|
||||||
var import = new CADability.DXF.Import(path);
|
var import = new CADability.DXF.Import(path);
|
||||||
Project project = import.Project
|
Project project = import.Project
|
||||||
?? throw new InvalidDataException("無法解析 DXF 檔");
|
?? throw new InvalidDataException(Get("Runtime.DxfParseFailed"));
|
||||||
Model model = project.GetActiveModel();
|
Model model = project.GetActiveModel();
|
||||||
BuildChildren(root, EnumerateGeo(model.AllObjects));
|
BuildChildren(root, EnumerateGeo(model.AllObjects));
|
||||||
}
|
}
|
||||||
@@ -223,7 +237,7 @@ public class StepImportService
|
|||||||
|
|
||||||
if (looseFaces.Count > 0)
|
if (looseFaces.Count > 0)
|
||||||
{
|
{
|
||||||
var leaf = new ImportedNode { Name = "面" };
|
var leaf = new ImportedNode { Name = Get("Runtime.FaceName"), NameKey = "Runtime.FaceName" };
|
||||||
leaf.SourceGeos.AddRange(looseFaces);
|
leaf.SourceGeos.AddRange(looseFaces);
|
||||||
_leafWork.Add(() =>
|
_leafWork.Add(() =>
|
||||||
{
|
{
|
||||||
@@ -247,7 +261,8 @@ public class StepImportService
|
|||||||
segs.Freeze();
|
segs.Freeze();
|
||||||
var leaf = new ImportedNode
|
var leaf = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = "曲線",
|
Name = Get("Runtime.CurveName"),
|
||||||
|
NameKey = "Runtime.CurveName",
|
||||||
HasBrep = false,
|
HasBrep = false,
|
||||||
EdgeSegments = segs,
|
EdgeSegments = segs,
|
||||||
Bounds = BoundsOfPoints(segs),
|
Bounds = BoundsOfPoints(segs),
|
||||||
@@ -264,13 +279,19 @@ public class StepImportService
|
|||||||
if (block.NumChildren == 1 && block.Child(0) is Solid s)
|
if (block.NumChildren == 1 && block.Child(0) is Solid s)
|
||||||
{
|
{
|
||||||
ImportedNode leaf = SolidNode(s);
|
ImportedNode leaf = SolidNode(s);
|
||||||
if (!string.IsNullOrWhiteSpace(block.Name)) leaf.Name = block.Name;
|
if (!string.IsNullOrWhiteSpace(block.Name))
|
||||||
|
{
|
||||||
|
leaf.Name = block.Name;
|
||||||
|
leaf.NameKey = null;
|
||||||
|
leaf.NameNumber = 0;
|
||||||
|
}
|
||||||
return leaf;
|
return leaf;
|
||||||
}
|
}
|
||||||
|
|
||||||
var node = new ImportedNode
|
var node = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = string.IsNullOrWhiteSpace(block.Name) ? "組件" : block.Name,
|
Name = string.IsNullOrWhiteSpace(block.Name) ? Get("Runtime.ComponentName") : block.Name,
|
||||||
|
NameKey = string.IsNullOrWhiteSpace(block.Name) ? "Runtime.ComponentName" : null,
|
||||||
};
|
};
|
||||||
BuildChildren(node, ChildrenOf(block));
|
BuildChildren(node, ChildrenOf(block));
|
||||||
if (node.Children.Count == 0) return null;
|
if (node.Children.Count == 0) return null;
|
||||||
@@ -280,8 +301,13 @@ public class StepImportService
|
|||||||
{
|
{
|
||||||
ImportedNode only = node.Children[0];
|
ImportedNode only = node.Children[0];
|
||||||
if (string.IsNullOrWhiteSpace(only.Name) || only.Name == node.Name)
|
if (string.IsNullOrWhiteSpace(only.Name) || only.Name == node.Name)
|
||||||
|
{
|
||||||
|
if (node.NameKey is null || only.NameKey is not null || string.IsNullOrWhiteSpace(only.Name))
|
||||||
{
|
{
|
||||||
only.Name = node.Name;
|
only.Name = node.Name;
|
||||||
|
only.NameKey = node.NameKey;
|
||||||
|
only.NameNumber = node.NameNumber;
|
||||||
|
}
|
||||||
return only;
|
return only;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +318,8 @@ public class StepImportService
|
|||||||
{
|
{
|
||||||
var leaf = new ImportedNode
|
var leaf = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = string.IsNullOrWhiteSpace(solid.NameOrEmpty) ? "Solid" : solid.NameOrEmpty,
|
Name = string.IsNullOrWhiteSpace(solid.NameOrEmpty) ? Get("Runtime.SolidName") : solid.NameOrEmpty,
|
||||||
|
NameKey = string.IsNullOrWhiteSpace(solid.NameOrEmpty) ? "Runtime.SolidName" : null,
|
||||||
SolidCount = 1,
|
SolidCount = 1,
|
||||||
};
|
};
|
||||||
leaf.SourceGeos.Add(solid);
|
leaf.SourceGeos.Add(solid);
|
||||||
@@ -316,7 +343,8 @@ public class StepImportService
|
|||||||
{
|
{
|
||||||
var leaf = new ImportedNode
|
var leaf = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = string.IsNullOrWhiteSpace(shell.NameOrEmpty) ? "Shell" : shell.NameOrEmpty,
|
Name = string.IsNullOrWhiteSpace(shell.NameOrEmpty) ? Get("Runtime.ShellName") : shell.NameOrEmpty,
|
||||||
|
NameKey = string.IsNullOrWhiteSpace(shell.NameOrEmpty) ? "Runtime.ShellName" : null,
|
||||||
};
|
};
|
||||||
leaf.SourceGeos.Add(shell);
|
leaf.SourceGeos.Add(shell);
|
||||||
_leafWork.Add(() =>
|
_leafWork.Add(() =>
|
||||||
@@ -385,7 +413,7 @@ public class StepImportService
|
|||||||
{
|
{
|
||||||
foreach (Edge e in face.AllEdges) edgeSet.Add(e);
|
foreach (Edge e in face.AllEdges) edgeSet.Add(e);
|
||||||
}
|
}
|
||||||
catch { /* 邊收集失敗不影響面渲染 */ }
|
catch { }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +469,7 @@ public class StepImportService
|
|||||||
prev = cur;
|
prev = cur;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* 個別曲線取樣失敗跳過 */ }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Rect3D BoundsOfMesh(MeshGeometry3D mesh) => BoundsOfPoints(mesh.Positions);
|
private static Rect3D BoundsOfMesh(MeshGeometry3D mesh) => BoundsOfPoints(mesh.Positions);
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using CADability;
|
||||||
|
using CADability.GeoObject;
|
||||||
|
|
||||||
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 網格精度:Current = 目前顯示網格;Fine = 由 B-rep 以較細精度重新三角化(無 B-rep 的來源維持現有網格)。
|
||||||
|
/// 不提供「較粗」:CADability 的 GetTriangulation 對「比快取粗」的精度要求會直接回傳既有較細快取,
|
||||||
|
/// 匯入時已三角化過 → 要求粗化實際上拿到同一份網格(實測驗證),故誠實不提供。
|
||||||
|
/// </summary>
|
||||||
|
public enum StlMeshQuality { Current, Fine }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// STL 匯出:WPF 三角網格 → binary / ASCII STL。
|
||||||
|
/// STL 本身無單位,約定輸出 mm(scale=1);inch 交接用 scale = 1/25.4。
|
||||||
|
/// </summary>
|
||||||
|
public static class StlExportService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 三角化精度係數(相對匯入預設):Fine 較細(三角形多、曲面平滑)。
|
||||||
|
/// 實測 CADability 三角形數對精度非單調(0.4× 重算反而比 1× 少 — 重算網格較有效率),
|
||||||
|
/// 要明顯更細需 ≤0.15×(test.stp:1× = 30,784 → 0.1× = 42,220 三角形)。
|
||||||
|
/// </summary>
|
||||||
|
public static double PrecisionFactor(StlMeshQuality q) =>
|
||||||
|
q == StlMeshQuality.Fine ? 0.15 : 1.0;
|
||||||
|
|
||||||
|
/// <summary>預估檔案大小:binary = 84 + 50×n;ASCII 每三角形約 230 bytes</summary>
|
||||||
|
public static long EstimateBytes(long triangles, bool ascii) =>
|
||||||
|
ascii ? 20 + triangles * 230 : 84 + triangles * 50;
|
||||||
|
|
||||||
|
public static string FormatBytes(long bytes) => bytes switch
|
||||||
|
{
|
||||||
|
>= 1L << 30 => $"{bytes / (double)(1L << 30):F1} GB",
|
||||||
|
>= 1L << 20 => $"{bytes / (double)(1L << 20):F1} MB",
|
||||||
|
>= 1L << 10 => $"{bytes / (double)(1L << 10):F1} KB",
|
||||||
|
_ => $"{bytes} B",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 把多個網格寫成一個 STL 檔。退化(零面積)三角形濾除。
|
||||||
|
/// 回傳實際寫出的三角形數。
|
||||||
|
/// </summary>
|
||||||
|
public static int Write(string path, IReadOnlyList<MeshGeometry3D> meshes, bool ascii, double scale) =>
|
||||||
|
ascii ? WriteAscii(path, meshes, scale) : WriteBinary(path, meshes, scale);
|
||||||
|
|
||||||
|
private static int WriteBinary(string path, IReadOnlyList<MeshGeometry3D> meshes, double scale)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 16);
|
||||||
|
using var bw = new BinaryWriter(fs);
|
||||||
|
|
||||||
|
// 80-byte header(不可以 "solid" 開頭,部分讀取器會誤判為 ASCII)
|
||||||
|
var header = new byte[80];
|
||||||
|
Encoding.ASCII.GetBytes("STPViewer binary STL (unit: mm x scale)").CopyTo(header, 0);
|
||||||
|
bw.Write(header);
|
||||||
|
bw.Write(0u); // 三角形數 placeholder,寫完回頭補
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (MeshGeometry3D mesh in meshes)
|
||||||
|
{
|
||||||
|
var pos = mesh.Positions;
|
||||||
|
var 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]];
|
||||||
|
if (!TryNormal(a, b, c, out Vector3D n)) continue;
|
||||||
|
bw.Write((float)n.X); bw.Write((float)n.Y); bw.Write((float)n.Z);
|
||||||
|
WriteVertex(bw, a, scale);
|
||||||
|
WriteVertex(bw, b, scale);
|
||||||
|
WriteVertex(bw, c, scale);
|
||||||
|
bw.Write((ushort)0); // attribute byte count
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.Seek(80, SeekOrigin.Begin);
|
||||||
|
bw.Write((uint)count);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteVertex(BinaryWriter bw, Point3D p, double scale)
|
||||||
|
{
|
||||||
|
bw.Write((float)(p.X * scale));
|
||||||
|
bw.Write((float)(p.Y * scale));
|
||||||
|
bw.Write((float)(p.Z * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteAscii(string path, IReadOnlyList<MeshGeometry3D> meshes, double scale)
|
||||||
|
{
|
||||||
|
var inv = CultureInfo.InvariantCulture;
|
||||||
|
using var sw = new StreamWriter(path, append: false, Encoding.ASCII, 1 << 16);
|
||||||
|
sw.WriteLine("solid STPViewer");
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (MeshGeometry3D mesh in meshes)
|
||||||
|
{
|
||||||
|
var pos = mesh.Positions;
|
||||||
|
var 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]];
|
||||||
|
if (!TryNormal(a, b, c, out Vector3D n)) continue;
|
||||||
|
sw.WriteLine(string.Format(inv, " facet normal {0:e6} {1:e6} {2:e6}", n.X, n.Y, n.Z));
|
||||||
|
sw.WriteLine(" outer loop");
|
||||||
|
WriteAsciiVertex(sw, a, scale, inv);
|
||||||
|
WriteAsciiVertex(sw, b, scale, inv);
|
||||||
|
WriteAsciiVertex(sw, c, scale, inv);
|
||||||
|
sw.WriteLine(" endloop");
|
||||||
|
sw.WriteLine(" endfacet");
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.WriteLine("endsolid STPViewer");
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteAsciiVertex(StreamWriter sw, Point3D p, double scale, CultureInfo inv) =>
|
||||||
|
sw.WriteLine(string.Format(inv, " vertex {0:e6} {1:e6} {2:e6}",
|
||||||
|
p.X * scale, p.Y * scale, p.Z * scale));
|
||||||
|
|
||||||
|
/// <returns>false = 退化三角形(面積 ≈ 0)</returns>
|
||||||
|
private static bool TryNormal(Point3D a, Point3D b, Point3D c, out Vector3D n)
|
||||||
|
{
|
||||||
|
n = Vector3D.CrossProduct(b - a, c - a);
|
||||||
|
double len = n.Length;
|
||||||
|
if (len < 1e-12) return false;
|
||||||
|
n /= len;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── B-rep 重新三角化(精細/較粗匯出用)────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 由 B-rep 幾何以指定精度係數重新三角化,回傳每面一個 frozen mesh。
|
||||||
|
/// CADability 非執行緒安全 → 呼叫端維持循序(可在單一背景執行緒)。
|
||||||
|
/// 個別面三角化失敗時跳過(與匯入一致的容錯策略)。
|
||||||
|
/// </summary>
|
||||||
|
public static List<MeshGeometry3D> Tessellate(IEnumerable<IGeoObject> geos, double precisionFactor)
|
||||||
|
{
|
||||||
|
var meshes = new List<MeshGeometry3D>();
|
||||||
|
foreach (IGeoObject g in geos)
|
||||||
|
{
|
||||||
|
switch (g)
|
||||||
|
{
|
||||||
|
case Solid solid:
|
||||||
|
foreach (Shell sh in solid.Shells) TessellateShell(sh, precisionFactor, meshes);
|
||||||
|
break;
|
||||||
|
case Shell shell:
|
||||||
|
TessellateShell(shell, precisionFactor, meshes);
|
||||||
|
break;
|
||||||
|
case Face face:
|
||||||
|
AddFaceMesh(face, PrecisionFor(SafeBounds(face), precisionFactor), meshes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meshes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TessellateShell(Shell shell, double factor, List<MeshGeometry3D> meshes)
|
||||||
|
{
|
||||||
|
double precision = PrecisionFor(SafeBounds(shell), factor);
|
||||||
|
foreach (Face f in shell.Faces) AddFaceMesh(f, precision, meshes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 匯入預設精度(與 StepImportService.PrecisionFor 同款 clamp)再乘 factor —
|
||||||
|
/// 先 clamp 再乘,保證 factor<1 時嚴格比匯入快取細(否則大殼在匯入被 0.5 上限截住,
|
||||||
|
/// 直接乘在 clamp 前會算出比快取粗的值 → CADability 回傳快取、精細無效)。
|
||||||
|
/// </summary>
|
||||||
|
private static double PrecisionFor(BoundingCube bc, double factor)
|
||||||
|
{
|
||||||
|
double diag;
|
||||||
|
try { diag = bc.DiagonalLength; }
|
||||||
|
catch { diag = 100; }
|
||||||
|
if (diag <= 0 || double.IsNaN(diag)) diag = 100;
|
||||||
|
double importDefault = Math.Clamp(diag * 0.0015, 0.02, 0.5);
|
||||||
|
return Math.Max(importDefault * factor, 0.005);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingCube SafeBounds(IGeoObject g)
|
||||||
|
{
|
||||||
|
try { return g.GetBoundingCube(); }
|
||||||
|
catch { return new BoundingCube(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFaceMesh(Face face, double precision, List<MeshGeometry3D> meshes)
|
||||||
|
{
|
||||||
|
GeoPoint[] pts; int[] ind;
|
||||||
|
try { face.GetTriangulation(precision, out pts, out _, out ind, out _); }
|
||||||
|
catch { return; }
|
||||||
|
if (pts is null || ind is null || ind.Length < 3) return;
|
||||||
|
|
||||||
|
var positions = new Point3DCollection(pts.Length);
|
||||||
|
foreach (GeoPoint p in pts) positions.Add(new Point3D(p.x, p.y, p.z));
|
||||||
|
var indices = new Int32Collection(ind.Length);
|
||||||
|
foreach (int i in ind) indices.Add(i);
|
||||||
|
|
||||||
|
var mesh = new MeshGeometry3D { Positions = positions, TriangleIndices = indices };
|
||||||
|
mesh.Freeze();
|
||||||
|
meshes.Add(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
|
||||||
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直接解析 STL 三角網格(binary / ASCII)→ WPF mesh,**不經過 CADability**。
|
||||||
|
/// CADability 的 ImportSTL 會為每個三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),
|
||||||
|
/// 大檔(百萬三角形)需數十分鐘且吃光記憶體;STL 本就無 B-rep,直接讀三角形即可。
|
||||||
|
/// 實測 100k 三角形:CADability 約 41,000 ms,直接解析約 11 ms。
|
||||||
|
/// </summary>
|
||||||
|
public static class StlMeshReader
|
||||||
|
{
|
||||||
|
/// <summary>一個 solid 區塊的網格(頂點未焊接:每三角形 3 個獨立頂點,與 STL 原生一致)</summary>
|
||||||
|
public record MeshGroup(string Name, Point3DCollection Positions, Int32Collection Indices,
|
||||||
|
string? NameKey = null, int NameNumber = 0);
|
||||||
|
|
||||||
|
public static List<MeshGroup> Read(string path)
|
||||||
|
{
|
||||||
|
return IsBinary(path) ? ReadBinary(path) : ReadAscii(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// binary STL 判定:檔案大小必須剛好等於 84 + 50×count(count = offset 80 的 uint32)。
|
||||||
|
/// 這是最可靠的判別法 — binary 的 80-byte header 常也以 "solid" 開頭,不能只看開頭字串。
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsBinary(string path)
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(path);
|
||||||
|
if (fi.Length < 84) return false; // 太小,當 ASCII 讓後續拋有意義的錯
|
||||||
|
Span<byte> head = stackalloc byte[84];
|
||||||
|
using (FileStream fs = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
int read = 0;
|
||||||
|
while (read < 84)
|
||||||
|
{
|
||||||
|
int n = fs.Read(head[read..]);
|
||||||
|
if (n <= 0) break;
|
||||||
|
read += n;
|
||||||
|
}
|
||||||
|
if (read < 84) return false;
|
||||||
|
}
|
||||||
|
uint count = BitConverter.ToUInt32(head[80..]);
|
||||||
|
return fi.Length == 84L + 50L * count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MeshGroup> ReadBinary(string path)
|
||||||
|
{
|
||||||
|
byte[] bytes = File.ReadAllBytes(path);
|
||||||
|
uint count = BitConverter.ToUInt32(bytes, 80);
|
||||||
|
var positions = new Point3DCollection((int)Math.Min(count * 3, int.MaxValue));
|
||||||
|
var indices = new Int32Collection((int)Math.Min(count * 3, int.MaxValue));
|
||||||
|
|
||||||
|
var span = bytes.AsSpan();
|
||||||
|
int off = 84, vi = 0;
|
||||||
|
for (uint t = 0; t < count; t++)
|
||||||
|
{
|
||||||
|
if (off + 50 > bytes.Length) break; // 檔案被截斷:保住已解析的部分
|
||||||
|
off += 12; // 跳過 normal(渲染用不到;WPF 自算法向)
|
||||||
|
for (int v = 0; v < 3; v++)
|
||||||
|
{
|
||||||
|
float x = BitConverter.ToSingle(span[off..]);
|
||||||
|
float y = BitConverter.ToSingle(span[(off + 4)..]);
|
||||||
|
float z = BitConverter.ToSingle(span[(off + 8)..]);
|
||||||
|
positions.Add(new Point3D(x, y, z));
|
||||||
|
indices.Add(vi++);
|
||||||
|
off += 12;
|
||||||
|
}
|
||||||
|
off += 2; // attribute byte count
|
||||||
|
}
|
||||||
|
return new List<MeshGroup> { new(LocalizationService.Get("Runtime.MeshName"), positions, indices, "Runtime.MeshName") };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ASCII STL:逐行掃 "vertex x y z",每 3 個湊成一個三角形;"solid <name>" 起新群組。
|
||||||
|
/// ASCII 大檔罕見(同樣三角數約為 binary 的 5 倍體積),故以正確性為主。
|
||||||
|
/// </summary>
|
||||||
|
private static List<MeshGroup> ReadAscii(string path)
|
||||||
|
{
|
||||||
|
var groups = new List<MeshGroup>();
|
||||||
|
Point3DCollection? pos = null;
|
||||||
|
Int32Collection? idx = null;
|
||||||
|
int vi = 0;
|
||||||
|
|
||||||
|
void FlushGroup()
|
||||||
|
{
|
||||||
|
if (pos is { Count: > 0 } && idx is not null)
|
||||||
|
groups.Add(new MeshGroup(groups.Count == 0 ? LocalizationService.Get("Runtime.MeshName") : LocalizationService.Format("Runtime.MeshNameNumbered", groups.Count + 1), pos, idx,
|
||||||
|
groups.Count == 0 ? "Runtime.MeshName" : "Runtime.MeshNameNumbered", groups.Count + 1));
|
||||||
|
pos = null; idx = null; vi = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] sep = { ' ', '\t' };
|
||||||
|
foreach (string raw in File.ReadLines(path))
|
||||||
|
{
|
||||||
|
string line = raw.Trim();
|
||||||
|
if (line.Length == 0) continue;
|
||||||
|
|
||||||
|
if (line.StartsWith("solid", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
FlushGroup();
|
||||||
|
pos = new Point3DCollection();
|
||||||
|
idx = new Int32Collection();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("endsolid", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
FlushGroup();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!line.StartsWith("vertex", StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
|
pos ??= new Point3DCollection();
|
||||||
|
idx ??= new Int32Collection();
|
||||||
|
string[] tok = line.Split(sep, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (tok.Length < 4) continue;
|
||||||
|
if (double.TryParse(tok[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double x) &&
|
||||||
|
double.TryParse(tok[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double y) &&
|
||||||
|
double.TryParse(tok[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double z))
|
||||||
|
{
|
||||||
|
pos.Add(new Point3D(x, y, z));
|
||||||
|
idx.Add(vi++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlushGroup();
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<Window x:Class="STPViewer.StlExportDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:STPViewer.ViewModels"
|
||||||
|
xmlns:loc="clr-namespace:STPViewer.Localization"
|
||||||
|
Title="{loc:Loc Ui.StlExport.Title}"
|
||||||
|
Width="520" SizeToContent="Height"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="NoResize" ShowInTaskbar="False"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:StlExportViewModel}"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Window.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||||
|
</Window.Resources>
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="Margin" Value="0,0,12,10" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="Margin" Value="0,0,0,10" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Resources>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{loc:Loc Ui.StlExport.Scope}" />
|
||||||
|
<ComboBox Grid.Row="0" Grid.Column="1" SelectedIndex="{Binding ScopeIndex}"
|
||||||
|
ToolTip="{loc:Loc Ui.StlExport.ScopeTooltip}">
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.ScopeMerge}" />
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.ScopePerFile}" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{loc:Loc Ui.StlExport.Format}" />
|
||||||
|
<ComboBox Grid.Row="1" Grid.Column="1" SelectedIndex="{Binding FormatIndex}">
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.FormatBinary}" />
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.FormatAscii}" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{loc:Loc Ui.StlExport.UnitScale}" />
|
||||||
|
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="{Binding UnitIndex}"
|
||||||
|
ToolTip="{loc:Loc Ui.StlExport.UnitTooltip}">
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.UnitMillimeter}" />
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.UnitInch}" />
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.UnitCustom}" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CustomScaleText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Margin="0,0,0,10" Padding="4,2"
|
||||||
|
Visibility="{Binding IsCustomScale, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="{loc:Loc Ui.StlExport.CustomScaleTooltip}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="{loc:Loc Ui.StlExport.MeshQuality}" />
|
||||||
|
<ComboBox Grid.Row="4" Grid.Column="1" SelectedIndex="{Binding QualityIndex}"
|
||||||
|
IsEnabled="{Binding HasBrep}"
|
||||||
|
ToolTip="{loc:Loc Ui.StlExport.QualityTooltip}">
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.QualityCurrent}" />
|
||||||
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.QualityFine}" />
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<Border Grid.Row="5" Grid.ColumnSpan="2" Background="#F0F4F8" CornerRadius="4"
|
||||||
|
Padding="10,8" Margin="0,2,0,12">
|
||||||
|
<TextBlock Text="{Binding Summary}" TextWrapping="Wrap" Margin="0" FontSize="12" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="6" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="{loc:Loc Ui.StlExport.Cancel}" Width="80" Margin="0,0,8,0" IsCancel="True" />
|
||||||
|
<Button Content="{loc:Loc Ui.StlExport.Export}" Width="96" IsDefault="True"
|
||||||
|
IsEnabled="{Binding CanConfirm}" Click="Confirm_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using STPViewer.ViewModels;
|
||||||
|
|
||||||
|
namespace STPViewer;
|
||||||
|
|
||||||
|
/// <summary>STL 匯出參數對話框 — 選項邏輯在 StlExportViewModel,這裡只做確認/取消</summary>
|
||||||
|
public partial class StlExportDialog : Window
|
||||||
|
{
|
||||||
|
public StlExportViewModel ViewModel { get; }
|
||||||
|
|
||||||
|
public StlExportDialog(StlExportViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ViewModel = vm;
|
||||||
|
DataContext = vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Confirm_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Windows;
|
|||||||
using System.Windows.Media.Media3D;
|
using System.Windows.Media.Media3D;
|
||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ public partial class MainViewModel
|
|||||||
// 拖曳中隱藏邊線(LinesVisual3D 隨 Transform 變更逐幀重建會卡)
|
// 拖曳中隱藏邊線(LinesVisual3D 隨 Transform 變更逐幀重建會卡)
|
||||||
_edgesSuspended = true;
|
_edgesSuspended = true;
|
||||||
SetEdgesActive(false);
|
SetEdgesActive(false);
|
||||||
StatusText = $"拖曳「{root.Name}」中…(放開定位)";
|
SetStatus(() => Format("Runtime.Dragging", root.Name));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ public partial class MainViewModel
|
|||||||
if (delta.LengthSquared > 1e-12)
|
if (delta.LengthSquared > 1e-12)
|
||||||
{
|
{
|
||||||
TranslateRoot(root, delta); // 烘進 B-rep + 網格 + 邊線 + 邊界(量測清空)
|
TranslateRoot(root, delta); // 烘進 B-rep + 網格 + 邊線 + 邊界(量測清空)
|
||||||
StatusText = $"已拖曳「{root.Name}」 Δ({delta.X:F2}, {delta.Y:F2}, {delta.Z:F2}) mm(可用 🧩干涉 驗證)";
|
SetStatus(() => Format("Runtime.Dragged", root.Name, delta.X, delta.Y, delta.Z));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
|
using STPViewer.Services;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
// ─── 匯出(量測 CSV / 視圖 PNG 截圖)────────────────────────────────
|
// ─── 匯出(量測 CSV / 視圖 PNG 截圖 / STEP / STL)────────────────────
|
||||||
public partial class MainViewModel
|
public partial class MainViewModel
|
||||||
{
|
{
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -17,26 +24,203 @@ public partial class MainViewModel
|
|||||||
{
|
{
|
||||||
if (Measurements.Count == 0)
|
if (Measurements.Count == 0)
|
||||||
{
|
{
|
||||||
StatusText = "沒有量測結果可匯出";
|
SetStatus(() => Get("Runtime.CsvEmpty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var dlg = new SaveFileDialog
|
var dlg = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Title = "匯出量測結果",
|
Title = Get("Runtime.CsvTitle"),
|
||||||
Filter = "CSV 檔案 (*.csv)|*.csv",
|
Filter = Get("Runtime.CsvFilter"),
|
||||||
FileName = $"measurements_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
|
FileName = $"measurements_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
|
||||||
};
|
};
|
||||||
if (dlg.ShowDialog() != true) return;
|
if (dlg.ShowDialog() != true) return;
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("標籤,類型,明細");
|
sb.AppendLine(Get("Runtime.CsvHeader"));
|
||||||
foreach (MeasurementResult m in Measurements)
|
foreach (MeasurementResult m in Measurements)
|
||||||
{
|
{
|
||||||
static string Esc(string s) => $"\"{s.Replace("\"", "\"\"")}\"";
|
static string Esc(string s) => $"\"{s.Replace("\"", "\"\"")}\"";
|
||||||
sb.AppendLine($"{Esc(m.Title)},{m.Kind},{Esc(m.Detail.Replace("\n", " | "))}");
|
sb.AppendLine($"{Esc(m.Title)},{Esc(Get($"Runtime.Kind{m.Kind}"))},{Esc(m.Detail.Replace("\n", " | "))}");
|
||||||
}
|
}
|
||||||
File.WriteAllText(dlg.FileName, sb.ToString(), new UTF8Encoding(true)); // BOM:Excel 中文不亂碼
|
File.WriteAllText(dlg.FileName, sb.ToString(), new UTF8Encoding(true)); // BOM:Excel 中文不亂碼
|
||||||
StatusText = $"已匯出 {Measurements.Count} 筆量測 → {Path.GetFileName(dlg.FileName)}";
|
int count = Measurements.Count;
|
||||||
|
string fileName = Path.GetFileName(dlg.FileName);
|
||||||
|
SetStatus(() => Format("Runtime.CsvExported", count, 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)
|
||||||
|
{
|
||||||
|
SetStatus(() => Get("Runtime.StepExportEmpty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dlg = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Title = Get("Runtime.StepExportTitle"),
|
||||||
|
Filter = Get("Runtime.StepExportFilter"),
|
||||||
|
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);
|
||||||
|
int count = geos.Count;
|
||||||
|
string fileName = Path.GetFileName(dlg.FileName);
|
||||||
|
SetStatus(() => Format("Runtime.StepExported", count, fileName));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
string error = ex.Message;
|
||||||
|
SetStatus(() => Format("Runtime.StepExportFailed", error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 匯出可見檔案(目前位置)為 STL 網格檔 — 先開參數對話框(範圍/格式/單位/精度 + 三角形數與大小預估)
|
||||||
|
/// 確認後才選路徑寫檔。精細/較粗 = 由 B-rep 重新三角化(背景執行緒);STL/DXF 來源一律用現有網格。
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsIdle))]
|
||||||
|
private async Task ExportStlFile()
|
||||||
|
{
|
||||||
|
// 可匯出 = 可見 leaf 且有網格(DXF 純線架構無網格 → 排除)
|
||||||
|
var groups = Roots.Where(r => r.IsVisible)
|
||||||
|
.Select(r => (Root: r, Leaves: r.Leaves()
|
||||||
|
.Where(l => l.IsVisible && GetMergedMesh(l) is not null).ToList()))
|
||||||
|
.Where(g => g.Leaves.Count > 0)
|
||||||
|
.ToList();
|
||||||
|
if (groups.Count == 0)
|
||||||
|
{
|
||||||
|
SetStatus(() => Get("Runtime.StlExportEmpty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long triangles = groups.SelectMany(g => g.Leaves)
|
||||||
|
.Sum(l => GetMergedMesh(l)!.TriangleIndices.Count / 3L);
|
||||||
|
bool hasBrep = groups.SelectMany(g => g.Leaves).Any(l => l.HasBrep && l.SourceGeos.Count > 0);
|
||||||
|
|
||||||
|
// 參數對話框(確認再匯出)
|
||||||
|
var vm = new StlExportViewModel(groups.Count, triangles, hasBrep);
|
||||||
|
var dialog = new StlExportDialog(vm) { Owner = Application.Current.MainWindow };
|
||||||
|
if (dialog.ShowDialog() != true || vm.Scale is not double scale) return;
|
||||||
|
|
||||||
|
var dlg = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Title = Get(vm.PerFile ? "Runtime.StlExportPerFileTitle" : "Runtime.StlExportMergedTitle"),
|
||||||
|
Filter = Get("Runtime.StlExportFilter"),
|
||||||
|
FileName = vm.PerFile
|
||||||
|
? $"stl_{DateTime.Now:yyyyMMdd_HHmmss}.stl"
|
||||||
|
: $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stl",
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
// UI 執行緒先快照:GeometryModel3D/Model3DGroup 是 DispatcherObject 不可跨執行緒,
|
||||||
|
// 背景只能碰 frozen MeshGeometry3D 與 CADability B-rep(SourceGeos)
|
||||||
|
var snapshot = groups
|
||||||
|
.Select(g => (g.Root.Name, Leaves: g.Leaves
|
||||||
|
.Select(l => (Mesh: GetMergedMesh(l)!, l.HasBrep, l.SourceGeos))
|
||||||
|
.ToList()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
SetStatus(() => Get(vm.Quality == StlMeshQuality.Current
|
||||||
|
? "Runtime.StlExporting" : "Runtime.StlRetriangulating"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
double factor = StlExportService.PrecisionFactor(vm.Quality);
|
||||||
|
var written = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var results = new List<(string File, int Triangles)>();
|
||||||
|
if (vm.PerFile)
|
||||||
|
{
|
||||||
|
string dir = Path.GetDirectoryName(dlg.FileName)!;
|
||||||
|
string baseName = Path.GetFileNameWithoutExtension(dlg.FileName);
|
||||||
|
foreach ((string rootName, var leaves) in snapshot)
|
||||||
|
{
|
||||||
|
string file = Path.Combine(dir, $"{baseName}_{SafeFileName(rootName)}.stl");
|
||||||
|
int n = StlExportService.Write(file, CollectMeshes(leaves, vm.Quality, factor), vm.Ascii, scale);
|
||||||
|
results.Add((file, n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var meshes = CollectMeshes(snapshot.SelectMany(g => g.Leaves).ToList(), vm.Quality, factor);
|
||||||
|
int n = StlExportService.Write(dlg.FileName, meshes, vm.Ascii, scale);
|
||||||
|
results.Add((dlg.FileName, n));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
|
long total = written.Sum(w => (long)w.Triangles);
|
||||||
|
int fileCount = written.Count;
|
||||||
|
string destination = fileCount == 1 ? Path.GetFileName(written[0].File) : Path.GetDirectoryName(dlg.FileName)!;
|
||||||
|
SetStatus(() => fileCount == 1
|
||||||
|
? Format("Runtime.StlExportedOne", total, destination)
|
||||||
|
: Format("Runtime.StlExportedMany", fileCount, total, destination));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
string error = ex.Message;
|
||||||
|
SetStatus(() => Format("Runtime.StlExportFailed", error));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>leaf 的合併網格(frozen、未剖切、目前位置)— STL 匯出的「目前顯示網格」來源</summary>
|
||||||
|
private static MeshGeometry3D? GetMergedMesh(ModelNodeViewModel leaf) =>
|
||||||
|
leaf.MergedContent?.Children.Count > 0
|
||||||
|
? (leaf.MergedContent.Children[0] as GeometryModel3D)?.Geometry as MeshGeometry3D
|
||||||
|
: null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收集要寫進 STL 的網格(背景執行緒;輸入是 UI 快照):
|
||||||
|
/// 精細且有 B-rep → 重新三角化(循序,CADability 非執行緒安全);
|
||||||
|
/// 其餘(目前網格 / STL 來源)→ 快照的合併網格(frozen,可安全跨執行緒讀取)。
|
||||||
|
/// </summary>
|
||||||
|
private static List<MeshGeometry3D> CollectMeshes(
|
||||||
|
IReadOnlyList<(MeshGeometry3D Mesh, bool HasBrep, List<CADability.GeoObject.IGeoObject> SourceGeos)> leaves,
|
||||||
|
StlMeshQuality quality, double precisionFactor)
|
||||||
|
{
|
||||||
|
var meshes = new List<MeshGeometry3D>();
|
||||||
|
foreach ((MeshGeometry3D mesh, bool hasBrep, var sourceGeos) in leaves)
|
||||||
|
{
|
||||||
|
if (quality != StlMeshQuality.Current && hasBrep && sourceGeos.Count > 0)
|
||||||
|
{
|
||||||
|
List<MeshGeometry3D> tess = StlExportService.Tessellate(sourceGeos, precisionFactor);
|
||||||
|
if (tess.Count > 0) { meshes.AddRange(tess); continue; }
|
||||||
|
// 重新三角化全數失敗 → 退回現有網格,不讓零件憑空消失
|
||||||
|
}
|
||||||
|
meshes.Add(mesh);
|
||||||
|
}
|
||||||
|
return meshes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SafeFileName(string name)
|
||||||
|
{
|
||||||
|
string stem = Path.GetFileNameWithoutExtension(name);
|
||||||
|
if (string.IsNullOrWhiteSpace(stem)) stem = name;
|
||||||
|
foreach (char c in Path.GetInvalidFileNameChars()) stem = stem.Replace(c, '_');
|
||||||
|
return stem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -45,8 +229,8 @@ public partial class MainViewModel
|
|||||||
if (_viewport is null || _viewport.ActualWidth < 1) return;
|
if (_viewport is null || _viewport.ActualWidth < 1) return;
|
||||||
var dlg = new SaveFileDialog
|
var dlg = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Title = "儲存視圖截圖",
|
Title = Get("Runtime.ScreenshotTitle"),
|
||||||
Filter = "PNG 圖片 (*.png)|*.png",
|
Filter = Get("Runtime.ScreenshotFilter"),
|
||||||
FileName = $"stpviewer_{DateTime.Now:yyyyMMdd_HHmmss}.png",
|
FileName = $"stpviewer_{DateTime.Now:yyyyMMdd_HHmmss}.png",
|
||||||
};
|
};
|
||||||
if (dlg.ShowDialog() != true) return;
|
if (dlg.ShowDialog() != true) return;
|
||||||
@@ -60,6 +244,7 @@ public partial class MainViewModel
|
|||||||
encoder.Frames.Add(BitmapFrame.Create(rtb));
|
encoder.Frames.Add(BitmapFrame.Create(rtb));
|
||||||
using FileStream fs = File.Create(dlg.FileName);
|
using FileStream fs = File.Create(dlg.FileName);
|
||||||
encoder.Save(fs);
|
encoder.Save(fs);
|
||||||
StatusText = $"已儲存截圖 → {Path.GetFileName(dlg.FileName)}";
|
string fileName = Path.GetFileName(dlg.FileName);
|
||||||
|
SetStatus(() => Format("Runtime.ScreenshotSaved", fileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Media.Media3D;
|
using System.Windows.Media.Media3D;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -23,7 +24,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 +38,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();
|
||||||
@@ -65,7 +91,7 @@ public partial class MainViewModel
|
|||||||
: Roots.Count == 1 ? Roots[0] : null;
|
: Roots.Count == 1 ? Roots[0] : null;
|
||||||
if (root is null || root.Bounds.IsEmpty)
|
if (root is null || root.Bounds.IsEmpty)
|
||||||
{
|
{
|
||||||
StatusText = "操作器:請先在樹面板點選要操作的檔案";
|
SetStatus(() => Get("Runtime.GizmoSelectFile"));
|
||||||
GizmoEnabled = false;
|
GizmoEnabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -98,7 +124,7 @@ public partial class MainViewModel
|
|||||||
AddPart(new RotateManipulator { Axis = new Vector3D(0, 1, 0), Color = Colors.Green, Diameter = diag * 0.34, InnerDiameter = diag * 0.30, Length = diag * 0.012 });
|
AddPart(new RotateManipulator { Axis = new Vector3D(0, 1, 0), Color = Colors.Green, Diameter = diag * 0.34, InnerDiameter = diag * 0.30, Length = diag * 0.012 });
|
||||||
AddPart(new RotateManipulator { Axis = new Vector3D(0, 0, 1), Color = Colors.Blue, Diameter = diag * 0.34, InnerDiameter = diag * 0.30, Length = diag * 0.012 });
|
AddPart(new RotateManipulator { Axis = new Vector3D(0, 0, 1), Color = Colors.Blue, Diameter = diag * 0.34, InnerDiameter = diag * 0.30, Length = diag * 0.012 });
|
||||||
|
|
||||||
StatusText = $"操作器:拖箭頭沿軸移動「{root.Name}」、拖環繞軸旋轉;放開即定位";
|
SetStatus(() => Format("Runtime.GizmoHint", root.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveGizmo()
|
private void RemoveGizmo()
|
||||||
@@ -186,6 +212,6 @@ public partial class MainViewModel
|
|||||||
foreach (HelixToolkit.Wpf.Manipulator man in _gizmoParts)
|
foreach (HelixToolkit.Wpf.Manipulator man in _gizmoParts)
|
||||||
man.Position = center;
|
man.Position = center;
|
||||||
}
|
}
|
||||||
StatusText = $"已套用操作器變換「{root.Name}」(可用 🧩干涉 驗證)";
|
SetStatus(() => Format("Runtime.GizmoApplied", root.Name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -20,9 +21,10 @@ 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} 個)— 用樹面板勾選";
|
int visibleCount = visibleRoots.Count;
|
||||||
|
SetStatus(() => Format("Runtime.InterferenceNeedTwo", visibleCount));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +35,36 @@ 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
|
||||||
{
|
{
|
||||||
|
int pairs = 0, intersecting = 0;
|
||||||
|
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;
|
||||||
|
int currentPair = pairs;
|
||||||
|
SetStatus(() => Format("Runtime.InterferenceChecking", currentPair, nameA, nameB));
|
||||||
|
var a = meshes[visibleRoots[i]];
|
||||||
|
var b = meshes[visibleRoots[j]];
|
||||||
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
||||||
AddInterferenceResult(result, nameA, nameB);
|
AddInterferenceResult(result, nameA, nameB);
|
||||||
|
if (result.Intersects) intersecting++;
|
||||||
|
}
|
||||||
|
if (pairs > 1)
|
||||||
|
{
|
||||||
|
int checkedPairs = pairs, hits = intersecting;
|
||||||
|
SetStatus(() => Format("Runtime.InterferenceCompleted", checkedPairs, hits));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusText = $"干涉檢查失敗:{ex.Message}";
|
string error = ex.Message;
|
||||||
|
SetStatus(() => Format("Runtime.InterferenceFailed", error));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -67,9 +85,8 @@ public partial class MainViewModel
|
|||||||
m = new MeasurementResult
|
m = new MeasurementResult
|
||||||
{
|
{
|
||||||
Kind = MeasureMode.Interference,
|
Kind = MeasureMode.Interference,
|
||||||
TitleFor = _ => $"🧩 干涉!{nameA} ⟷ {nameB}",
|
TitleFor = _ => Format("Runtime.InterferenceHitTitle", nameA, nameB),
|
||||||
DetailFor = _ => $"兩零件相交(不 match)\n相交三角形對 = {pairs:N0}\n" +
|
DetailFor = _ => Format("Runtime.InterferenceHitDetail", pairs, segCount),
|
||||||
$"紅色線 = 干涉交線({segCount:N0} 段)\n(共面貼合不會被算為干涉)",
|
|
||||||
};
|
};
|
||||||
var pts = new Point3DCollection(Math.Min(result.Segments.Count, 20000) * 2);
|
var pts = new Point3DCollection(Math.Min(result.Segments.Count, 20000) * 2);
|
||||||
foreach ((Point3D s0, Point3D s1) in result.Segments) { pts.Add(s0); pts.Add(s1); }
|
foreach ((Point3D s0, Point3D s1) in result.Segments) { pts.Add(s0); pts.Add(s1); }
|
||||||
@@ -77,15 +94,17 @@ public partial class MainViewModel
|
|||||||
if (result.Segments.Count > 0)
|
if (result.Segments.Count > 0)
|
||||||
{
|
{
|
||||||
Point3D at = result.Segments[0].A;
|
Point3D at = result.Segments[0].A;
|
||||||
m.Overlays.Add(new BillboardTextVisual3D
|
var label = new BillboardTextVisual3D
|
||||||
{
|
{
|
||||||
Position = at + new Vector3D(0, 0, markerR * 3),
|
Position = at + new Vector3D(0, 0, markerR * 3),
|
||||||
Text = "干涉",
|
Text = Get("Runtime.InterferenceLabel"),
|
||||||
Foreground = Brushes.White,
|
Foreground = Brushes.White,
|
||||||
Background = Brushes.Red,
|
Background = Brushes.Red,
|
||||||
Padding = new Thickness(5, 2, 5, 2),
|
Padding = new Thickness(5, 2, 5, 2),
|
||||||
FontSize = 14,
|
FontSize = 14,
|
||||||
});
|
};
|
||||||
|
m.Overlays.Add(label);
|
||||||
|
m.DynamicLabels.Add((label, _ => Get("Runtime.InterferenceLabel")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -96,9 +115,8 @@ public partial class MainViewModel
|
|||||||
m = new MeasurementResult
|
m = new MeasurementResult
|
||||||
{
|
{
|
||||||
Kind = MeasureMode.Interference,
|
Kind = MeasureMode.Interference,
|
||||||
TitleFor = u => $"🧩 無干涉 gap ≈ {Units.L(gap, u)}",
|
TitleFor = u => Format("Runtime.InterferenceClearTitle", Units.L(gap, u)),
|
||||||
DetailFor = u => $"{nameA} ⟷ {nameB} 無相交\n最小間隙 ≈ {Units.L(gap, u)}(網格近似)\n" +
|
DetailFor = u => Format("Runtime.InterferenceClearDetail", nameA, nameB, Units.L(gap, u), Units.P(ga, u), Units.P(gb, u)),
|
||||||
$"gap ≈ 0 即為貼合(match)\n點 1 {Units.P(ga, u)}\n點 2 {Units.P(gb, u)}",
|
|
||||||
};
|
};
|
||||||
m.Overlays.Add(new SphereVisual3D { Center = ga, Radius = markerR, Fill = Brushes.OrangeRed });
|
m.Overlays.Add(new SphereVisual3D { Center = ga, Radius = markerR, Fill = Brushes.OrangeRed });
|
||||||
m.Overlays.Add(new SphereVisual3D { Center = gb, Radius = markerR, Fill = Brushes.OrangeRed });
|
m.Overlays.Add(new SphereVisual3D { Center = gb, Radius = markerR, Fill = Brushes.OrangeRed });
|
||||||
@@ -111,14 +129,14 @@ public partial class MainViewModel
|
|||||||
var label = new BillboardTextVisual3D
|
var label = new BillboardTextVisual3D
|
||||||
{
|
{
|
||||||
Position = ga + d / 2 + new Vector3D(0, 0, markerR * 2),
|
Position = ga + d / 2 + new Vector3D(0, 0, markerR * 2),
|
||||||
Text = $"gap {Units.L(gap, UnitSystem.Millimeter)}",
|
Text = Format("Runtime.GapLabel", Units.L(gap, UnitSystem.Millimeter)),
|
||||||
Foreground = Brushes.Black,
|
Foreground = Brushes.Black,
|
||||||
Background = new SolidColorBrush(Color.FromArgb(200, 255, 255, 210)),
|
Background = new SolidColorBrush(Color.FromArgb(200, 255, 255, 210)),
|
||||||
Padding = new Thickness(4, 2, 4, 2),
|
Padding = new Thickness(4, 2, 4, 2),
|
||||||
FontSize = 14,
|
FontSize = 14,
|
||||||
};
|
};
|
||||||
m.Overlays.Add(label);
|
m.Overlays.Add(label);
|
||||||
m.DynamicLabels.Add((label, u => $"gap {Units.L(gap, u)}"));
|
m.DynamicLabels.Add((label, u => Format("Runtime.GapLabel", Units.L(gap, u))));
|
||||||
}
|
}
|
||||||
AddMeasurement(m);
|
AddMeasurement(m);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using STPViewer.Models;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
|
public partial class MainViewModel
|
||||||
|
{
|
||||||
|
public string SelectedLanguage
|
||||||
|
{
|
||||||
|
get => LocalizationService.Instance.Language;
|
||||||
|
set => LocalizationService.Instance.SetLanguage(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<LanguageDefinition> Languages => LocalizationService.Instance.Languages;
|
||||||
|
|
||||||
|
public void InitializeLocalization() =>
|
||||||
|
LocalizationService.Instance.LanguageChanged += OnLanguageChanged;
|
||||||
|
|
||||||
|
public void DisposeLocalization() =>
|
||||||
|
LocalizationService.Instance.LanguageChanged -= OnLanguageChanged;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ChangeLanguage(string language) => SelectedLanguage = language;
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(SelectedLanguage));
|
||||||
|
QuickBar.RefreshText();
|
||||||
|
RefreshLocalizedText();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,11 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Media.Media3D;
|
using System.Windows.Media.Media3D;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -38,11 +40,122 @@ public partial class MainViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool sectionFlip;
|
private bool sectionFlip;
|
||||||
|
|
||||||
partial void OnSectionEnabledChanged(bool value) => ScheduleSection();
|
/// <summary>剖面參數列的「✕ 關閉」按鈕(參數列只在剖面開啟時顯示)</summary>
|
||||||
partial void OnSectionAxisIndexChanged(int value) => ScheduleSection();
|
[RelayCommand]
|
||||||
partial void OnSectionPositionChanged(double value) => ScheduleSection();
|
private void CloseSection() => SectionEnabled = false;
|
||||||
|
|
||||||
|
// ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)──
|
||||||
|
private const int CustomAxisIndex = 3;
|
||||||
|
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)
|
||||||
|
SetStatus(() => Get("Runtime.SectionPickPrompt"));
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
int selected = _sectionPicks.Count;
|
||||||
|
SetStatus(() => Format("Runtime.SectionNoHit", selected));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_sectionPicks.Count == 2 &&
|
||||||
|
Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], hit.Value - _sectionPicks[0]).LengthSquared < 1e-12)
|
||||||
|
{
|
||||||
|
SetStatus(() => Get("Runtime.SectionCollinear"));
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
int selected = _sectionPicks.Count;
|
||||||
|
SetStatus(() => Format("Runtime.SectionPicked", selected));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3D n = Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], _sectionPicks[2] - _sectionPicks[0]);
|
||||||
|
ClearSectionPicks();
|
||||||
|
if (n.LengthSquared < 1e-12)
|
||||||
|
{
|
||||||
|
SetStatus(() => Get("Runtime.SectionDegenerate"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
n.Normalize();
|
||||||
|
_customNormal = n;
|
||||||
|
SetStatus(() => Format("Runtime.SectionDefined", n.X, n.Y, n.Z));
|
||||||
|
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) SetStatus(() => Get("Runtime.SectionCancelled"));
|
||||||
|
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();
|
||||||
@@ -67,7 +180,7 @@ public partial class MainViewModel
|
|||||||
_sectionPlaneVisual = null;
|
_sectionPlaneVisual = null;
|
||||||
}
|
}
|
||||||
ApplyRenderMode(); // 剖面關閉 → 瀏覽模式可切回合併網格
|
ApplyRenderMode(); // 剖面關閉 → 瀏覽模式可切回合併網格
|
||||||
StatusText = "剖面已關閉";
|
SetStatus(() => Get("Runtime.SectionClosed"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,28 +195,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)
|
||||||
|
{
|
||||||
|
if (_customNormal is null) return; // 平面尚未定義(等使用者點 3 點)
|
||||||
|
axis = _customNormal.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
axis = SectionAxisIndex switch
|
||||||
{
|
{
|
||||||
0 => new Vector3D(1, 0, 0),
|
0 => new Vector3D(1, 0, 0),
|
||||||
1 => new Vector3D(0, 1, 0),
|
1 => new Vector3D(0, 1, 0),
|
||||||
_ => new Vector3D(0, 0, 1),
|
_ => 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 +262,12 @@ public partial class MainViewModel
|
|||||||
|
|
||||||
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
||||||
UpdateSectionPlaneVisual(planePoint, axis, size);
|
UpdateSectionPlaneVisual(planePoint, axis, size);
|
||||||
StatusText = $"剖面:{"XYZ"[SectionAxisIndex]} 軸 {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : "");
|
int axisIndex = SectionAxisIndex;
|
||||||
|
double position = SectionPosition;
|
||||||
|
bool flip = SectionFlip;
|
||||||
|
SetStatus(() => Format("Runtime.SectionStatus",
|
||||||
|
axisIndex == CustomAxisIndex ? Get("Runtime.CustomPlane") : Format("Runtime.Axis", "XYZ"[axisIndex]),
|
||||||
|
position, flip ? Get("Runtime.Reversed") : ""));
|
||||||
} while (_sectionReapply);
|
} while (_sectionReapply);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -155,8 +285,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 +300,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using HelixToolkit.Wpf;
|
|||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
|
using static STPViewer.Services.LocalizationService;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -57,20 +58,83 @@ public partial class MainViewModel : ObservableObject
|
|||||||
private MeasureMode currentMode = MeasureMode.None;
|
private MeasureMode currentMode = MeasureMode.None;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string statusText = "就緒 — 匯入 STP/STEP/STL/DXF(拖放亦可)。右鍵旋轉、滾輪縮放、中鍵或 Shift+左鍵平移";
|
private string statusText = Get("Runtime.Ready");
|
||||||
|
|
||||||
|
private Func<string> _statusTextFor = () => Get("Runtime.Ready");
|
||||||
|
|
||||||
|
private void SetStatus(Func<string> textFor)
|
||||||
|
{
|
||||||
|
_statusTextFor = textFor;
|
||||||
|
StatusText = textFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshLocalizedText()
|
||||||
|
{
|
||||||
|
foreach (MeasurementResult result in Measurements) result.RefreshText();
|
||||||
|
foreach (ModelNodeViewModel root in Roots) root.RefreshLocalizedText();
|
||||||
|
if (!string.IsNullOrWhiteSpace(TreeFilter))
|
||||||
|
foreach (ModelNodeViewModel root in Roots) root.ApplyFilter(TreeFilter);
|
||||||
|
StatusText = _statusTextFor();
|
||||||
|
}
|
||||||
|
|
||||||
[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))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ExportStlFileCommand))]
|
||||||
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();
|
||||||
|
|
||||||
|
/// <summary>快速工具列自訂(選單列有全部功能,快速列只放勾選的常用按鈕)</summary>
|
||||||
|
public QuickBarViewModel QuickBar { 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);
|
||||||
|
QuickBar.Load(s.QuickBarKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveSettingsInto(AppSettings s)
|
||||||
|
{
|
||||||
|
s.UseInch = UseInch;
|
||||||
|
s.Language = SelectedLanguage;
|
||||||
|
s.RecentFiles = RecentFiles.ToList();
|
||||||
|
s.QuickBarKeys = QuickBar.ToKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) };
|
||||||
@@ -131,21 +195,24 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var dlg = new OpenFileDialog
|
var dlg = new OpenFileDialog
|
||||||
{
|
{
|
||||||
Title = "匯入 CAD 檔案(可複選)",
|
Title = Get("Runtime.ImportTitle"),
|
||||||
Filter = "CAD 檔案 (*.stp;*.step;*.stl;*.dxf)|*.stp;*.step;*.stl;*.dxf|" +
|
Filter = Get("Runtime.ImportFilter"),
|
||||||
"STEP (*.stp;*.step)|*.stp;*.step|STL (*.stl)|*.stl|DXF (*.dxf)|*.dxf|所有檔案 (*.*)|*.*",
|
|
||||||
Multiselect = true,
|
Multiselect = true,
|
||||||
};
|
};
|
||||||
if (dlg.ShowDialog() == true)
|
if (dlg.ShowDialog() == true)
|
||||||
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;
|
||||||
// 拖放/命令列可繞過指令 CanExecute 直接進來 → 匯入或背景運算期間直接擋掉,
|
// 拖放/命令列可繞過指令 CanExecute 直接進來 → 匯入或背景運算期間直接擋掉,
|
||||||
// 避免兩批匯入交錯(StepImportService 有共享狀態,非重入安全)
|
// 避免兩批匯入交錯(StepImportService 有共享狀態,非重入安全)
|
||||||
if (IsBusy) { StatusText = "忙碌中(匯入或運算進行中),請稍候再匯入"; return; }
|
if (IsBusy) { SetStatus(() => Get("Runtime.ImportBusy")); return; }
|
||||||
int ok = 0, fail = 0;
|
int ok = 0, fail = 0;
|
||||||
IsBusy = true; // 整批匯入期間維持 busy(v0.4.0:不再逐檔開關,杜絕檔案之間的重入空窗)
|
IsBusy = true; // 整批匯入期間維持 busy(v0.4.0:不再逐檔開關,杜絕檔案之間的重入空窗)
|
||||||
try
|
try
|
||||||
@@ -153,22 +220,24 @@ public partial class MainViewModel : ObservableObject
|
|||||||
foreach (string path in paths)
|
foreach (string path in paths)
|
||||||
{
|
{
|
||||||
string name = Path.GetFileNameWithoutExtension(path);
|
string name = Path.GetFileNameWithoutExtension(path);
|
||||||
StatusText = $"匯入中:{name} …";
|
SetStatus(() => Format("Runtime.Importing", name));
|
||||||
// 匯入階段回報(從背景執行緒來 → 切回 UI 執行緒)
|
// 匯入階段回報(從背景執行緒來 → 切回 UI 執行緒)
|
||||||
_importService.Progress = msg =>
|
_importService.ProgressTextFor = textFor =>
|
||||||
System.Windows.Application.Current?.Dispatcher.BeginInvoke(
|
System.Windows.Application.Current?.Dispatcher.BeginInvoke(
|
||||||
() => StatusText = $"匯入 {name}:{msg}");
|
() => SetStatus(() => Format("Runtime.ImportProgress", name, textFor())));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
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)
|
||||||
{
|
{
|
||||||
fail++;
|
fail++;
|
||||||
StatusText = $"匯入失敗:{name} — {ex.Message}";
|
string error = ex.Message;
|
||||||
MessageBox.Show($"{path}\n\n{ex.Message}", "匯入失敗",
|
SetStatus(() => Format("Runtime.ImportFailed", name, error));
|
||||||
|
MessageBox.Show($"{path}\n\n{error}", Get("Runtime.ImportFailedTitle"),
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +250,8 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (SectionEnabled) ApplySection();
|
if (SectionEnabled) ApplySection();
|
||||||
_viewport.ZoomExtents(500);
|
_viewport.ZoomExtents(500);
|
||||||
StatusText = $"匯入完成:成功 {ok} 檔" + (fail > 0 ? $"、失敗 {fail} 檔" : "");
|
int succeeded = ok, failed = fail;
|
||||||
|
SetStatus(() => Format("Runtime.ImportCompleted", succeeded, failed > 0 ? Format("Runtime.ImportFailedCount", failed) : ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +270,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);
|
||||||
@@ -224,6 +295,8 @@ public partial class MainViewModel : ObservableObject
|
|||||||
var vm = new ModelNodeViewModel(n.Faces.Count > 0 || n.EdgeSegments is not null ? _paletteIndex++ : _paletteIndex)
|
var vm = new ModelNodeViewModel(n.Faces.Count > 0 || n.EdgeSegments is not null ? _paletteIndex++ : _paletteIndex)
|
||||||
{
|
{
|
||||||
Name = overrideName ?? n.Name,
|
Name = overrideName ?? n.Name,
|
||||||
|
NameKey = overrideName is null ? n.NameKey : null,
|
||||||
|
NameNumber = n.NameNumber,
|
||||||
FilePath = isRoot ? filePath : null,
|
FilePath = isRoot ? filePath : null,
|
||||||
SolidCount = n.SolidCount,
|
SolidCount = n.SolidCount,
|
||||||
FaceCount = n.FaceCount,
|
FaceCount = n.FaceCount,
|
||||||
@@ -410,6 +483,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);
|
||||||
|
SetStatus(() => Format("Runtime.View", Get($"Runtime.View{name}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>正交投影切換(平行投影,量尺寸不受透視變形;Helix 保持視角換相機型別)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool orthographicView;
|
||||||
|
|
||||||
|
partial void OnOrthographicViewChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_viewport is null) return;
|
||||||
|
_viewport.Orthographic = value;
|
||||||
|
SetStatus(() => Get(value ? "Runtime.Orthographic" : "Runtime.Perspective"));
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void RemoveRoot(ModelNodeViewModel root)
|
private void RemoveRoot(ModelNodeViewModel root)
|
||||||
{
|
{
|
||||||
@@ -437,7 +548,92 @@ public partial class MainViewModel : ObservableObject
|
|||||||
if (root.EdgeVisual is not null)
|
if (root.EdgeVisual is not null)
|
||||||
_viewport.Children.Remove(root.EdgeVisual);
|
_viewport.Children.Remove(root.EdgeVisual);
|
||||||
Roots.Remove(root);
|
Roots.Remove(root);
|
||||||
StatusText = $"已移除:{root.Name}";
|
string removedName = root.Name;
|
||||||
|
SetStatus(() => Format("Runtime.Removed", removedName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 顯示控制 / 體積(樹右鍵選單)────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>只顯示此節點(其餘全部隱藏;「全部顯示」復原)</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void IsolateNode(ModelNodeViewModel node)
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, false);
|
||||||
|
SetVisibleRecursive(node, true);
|
||||||
|
SetStatus(() => Format("Runtime.Isolated", node.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void InvertVisibility()
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel l in Roots.SelectMany(r => r.Leaves()))
|
||||||
|
l.IsVisible = !l.IsVisible;
|
||||||
|
SetStatus(() => Get("Runtime.VisibilityInverted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowAll()
|
||||||
|
{
|
||||||
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, true);
|
||||||
|
SetStatus(() => Get("Runtime.AllVisible"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
SetStatus(() => Format("Runtime.VolumeNoMesh", node.Name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(double vol, Point3D centroid, bool reliable) = MeasurementService.MeshVolume(meshes);
|
||||||
|
if (!reliable)
|
||||||
|
{
|
||||||
|
SetStatus(() => Format("Runtime.VolumeOpenMesh", 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 => Format("Runtime.VolumeDetail", Units.V(vol, u), Units.P(centroid, u), 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 量測 ────────────────────────────────────────────────────
|
// ─── 量測 ────────────────────────────────────────────────────
|
||||||
@@ -446,34 +642,77 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
CancelPending();
|
CancelPending();
|
||||||
ApplyRenderMode(); // 非剖面一律合併網格(量測拾取靠反查面);不再因切量測模式而掛逐面
|
ApplyRenderMode(); // 非剖面一律合併網格(量測拾取靠反查面);不再因切量測模式而掛逐面
|
||||||
StatusText = value switch
|
string? key = value switch
|
||||||
{
|
{
|
||||||
MeasureMode.None => "瀏覽模式(右鍵旋轉、滾輪縮放、中鍵或 Shift+左鍵拖曳平移)",
|
MeasureMode.None => "Runtime.ModeBrowse",
|
||||||
MeasureMode.Point => "點量測:點擊模型表面(自動吸附鄰近頂點)",
|
MeasureMode.Point => "Runtime.ModePoint",
|
||||||
MeasureMode.Distance => "距離量測:點選第 1 點",
|
MeasureMode.Distance => "Runtime.ModeDistance",
|
||||||
MeasureMode.Edge => "邊量測:點擊靠近要量的邊",
|
MeasureMode.Edge => "Runtime.ModeEdge",
|
||||||
MeasureMode.Face => "面量測:點擊要量的面",
|
MeasureMode.Face => "Runtime.ModeFace",
|
||||||
MeasureMode.Circle => "圓量測:點擊圓孔內壁或圓邊附近",
|
MeasureMode.Circle => "Runtime.ModeCircle",
|
||||||
MeasureMode.Angle => "角度量測:點選第 1 個面(或靠近直線邊)",
|
MeasureMode.Angle => "Runtime.ModeAngle",
|
||||||
MeasureMode.FaceDistance => "面距量測:點選第 1 個面",
|
MeasureMode.FaceDistance => "Runtime.ModeFaceDistance",
|
||||||
MeasureMode.Align => "兩點對齊:先點「要移動零件」上的點,再點目標點(純平移,量測會清空)",
|
MeasureMode.Align => "Runtime.ModeAlign",
|
||||||
MeasureMode.Align3 => "三點對齊:先在「要移動的檔案」點 3 個特徵點,再到目標檔案點 3 個對應點(旋轉+平移)",
|
MeasureMode.Align3 => "Runtime.ModeAlign3",
|
||||||
MeasureMode.Drag => "拖曳模式:左鍵按住零件拖動(沿螢幕平面),放開定位;深度方向先轉視角再拖。右鍵仍可轉視角",
|
MeasureMode.Drag => "Runtime.ModeDrag",
|
||||||
_ => StatusText,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
if (key is not null) SetStatus(() => Get(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnUseInchChanged(bool value)
|
partial void OnUseInchChanged(bool value)
|
||||||
{
|
{
|
||||||
UnitSystem u = CurrentUnit;
|
UnitSystem u = CurrentUnit;
|
||||||
foreach (MeasurementResult m in Measurements) m.SetUnit(u);
|
foreach (MeasurementResult m in Measurements) m.SetUnit(u);
|
||||||
StatusText = value ? "顯示單位:inch" : "顯示單位:mm";
|
SetStatus(() => Get(value ? "Runtime.UnitInch" : "Runtime.UnitMillimeter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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();
|
||||||
|
SetStatus(() => Get("Runtime.MeasurementStartCancelled"));
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -501,7 +740,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
if (fi is null)
|
if (fi is null)
|
||||||
{
|
{
|
||||||
StatusText = "未命中模型表面";
|
SetStatus(() => Get("Runtime.NoSurfaceHit"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +750,8 @@ public partial class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusText = $"量測失敗:{ex.Message}";
|
string error = ex.Message;
|
||||||
|
SetStatus(() => Format("Runtime.MeasureFailed", error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +781,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
// 無 B-rep 來源(STL)只支援部分模式
|
// 無 B-rep 來源(STL)只支援部分模式
|
||||||
if (fi.BrepFace is null && CurrentMode is MeasureMode.Edge or MeasureMode.Circle or MeasureMode.Face)
|
if (fi.BrepFace is null && CurrentMode is MeasureMode.Edge or MeasureMode.Circle or MeasureMode.Face)
|
||||||
{
|
{
|
||||||
StatusText = "此模型無 B-rep(STL/網格),僅支援 點 / 距離 / 角度 / 面距 量測";
|
SetStatus(() => Get("Runtime.MeshMeasurementLimited"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +800,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_pendingPoint = p;
|
_pendingPoint = p;
|
||||||
AddPendingMarker(p, markerR);
|
AddPendingMarker(p, markerR);
|
||||||
StatusText = $"第 1 點 ({p.X:F2}, {p.Y:F2}, {p.Z:F2}) — 點選第 2 點";
|
SetStatus(() => Format("Runtime.DistanceFirstPoint", p.X, p.Y, p.Z));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -574,7 +814,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
case MeasureMode.Edge:
|
case MeasureMode.Edge:
|
||||||
{
|
{
|
||||||
var result = _measureService.MeasureEdge(fi, hit, NextLabel(MeasureMode.Edge, "E"), markerR, circlesOnly: false);
|
var result = _measureService.MeasureEdge(fi, hit, NextLabel(MeasureMode.Edge, "E"), markerR, circlesOnly: false);
|
||||||
if (result is null) { StatusText = "此面沒有可量測的邊"; UndoLabel(MeasureMode.Edge); }
|
if (result is null) { SetStatus(() => Get("Runtime.NoMeasurableEdge")); UndoLabel(MeasureMode.Edge); }
|
||||||
else AddMeasurement(result);
|
else AddMeasurement(result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -586,7 +826,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
case MeasureMode.Circle:
|
case MeasureMode.Circle:
|
||||||
{
|
{
|
||||||
var result = _measureService.MeasureEdge(fi, hit, NextLabel(MeasureMode.Circle, "C"), markerR, circlesOnly: true);
|
var result = _measureService.MeasureEdge(fi, hit, NextLabel(MeasureMode.Circle, "C"), markerR, circlesOnly: true);
|
||||||
if (result is null) { StatusText = "此面沒有圓形邊(請點圓孔內壁或圓邊附近)"; UndoLabel(MeasureMode.Circle); }
|
if (result is null) { SetStatus(() => Get("Runtime.NoCircularEdge")); UndoLabel(MeasureMode.Circle); }
|
||||||
else AddMeasurement(result);
|
else AddMeasurement(result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -597,7 +837,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_pendingDirection = pick;
|
_pendingDirection = pick;
|
||||||
AddPendingMarker(pick.At, markerR);
|
AddPendingMarker(pick.At, markerR);
|
||||||
StatusText = $"已選第 1 個方向({pick.Desc})— 點選第 2 個面或邊";
|
SetStatus(() => Format("Runtime.AngleFirstDirection", Get(pick.Desc)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -614,11 +854,11 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_pendingFace = fi;
|
_pendingFace = fi;
|
||||||
AddPendingMarker(hit, markerR);
|
AddPendingMarker(hit, markerR);
|
||||||
StatusText = "已選第 1 個面 — 點選另一個面";
|
SetStatus(() => Get("Runtime.FaceDistanceFirstFace"));
|
||||||
}
|
}
|
||||||
else if (ReferenceEquals(_pendingFace, fi))
|
else if (ReferenceEquals(_pendingFace, fi))
|
||||||
{
|
{
|
||||||
StatusText = "兩次點到同一個面,請點選另一個面";
|
SetStatus(() => Get("Runtime.SameFace"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -633,16 +873,16 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
Point3D p = _measureService.Snap(fi, hit, snapTol);
|
Point3D p = _measureService.Snap(fi, hit, snapTol);
|
||||||
ModelNodeViewModel? root = FindRootOf(fi.Owner);
|
ModelNodeViewModel? root = FindRootOf(fi.Owner);
|
||||||
if (root is null) { StatusText = "找不到此零件所屬檔案"; break; }
|
if (root is null) { SetStatus(() => Get("Runtime.RootNotFound")); break; }
|
||||||
if (_pendingAlign is null)
|
if (_pendingAlign is null)
|
||||||
{
|
{
|
||||||
_pendingAlign = (root, p);
|
_pendingAlign = (root, p);
|
||||||
AddPendingMarker(p, markerR);
|
AddPendingMarker(p, markerR);
|
||||||
StatusText = $"將移動「{root.Name}」— 點選目標點(另一個檔案上)";
|
SetStatus(() => Format("Runtime.AlignMoving", root.Name));
|
||||||
}
|
}
|
||||||
else if (ReferenceEquals(_pendingAlign.Value.Root, root))
|
else if (ReferenceEquals(_pendingAlign.Value.Root, root))
|
||||||
{
|
{
|
||||||
StatusText = "目標點要點在另一個檔案上(兩點同屬一個檔案無法對齊)";
|
SetStatus(() => Get("Runtime.AlignOtherFile"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -650,8 +890,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
Vector3D offset = p - from;
|
Vector3D offset = p - from;
|
||||||
CancelPending();
|
CancelPending();
|
||||||
TranslateRoot(moveRoot, offset);
|
TranslateRoot(moveRoot, offset);
|
||||||
StatusText = $"已移動「{moveRoot.Name}」 Δ({offset.X:F3}, {offset.Y:F3}, {offset.Z:F3}) mm" +
|
SetStatus(() => Format("Runtime.AlignMoved", moveRoot.Name, offset.X, offset.Y, offset.Z));
|
||||||
"(量測已清空;可用 ⇔面距 / 🧩干涉 驗證配合)";
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -659,43 +898,46 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
Point3D p = _measureService.Snap(fi, hit, snapTol);
|
Point3D p = _measureService.Snap(fi, hit, snapTol);
|
||||||
ModelNodeViewModel? root = FindRootOf(fi.Owner);
|
ModelNodeViewModel? root = FindRootOf(fi.Owner);
|
||||||
if (root is null) { StatusText = "找不到此零件所屬檔案"; break; }
|
if (root is null) { SetStatus(() => Get("Runtime.RootNotFound")); break; }
|
||||||
|
|
||||||
if (_align3.Count < 3) // 來源 3 點(要移動的檔案)
|
if (_align3.Count < 3) // 來源 3 點(要移動的檔案)
|
||||||
{
|
{
|
||||||
if (_align3.Count > 0 && !ReferenceEquals(_align3[0].Root, root))
|
if (_align3.Count > 0 && !ReferenceEquals(_align3[0].Root, root))
|
||||||
{
|
{
|
||||||
StatusText = $"來源 3 點要同一個檔案(要移動:{_align3[0].Root.Name})";
|
ModelNodeViewModel sourceRoot = _align3[0].Root;
|
||||||
|
SetStatus(() => Format("Runtime.Align3SameSource", sourceRoot.Name));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (_align3.Count == 2 && RigidAlign.Collinear(_align3[0].P, _align3[1].P, p))
|
if (_align3.Count == 2 && RigidAlign.Collinear(_align3[0].P, _align3[1].P, p))
|
||||||
{
|
{
|
||||||
StatusText = "三點共線,請改選不共線的第 3 點";
|
SetStatus(() => Get("Runtime.Align3SourceCollinear"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_align3.Add((root, p));
|
_align3.Add((root, p));
|
||||||
AddPendingMarker(p, markerR);
|
AddPendingMarker(p, markerR);
|
||||||
StatusText = _align3.Count < 3
|
int sourceCount = _align3.Count;
|
||||||
? $"來源點 {_align3.Count}/3 — 繼續點「{root.Name}」上的點"
|
SetStatus(() => sourceCount < 3
|
||||||
: "來源 3 點完成 — 換點「目標檔案」上的對應點 1/3";
|
? Format("Runtime.Align3SourcePoint", sourceCount, root.Name)
|
||||||
|
: Get("Runtime.Align3SourceComplete"));
|
||||||
}
|
}
|
||||||
else // 目標 3 點(另一個檔案,順序對應來源點)
|
else // 目標 3 點(另一個檔案,順序對應來源點)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_align3[0].Root, root))
|
if (ReferenceEquals(_align3[0].Root, root))
|
||||||
{
|
{
|
||||||
StatusText = "目標點要點在另一個檔案上";
|
SetStatus(() => Get("Runtime.Align3OtherFile"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (_align3.Count == 5 && RigidAlign.Collinear(_align3[3].P, _align3[4].P, p))
|
if (_align3.Count == 5 && RigidAlign.Collinear(_align3[3].P, _align3[4].P, p))
|
||||||
{
|
{
|
||||||
StatusText = "目標三點共線,請改選不共線的第 3 點";
|
SetStatus(() => Get("Runtime.Align3TargetCollinear"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_align3.Add((root, p));
|
_align3.Add((root, p));
|
||||||
AddPendingMarker(p, markerR);
|
AddPendingMarker(p, markerR);
|
||||||
if (_align3.Count < 6)
|
if (_align3.Count < 6)
|
||||||
{
|
{
|
||||||
StatusText = $"目標點 {_align3.Count - 3}/3 — 繼續點目標檔案上的對應點";
|
int targetCount = _align3.Count - 3;
|
||||||
|
SetStatus(() => Format("Runtime.Align3TargetPoint", targetCount));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,11 +947,11 @@ public partial class MainViewModel : ObservableObject
|
|||||||
CancelPending();
|
CancelPending();
|
||||||
if (!RigidAlign.TryRigidTransform(p1, p2, p3, q1, q2, q3, out Matrix3D m))
|
if (!RigidAlign.TryRigidTransform(p1, p2, p3, q1, q2, q3, out Matrix3D m))
|
||||||
{
|
{
|
||||||
StatusText = "三點對齊失敗(點位退化),請重新選點";
|
SetStatus(() => Get("Runtime.Align3Failed"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
TransformRoot(moveRoot, RigidAlign.ToModOp(m), m);
|
TransformRoot(moveRoot, RigidAlign.ToModOp(m), m);
|
||||||
StatusText = $"已三點對齊「{moveRoot.Name}」(旋轉+平移;點1精確貼合,量測已清空,可用 🧩干涉 驗證)";
|
SetStatus(() => Format("Runtime.Align3Completed", moveRoot.Name));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -744,7 +986,8 @@ public partial class MainViewModel : ObservableObject
|
|||||||
Measurements.Add(result);
|
Measurements.Add(result);
|
||||||
foreach (Visual3D v in result.Overlays)
|
foreach (Visual3D v in result.Overlays)
|
||||||
_viewport!.Children.Add(v);
|
_viewport!.Children.Add(v);
|
||||||
StatusText = $"✓ {result.Title}";
|
Func<UnitSystem, string> titleFor = result.TitleFor;
|
||||||
|
SetStatus(() => $"✓ {titleFor(CurrentUnit)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -764,7 +1007,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
Measurements.Clear();
|
Measurements.Clear();
|
||||||
_counters.Clear();
|
_counters.Clear();
|
||||||
CancelPending();
|
CancelPending();
|
||||||
StatusText = "已清除全部量測";
|
SetStatus(() => Get("Runtime.MeasurementsCleared"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelPending()
|
private void CancelPending()
|
||||||
@@ -807,7 +1050,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
foreach (CADability.GeoObject.IGeoObject g in leaf.SourceGeos)
|
foreach (CADability.GeoObject.IGeoObject g in leaf.SourceGeos)
|
||||||
{
|
{
|
||||||
try { g.Modify(op); }
|
try { g.Modify(op); }
|
||||||
catch { /* 個別物件變換失敗不中斷 */ }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染網格:面級平行變換(來源/輸出皆 frozen、各面獨立 → 安全;大檔 64k 面吃滿多核)
|
// 渲染網格:面級平行變換(來源/輸出皆 frozen、各面獨立 → 安全;大檔 64k 面吃滿多核)
|
||||||
@@ -893,7 +1136,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
: Roots.Count == 1 ? Roots[0] : null;
|
: Roots.Count == 1 ? Roots[0] : null;
|
||||||
if (root is null)
|
if (root is null)
|
||||||
{
|
{
|
||||||
StatusText = "請先在樹面板點選要旋轉的檔案(任一節點即可)";
|
SetStatus(() => Get("Runtime.RotateSelectFile"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Rect3D b = root.Bounds;
|
Rect3D b = root.Bounds;
|
||||||
@@ -909,7 +1152,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
var m = Matrix3D.Identity;
|
var m = Matrix3D.Identity;
|
||||||
m.RotateAt(new Quaternion(axis, 90), center);
|
m.RotateAt(new Quaternion(axis, 90), center);
|
||||||
TransformRoot(root, RigidAlign.ToModOp(m), m);
|
TransformRoot(root, RigidAlign.ToModOp(m), m);
|
||||||
StatusText = $"已旋轉「{root.Name}」繞 {axisName} 軸 +90°(再按可繼續轉)";
|
SetStatus(() => Format("Runtime.Rotated", root.Name, axisName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ModelNodeViewModel? RootContaining(ModelNodeViewModel node) =>
|
private ModelNodeViewModel? RootContaining(ModelNodeViewModel node) =>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Windows.Media.Media3D;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -44,7 +45,15 @@ public partial class ModelNodeViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private Color color;
|
private Color color;
|
||||||
|
|
||||||
public string Name { get; init; } = "";
|
public string Name
|
||||||
|
{
|
||||||
|
get => NameKey is null ? _name : NameNumber > 0 && NameKey == "Runtime.MeshNameNumbered"
|
||||||
|
? LocalizationService.Format(NameKey, NameNumber) : LocalizationService.Get(NameKey);
|
||||||
|
init => _name = value;
|
||||||
|
}
|
||||||
|
private string _name = "";
|
||||||
|
public string? NameKey { get; init; }
|
||||||
|
public int NameNumber { get; init; }
|
||||||
|
|
||||||
/// <summary>root(檔案層級)才有值</summary>
|
/// <summary>root(檔案層級)才有值</summary>
|
||||||
public string? FilePath { get; init; }
|
public string? FilePath { get; init; }
|
||||||
@@ -57,8 +66,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,14 +92,45 @@ 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");
|
LocalizationService.Format("Runtime.NodeStats", SolidCount, FaceCount, TriangleCount) +
|
||||||
|
(HasBrep ? "" : LocalizationService.Get("Runtime.NoBrep")) +
|
||||||
|
(Bounds.IsEmpty ? "" : LocalizationService.Format("Runtime.OutlineSize", SizeText));
|
||||||
|
|
||||||
public string ToolTipText => (FilePath is not null ? FilePath + "\n" : "") + Stats;
|
public string ToolTipText => (FilePath is not null ? FilePath + "\n" : "") + Stats;
|
||||||
|
|
||||||
|
public void RefreshLocalizedText()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Name));
|
||||||
|
OnPropertyChanged(nameof(Stats));
|
||||||
|
OnPropertyChanged(nameof(ToolTipText));
|
||||||
|
foreach (ModelNodeViewModel child in Children) child.RefreshLocalizedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 樹搜尋/過濾(只影響樹面板顯示,不影響 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(剖面用;量測打合併網格)
|
||||||
public Model3DGroup? MergedContent { get; set; } // leaf:整零件合併成 1 個 GeometryModel3D(瀏覽用,draw call 大減)
|
public Model3DGroup? MergedContent { get; set; } // leaf:整零件合併成 1 個 GeometryModel3D(瀏覽+量測,draw call 大減)
|
||||||
public LinesVisual3D? EdgeVisual { get; set; } // root:整檔合併邊線(一檔一條,避免轉動卡頓)
|
public LinesVisual3D? EdgeVisual { get; set; } // root:整檔合併邊線(一檔一條,避免轉動卡頓)
|
||||||
public DiffuseMaterial? SharedMaterial { get; set; }
|
public DiffuseMaterial? SharedMaterial { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>快速列單一按鈕的顯示開關(選單「快速列 → 自訂按鈕」的 checkbox 項)</summary>
|
||||||
|
public partial class QuickBarItemViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Label => LocalizationService.Get(_labelKey);
|
||||||
|
private readonly string _labelKey;
|
||||||
|
public bool Default { get; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isChecked;
|
||||||
|
|
||||||
|
public QuickBarItemViewModel(string key, string label, bool @default)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
_labelKey = label;
|
||||||
|
Default = @default;
|
||||||
|
isChecked = @default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshText() => OnPropertyChanged(nameof(Label));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速工具列自訂:所有功能都在選單列,快速列只放使用者勾選的常用按鈕。
|
||||||
|
/// XAML 以索引子綁定:Visibility="{Binding QuickBar[Point].IsChecked, Converter=…}"。
|
||||||
|
/// 勾選狀態隨 settings.json(QuickBarKeys)保存;null = 用預設。
|
||||||
|
/// </summary>
|
||||||
|
public partial class QuickBarViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
/// <summary>註冊表 = 快速列順序 + 選單顯示名 + 是否預設顯示(一般 user 常用)</summary>
|
||||||
|
private static readonly (string Key, string Label, bool Default)[] Registry =
|
||||||
|
{
|
||||||
|
("Import", "QuickBar.Import", true),
|
||||||
|
("Recent", "QuickBar.Recent", true),
|
||||||
|
("ZoomAll", "QuickBar.ZoomAll", true),
|
||||||
|
("ViewIso", "QuickBar.ViewIso", true),
|
||||||
|
("ViewFront", "QuickBar.ViewFront", false),
|
||||||
|
("ViewTop", "QuickBar.ViewTop", false),
|
||||||
|
("ViewRight", "QuickBar.ViewRight", false),
|
||||||
|
("Ortho", "QuickBar.Ortho", true),
|
||||||
|
("Point", "QuickBar.Point", true),
|
||||||
|
("Distance", "QuickBar.Distance", true),
|
||||||
|
("Edge", "QuickBar.Edge", false),
|
||||||
|
("Face", "QuickBar.Face", false),
|
||||||
|
("Circle", "QuickBar.Circle", true),
|
||||||
|
("Angle", "QuickBar.Angle", true),
|
||||||
|
("FaceDistance", "QuickBar.FaceDistance", false),
|
||||||
|
("Align", "QuickBar.Align", false),
|
||||||
|
("Align3", "QuickBar.Align3", false),
|
||||||
|
("RotateX", "QuickBar.RotateX", false),
|
||||||
|
("RotateY", "QuickBar.RotateY", false),
|
||||||
|
("RotateZ", "QuickBar.RotateZ", false),
|
||||||
|
("Drag", "QuickBar.Drag", false),
|
||||||
|
("Gizmo", "QuickBar.Gizmo", false),
|
||||||
|
("Interference", "QuickBar.Interference", true),
|
||||||
|
("Section", "QuickBar.Section", true),
|
||||||
|
("Clear", "QuickBar.Clear", true),
|
||||||
|
("Unit", "QuickBar.Unit", true),
|
||||||
|
("ExportCsv", "QuickBar.ExportCsv", false),
|
||||||
|
("Screenshot", "QuickBar.Screenshot", false),
|
||||||
|
("ExportStep", "QuickBar.ExportStep", false),
|
||||||
|
("ExportStl", "QuickBar.ExportStl", false),
|
||||||
|
("Language", "QuickBar.Language", true),
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly Dictionary<string, QuickBarItemViewModel> _byKey;
|
||||||
|
|
||||||
|
/// <summary>依快速列順序的全部項目(選單自訂清單用)</summary>
|
||||||
|
public IReadOnlyList<QuickBarItemViewModel> Items { get; }
|
||||||
|
|
||||||
|
public QuickBarViewModel()
|
||||||
|
{
|
||||||
|
Items = Registry.Select(r => new QuickBarItemViewModel(r.Key, r.Label, r.Default)).ToList();
|
||||||
|
_byKey = Items.ToDictionary(i => i.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>XAML 索引子綁定入口:QuickBar[Point].IsChecked</summary>
|
||||||
|
public QuickBarItemViewModel this[string key] => _byKey[key];
|
||||||
|
|
||||||
|
/// <summary>套用保存的勾選清單;null = 維持預設(含未知 key 容錯:新版本新增按鈕用預設值)</summary>
|
||||||
|
public void Load(List<string>? visibleKeys)
|
||||||
|
{
|
||||||
|
if (visibleKeys is null) return;
|
||||||
|
var set = new HashSet<string>(visibleKeys, StringComparer.Ordinal);
|
||||||
|
foreach (QuickBarItemViewModel item in Items)
|
||||||
|
item.IsChecked = set.Contains(item.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>目前勾選清單(存 settings.json)</summary>
|
||||||
|
public List<string> ToKeys() =>
|
||||||
|
Items.Where(i => i.IsChecked).Select(i => i.Key).ToList();
|
||||||
|
|
||||||
|
public void RefreshText()
|
||||||
|
{
|
||||||
|
foreach (QuickBarItemViewModel item in Items)
|
||||||
|
item.RefreshText();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ResetDefaults()
|
||||||
|
{
|
||||||
|
foreach (QuickBarItemViewModel item in Items)
|
||||||
|
item.IsChecked = item.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// STL 匯出參數對話框的 ViewModel:範圍 / 格式 / 單位縮放 / 網格精度 + 即時摘要(三角形數、預估大小)。
|
||||||
|
/// 對話框按「匯出」後由 MainViewModel.Export 讀取選項執行。
|
||||||
|
/// </summary>
|
||||||
|
public partial class StlExportViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
/// <summary>可見(可匯出)檔案數</summary>
|
||||||
|
public int FileCount { get; }
|
||||||
|
|
||||||
|
/// <summary>目前顯示網格的三角形總數(精細/較粗會重新三角化,實際數量以結果為準)</summary>
|
||||||
|
public long TriangleCount { get; }
|
||||||
|
|
||||||
|
/// <summary>是否有 B-rep 來源(全 STL/DXF 時精度選項無意義 → 停用)</summary>
|
||||||
|
public bool HasBrep { get; }
|
||||||
|
|
||||||
|
public StlExportViewModel(int fileCount, long triangleCount, bool hasBrep)
|
||||||
|
{
|
||||||
|
FileCount = fileCount;
|
||||||
|
TriangleCount = triangleCount;
|
||||||
|
HasBrep = hasBrep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>0 = 全部可見合併成單一 STL;1 = 每個可見檔案各一個 STL</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(Summary))]
|
||||||
|
private int scopeIndex;
|
||||||
|
|
||||||
|
/// <summary>0 = Binary(建議,檔案小);1 = ASCII(文字可讀,檔案大)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(Summary))]
|
||||||
|
private int formatIndex;
|
||||||
|
|
||||||
|
/// <summary>0 = mm(1:1);1 = inch(÷25.4);2 = 自訂縮放</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(Summary))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsCustomScale))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanConfirm))]
|
||||||
|
private int unitIndex;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(Summary))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(CanConfirm))]
|
||||||
|
private string customScaleText = "1.0";
|
||||||
|
|
||||||
|
/// <summary>0 = 目前顯示網格;1 = 精細(B-rep 重新三角化)</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(Summary))]
|
||||||
|
private int qualityIndex;
|
||||||
|
|
||||||
|
public bool IsCustomScale => UnitIndex == 2;
|
||||||
|
|
||||||
|
public bool Ascii => FormatIndex == 1;
|
||||||
|
public bool PerFile => ScopeIndex == 1;
|
||||||
|
public StlMeshQuality Quality => (StlMeshQuality)QualityIndex;
|
||||||
|
|
||||||
|
/// <summary>座標縮放;自訂值無效時回 null(擋確認)</summary>
|
||||||
|
public double? Scale => UnitIndex switch
|
||||||
|
{
|
||||||
|
0 => 1.0,
|
||||||
|
1 => 1.0 / 25.4,
|
||||||
|
_ => double.TryParse(CustomScaleText, NumberStyles.Float, CultureInfo.InvariantCulture, out double s)
|
||||||
|
&& s > 0 && double.IsFinite(s) ? s : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool CanConfirm => Scale is not null;
|
||||||
|
|
||||||
|
public string Summary
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
long est = StlExportService.EstimateBytes(TriangleCount, Ascii);
|
||||||
|
string size = StlExportService.FormatBytes(est);
|
||||||
|
string basis = QualityIndex == 0
|
||||||
|
? LocalizationService.Format("Ui.StlExport.EstimatedSize", size)
|
||||||
|
: LocalizationService.Format("Ui.StlExport.EstimatedSizeFine", size);
|
||||||
|
return LocalizationService.Format("Ui.StlExport.Summary", FileCount, TriangleCount.ToString("N0"), basis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
{
|
||||||
|
"key": "English",
|
||||||
|
"data": {
|
||||||
|
"App.ErrorTitle": "STPViewer Error",
|
||||||
|
"App.NoRecentFiles": "(No recent files)",
|
||||||
|
"App.UnexpectedError": "An unexpected error interrupted the operation. The application will try to continue.\n\n{0}\n\nDetails: {1}",
|
||||||
|
"App.WindowTitle": "STPViewer V{0} — STP/STEP 3D Viewer",
|
||||||
|
"QuickBar.Align": "🎯 Two-point alignment",
|
||||||
|
"QuickBar.Align3": "🎯 Three-point alignment",
|
||||||
|
"QuickBar.Angle": "∠ Angle",
|
||||||
|
"QuickBar.Circle": "⭕ Circle",
|
||||||
|
"QuickBar.Clear": "🧹 Clear measurements",
|
||||||
|
"QuickBar.Distance": "📏 Distance",
|
||||||
|
"QuickBar.Drag": "🖐 Drag",
|
||||||
|
"QuickBar.Edge": "📐 Edge",
|
||||||
|
"QuickBar.ExportCsv": "💾 Export CSV",
|
||||||
|
"QuickBar.ExportStep": "📤 Export STEP",
|
||||||
|
"QuickBar.ExportStl": "📐 Export STL",
|
||||||
|
"QuickBar.Face": "⬛ Face",
|
||||||
|
"QuickBar.FaceDistance": "⇔ Face distance",
|
||||||
|
"QuickBar.Gizmo": "⊹ Gizmo",
|
||||||
|
"QuickBar.Import": "📂 Import",
|
||||||
|
"QuickBar.Interference": "🧩 Interference",
|
||||||
|
"QuickBar.Language": "Language",
|
||||||
|
"QuickBar.Ortho": "Orthographic projection",
|
||||||
|
"QuickBar.Point": "📍 Point",
|
||||||
|
"QuickBar.Recent": "▾ Recent files",
|
||||||
|
"QuickBar.RotateX": "Rotate ↻X",
|
||||||
|
"QuickBar.RotateY": "Rotate ↻Y",
|
||||||
|
"QuickBar.RotateZ": "Rotate ↻Z",
|
||||||
|
"QuickBar.Screenshot": "📷 Screenshot",
|
||||||
|
"QuickBar.Section": "✂ Section",
|
||||||
|
"QuickBar.Unit": "mm ⇄ in",
|
||||||
|
"QuickBar.ViewFront": "View: Front",
|
||||||
|
"QuickBar.ViewIso": "View: Isometric",
|
||||||
|
"QuickBar.ViewRight": "View: Right",
|
||||||
|
"QuickBar.ViewTop": "View: Top",
|
||||||
|
"QuickBar.ZoomAll": "🔍 Zoom all",
|
||||||
|
"Runtime.Align3Completed": "Aligned “{0}” using three points (rotation + translation; point 1 matched exactly, measurements cleared)",
|
||||||
|
"Runtime.Align3Failed": "Three-point alignment failed (degenerate points); select new points",
|
||||||
|
"Runtime.Align3OtherFile": "Target points must be on another file",
|
||||||
|
"Runtime.Align3SameSource": "All three source points must be on one file (moving: {0})",
|
||||||
|
"Runtime.Align3SourceCollinear": "The three points are collinear; choose a different third point",
|
||||||
|
"Runtime.Align3SourceComplete": "Three source points selected — select matching point 1/3 on the target file",
|
||||||
|
"Runtime.Align3SourcePoint": "Source point {0}/3 — keep selecting points on “{1}”",
|
||||||
|
"Runtime.Align3TargetCollinear": "Target points are collinear; choose a different third point",
|
||||||
|
"Runtime.Align3TargetPoint": "Target point {0}/3 — select the next matching point",
|
||||||
|
"Runtime.AlignMoved": "Moved “{0}” Δ({1:F3}, {2:F3}, {3:F3}) mm (measurements cleared; use face distance or interference to check fit)",
|
||||||
|
"Runtime.AlignMoving": "Moving “{0}” — select a target point on another file",
|
||||||
|
"Runtime.AlignOtherFile": "The target point must be on another file",
|
||||||
|
"Runtime.AllVisible": "All shown",
|
||||||
|
"Runtime.AngleDetail": "Angle = {0:F2}° (supplement {1:F2}°)\nDirection 1: {2} ({3:F3}, {4:F3}, {5:F3})\nDirection 2: {6} ({7:F3}, {8:F3}, {9:F3})\n(Face angles use normals; see supplement for the complementary angle)",
|
||||||
|
"Runtime.AngleFirstDirection": "First direction selected ({0}) — select another face or edge",
|
||||||
|
"Runtime.ArcLength": "Arc length",
|
||||||
|
"Runtime.Axis": "{0} axis",
|
||||||
|
"Runtime.CircleDetail": "Diameter = {0}\nRadius = {1}\n{2} = {3}\nCenter {4}",
|
||||||
|
"Runtime.Circumference": "Circumference",
|
||||||
|
"Runtime.ComponentName": "Component",
|
||||||
|
"Runtime.ConicalSurface": "Conical surface",
|
||||||
|
"Runtime.CsvEmpty": "No measurements to export",
|
||||||
|
"Runtime.CsvExported": "Exported {0} measurements → {1}",
|
||||||
|
"Runtime.CsvFilter": "CSV files (*.csv)|*.csv",
|
||||||
|
"Runtime.CsvHeader": "Label,Type,Details",
|
||||||
|
"Runtime.CsvTitle": "Export measurements",
|
||||||
|
"Runtime.CurveDetail": "Curve length = {0}\nType = {1}",
|
||||||
|
"Runtime.CurveName": "Curve",
|
||||||
|
"Runtime.CustomPlane": "custom plane",
|
||||||
|
"Runtime.CylinderSurface": "Type = cylindrical surface\nRadius = {0}\nAxis ({1:F3}, {2:F3}, {3:F3})",
|
||||||
|
"Runtime.DirectionEdge": "Edge direction",
|
||||||
|
"Runtime.DirectionFace": "Face normal",
|
||||||
|
"Runtime.DirectionMeshFace": "Face normal (mesh)",
|
||||||
|
"Runtime.DistanceDetail": "Distance = {0}\nΔX = {1}\nΔY = {2}\nΔZ = {3}\nP1 {4}\nP2 {5}",
|
||||||
|
"Runtime.DistanceFirstPoint": "First point ({0:F2}, {1:F2}, {2:F2}) — select the second point",
|
||||||
|
"Runtime.Dragged": "Dragged “{0}” Δ({1:F2}, {2:F2}, {3:F2}) mm (use interference check to verify)",
|
||||||
|
"Runtime.Dragging": "Dragging “{0}”… (release to place)",
|
||||||
|
"Runtime.DxfParseFailed": "Could not parse DXF file",
|
||||||
|
"Runtime.ExtrusionSurface": "Extruded surface",
|
||||||
|
"Runtime.FaceDetail": "Area ≈ {0} (mesh approximation)\n{1}\nTriangles = {2:N0}",
|
||||||
|
"Runtime.FaceDistanceDetail": "Minimum distance ≈ {0} (mesh approximation)\nΔX = {1}\nΔY = {2}\nΔZ = {3}\nPoint 1 {4}\nPoint 2 {5}",
|
||||||
|
"Runtime.FaceDistanceFirstFace": "First face selected — select another face",
|
||||||
|
"Runtime.FaceName": "Face",
|
||||||
|
"Runtime.FileNotFound": "File not found",
|
||||||
|
"Runtime.GapLabel": "gap {0}",
|
||||||
|
"Runtime.GeometryProcessed": "Geometry processed in {0:N0} ms (triangulation {1:N0} ms, edge sampling {2:N0} ms{3})",
|
||||||
|
"Runtime.GizmoApplied": "Applied gizmo transform to “{0}” (use interference check to verify)",
|
||||||
|
"Runtime.GizmoHint": "Gizmo: drag an arrow to move “{0}” along an axis or a ring to rotate; release to place",
|
||||||
|
"Runtime.GizmoSelectFile": "Gizmo: select a file in the tree first",
|
||||||
|
"Runtime.ImportBusy": "Busy importing or calculating; please wait",
|
||||||
|
"Runtime.ImportCompleted": "Import complete: {0} succeeded{1}",
|
||||||
|
"Runtime.ImportFailed": "Import failed: {0} — {1}",
|
||||||
|
"Runtime.ImportFailedCount": ", {0} failed",
|
||||||
|
"Runtime.ImportFailedTitle": "Import failed",
|
||||||
|
"Runtime.ImportFilter": "CAD files (*.stp;*.step;*.stl;*.dxf)|*.stp;*.step;*.stl;*.dxf|STEP (*.stp;*.step)|*.stp;*.step|STL (*.stl)|*.stl|DXF (*.dxf)|*.dxf|All files (*.*)|*.*",
|
||||||
|
"Runtime.ImportProgress": "Importing {0}: {1}",
|
||||||
|
"Runtime.ImportTitle": "Import CAD files (multiple selection)",
|
||||||
|
"Runtime.Importing": "Importing: {0}…",
|
||||||
|
"Runtime.InterferenceChecking": "Checking interference (pair {0}): {1} ⟷ {2}…",
|
||||||
|
"Runtime.InterferenceClearDetail": "{0} ⟷ {1} do not intersect\nMinimum gap ≈ {2} (mesh approximation)\nGap ≈ 0 means contact (match)\nPoint 1 {3}\nPoint 2 {4}",
|
||||||
|
"Runtime.InterferenceClearTitle": "🧩 No interference gap ≈ {0}",
|
||||||
|
"Runtime.InterferenceCompleted": "Interference check complete: {0} pairs, {1} intersecting",
|
||||||
|
"Runtime.InterferenceFailed": "Interference check failed: {0}",
|
||||||
|
"Runtime.InterferenceHitDetail": "Parts intersect (no match)\nIntersecting triangle pairs = {0:N0}\nRed lines = intersection curves ({1:N0} segments)\n(Coplanar contact is not counted as interference)",
|
||||||
|
"Runtime.InterferenceHitTitle": "🧩 Interference! {0} ⟷ {1}",
|
||||||
|
"Runtime.InterferenceLabel": "Interference",
|
||||||
|
"Runtime.InterferenceNeedTwo": "Interference check needs at least two visible files ({0} now); select them in the tree",
|
||||||
|
"Runtime.Isolated": "Showing only “{0}” (tree context menu → Show all to restore)",
|
||||||
|
"Runtime.KindAlign": "Align",
|
||||||
|
"Runtime.KindAlign3": "Three-point align",
|
||||||
|
"Runtime.KindAngle": "Angle",
|
||||||
|
"Runtime.KindCircle": "Circle",
|
||||||
|
"Runtime.KindDistance": "Distance",
|
||||||
|
"Runtime.KindDrag": "Drag",
|
||||||
|
"Runtime.KindEdge": "Edge",
|
||||||
|
"Runtime.KindFace": "Face",
|
||||||
|
"Runtime.KindFaceDistance": "Face distance",
|
||||||
|
"Runtime.KindInterference": "Interference",
|
||||||
|
"Runtime.KindNone": "None",
|
||||||
|
"Runtime.KindPoint": "Point",
|
||||||
|
"Runtime.KindVolume": "Volume",
|
||||||
|
"Runtime.LineDetail": "Length = {0}\nΔX = {1} ΔY = {2} ΔZ = {3}\nStart {4}\nEnd {5}",
|
||||||
|
"Runtime.MeasureFailed": "Measurement failed: {0}",
|
||||||
|
"Runtime.MeasurementStartCancelled": "Current measurement start cancelled (press Esc again to leave measurement mode)",
|
||||||
|
"Runtime.MeasurementsCleared": "All measurements cleared",
|
||||||
|
"Runtime.MeshMeasurementLimited": "This model has no B-rep (STL/mesh); only point, distance, angle and face distance measurements are available",
|
||||||
|
"Runtime.MeshName": "Mesh",
|
||||||
|
"Runtime.MeshNameNumbered": "Mesh {0}",
|
||||||
|
"Runtime.MeshSurface": "Type = mesh (no B-rep)",
|
||||||
|
"Runtime.ModeAlign": "Two-point align: select a point on the part to move, then a target point (translation only; measurements cleared)",
|
||||||
|
"Runtime.ModeAlign3": "Three-point align: select three feature points on the file to move, then three matching points on the target file",
|
||||||
|
"Runtime.ModeAngle": "Angle measurement: select the first face (or near a straight edge)",
|
||||||
|
"Runtime.ModeBrowse": "Browse mode (right drag to orbit, wheel to zoom, middle drag or Shift+left drag to pan)",
|
||||||
|
"Runtime.ModeCircle": "Circle measurement: click near a circular edge or hole wall",
|
||||||
|
"Runtime.ModeDistance": "Distance measurement: select the first point",
|
||||||
|
"Runtime.ModeDrag": "Drag mode: hold left mouse button to drag a part in the screen plane, release to place it; rotate the view to change depth",
|
||||||
|
"Runtime.ModeEdge": "Edge measurement: click near an edge",
|
||||||
|
"Runtime.ModeFace": "Face measurement: click a face",
|
||||||
|
"Runtime.ModeFaceDistance": "Face distance measurement: select the first face",
|
||||||
|
"Runtime.ModePoint": "Point measurement: click a model surface (snaps to nearby vertices)",
|
||||||
|
"Runtime.NoBrep": " · No B-rep",
|
||||||
|
"Runtime.NoCircularEdge": "This face has no circular edge (click near a hole wall or circular edge)",
|
||||||
|
"Runtime.NoGeometry": "File contains no displayable geometry",
|
||||||
|
"Runtime.NoMeasurableEdge": "This face has no measurable edge",
|
||||||
|
"Runtime.NoSurfaceHit": "No model surface hit",
|
||||||
|
"Runtime.NodeStats": "{0} solids · {1} faces · {2:N0} △",
|
||||||
|
"Runtime.NurbsSurface": "NURBS surface",
|
||||||
|
"Runtime.Orthographic": "Orthographic projection (parallel view for checking dimensions)",
|
||||||
|
"Runtime.OutlineSize": "\nBounds {0}",
|
||||||
|
"Runtime.ParsingStep": "Parsing STEP (large files may take several minutes)…",
|
||||||
|
"Runtime.ParsingStl": "Parsing STL triangles…",
|
||||||
|
"Runtime.Perspective": "Perspective projection",
|
||||||
|
"Runtime.PlaneSurface": "Type = plane\nNormal ({0:F3}, {1:F3}, {2:F3})",
|
||||||
|
"Runtime.Ready": "Ready — import STP/STEP/STL/DXF (or drag and drop). Right drag to orbit, wheel to zoom, middle drag or Shift+left drag to pan",
|
||||||
|
"Runtime.Removed": "Removed: {0}",
|
||||||
|
"Runtime.RetriedFaces": ", recovered {0} faces",
|
||||||
|
"Runtime.Reversed": " (reversed)",
|
||||||
|
"Runtime.RevolutionSurface": "Surface of revolution",
|
||||||
|
"Runtime.RootNotFound": "Could not find this part's file",
|
||||||
|
"Runtime.RotateSelectFile": "Select a file to rotate in the tree first",
|
||||||
|
"Runtime.Rotated": "Rotated “{0}” +90° around {1} axis (click again to continue)",
|
||||||
|
"Runtime.SameFace": "The same face was selected twice; select another face",
|
||||||
|
"Runtime.ScreenshotFilter": "PNG images (*.png)|*.png",
|
||||||
|
"Runtime.ScreenshotSaved": "Screenshot saved → {0}",
|
||||||
|
"Runtime.ScreenshotTitle": "Save view screenshot",
|
||||||
|
"Runtime.SectionCancelled": "3-point section definition cancelled",
|
||||||
|
"Runtime.SectionClosed": "Section closed",
|
||||||
|
"Runtime.SectionCollinear": "3-point section: points are collinear; choose a different third point",
|
||||||
|
"Runtime.SectionDefined": "3-point section: plane defined (normal {0:F2}, {1:F2}, {2:F2}) — move the position slider to scan",
|
||||||
|
"Runtime.SectionDegenerate": "3-point section: points are degenerate; select again",
|
||||||
|
"Runtime.SectionNoHit": "3-point section: no model surface hit ({0}/3 points selected, Esc to cancel)",
|
||||||
|
"Runtime.SectionPickPrompt": "3-point section: click three points on the model to define the cutting plane (Esc to cancel)",
|
||||||
|
"Runtime.SectionPicked": "3-point section: {0}/3 points selected",
|
||||||
|
"Runtime.SectionStatus": "Section: {0} {1:F0}%{2}",
|
||||||
|
"Runtime.ShellName": "Shell",
|
||||||
|
"Runtime.SolidName": "Solid",
|
||||||
|
"Runtime.SphericalSurface": "Spherical surface",
|
||||||
|
"Runtime.StepExportEmpty": "No B-rep geometry to export (STEP supports Solid/Shell; select a STEP file to export)",
|
||||||
|
"Runtime.StepExportFailed": "STEP export failed: {0}",
|
||||||
|
"Runtime.StepExportFilter": "STEP files (*.stp)|*.stp|STEP files (*.step)|*.step",
|
||||||
|
"Runtime.StepExportTitle": "Export current scene as STEP (parts at current aligned positions)",
|
||||||
|
"Runtime.StepExported": "Exported STEP ({0} solids at current positions) → {1}",
|
||||||
|
"Runtime.StepParseFailed": "CADability could not parse this STEP file",
|
||||||
|
"Runtime.StepParsed": "STEP parsed in {0:N0} ms",
|
||||||
|
"Runtime.StlExportEmpty": "No meshes to export (select at least one file with a solid mesh)",
|
||||||
|
"Runtime.StlExportFailed": "STL export failed: {0}",
|
||||||
|
"Runtime.StlExportFilter": "STL files (*.stl)|*.stl",
|
||||||
|
"Runtime.StlExportMergedTitle": "Export STL (merged into one file)",
|
||||||
|
"Runtime.StlExportPerFileTitle": "Export STL (one per file; use this as the base filename)",
|
||||||
|
"Runtime.StlExportedMany": "Exported {0} STL files ({1:N0} triangles total) → {2}",
|
||||||
|
"Runtime.StlExportedOne": "Exported STL ({0:N0} triangles) → {1}",
|
||||||
|
"Runtime.StlExporting": "Exporting STL…",
|
||||||
|
"Runtime.StlNoTriangles": "STL file contains no triangles",
|
||||||
|
"Runtime.StlParsed": "STL parsed in {0:N0} ms ({1} meshes)",
|
||||||
|
"Runtime.StlRetriangulating": "Retriangulating and exporting STL… (large assemblies take time)",
|
||||||
|
"Runtime.SurfaceType": "Type = {0}",
|
||||||
|
"Runtime.ToroidalSurface": "Toroidal surface",
|
||||||
|
"Runtime.Triangulating": "Triangulating {0} parts in parallel…",
|
||||||
|
"Runtime.UnitInch": "Display unit: inch",
|
||||||
|
"Runtime.UnitMillimeter": "Display unit: mm",
|
||||||
|
"Runtime.UnsupportedFormat": "Unsupported format (.stp / .step / .stl / .dxf supported; CADability has no IGES reader)",
|
||||||
|
"Runtime.View": "View: {0}",
|
||||||
|
"Runtime.ViewBack": "Back",
|
||||||
|
"Runtime.ViewBottom": "Bottom",
|
||||||
|
"Runtime.ViewFront": "Front",
|
||||||
|
"Runtime.ViewIso": "Isometric",
|
||||||
|
"Runtime.ViewLeft": "Left",
|
||||||
|
"Runtime.ViewRight": "Right",
|
||||||
|
"Runtime.ViewTop": "Top",
|
||||||
|
"Runtime.VisibilityInverted": "Visibility inverted",
|
||||||
|
"Runtime.VolumeDetail": "Volume ≈ {0} (mesh approximation)\nCentroid {1}\nBounds (AABB) {2} × {3} × {4} {5}",
|
||||||
|
"Runtime.VolumeNoMesh": "“{0}” has no face mesh for volume calculation (wireframe has no volume)",
|
||||||
|
"Runtime.VolumeOpenMesh": "“{0}” is an open mesh; volume cannot be calculated reliably",
|
||||||
|
"Ui.About.Changelog": "📋 Release History",
|
||||||
|
"Ui.About.Close": "Close",
|
||||||
|
"Ui.About.Description": "STP / STEP 3D Viewer. Open, measure, align, check interference, section, and export without starting a full CAD application.",
|
||||||
|
"Ui.About.Developer": "👤 Developer",
|
||||||
|
"Ui.About.Email": "Email",
|
||||||
|
"Ui.About.Name": "Name",
|
||||||
|
"Ui.About.Release010.Change1": "• STEP, STL, and DXF import with an assembly tree; point, distance, edge, face, circle, angle, and face-distance measurement; alignment, interference checking, sections, units, CSV, and screenshots.",
|
||||||
|
"Ui.About.Release010.Title": "First Release",
|
||||||
|
"Ui.About.Release020.Change1": "• Hold the left mouse button on a part to drag it into position.",
|
||||||
|
"Ui.About.Release020.Change2": "• Version 0.2.1 fixed a crash after clicking a dragged part.",
|
||||||
|
"Ui.About.Release020.Title": "Drag Mode",
|
||||||
|
"Ui.About.Release030.Change1": "• Select a model and use XYZ arrows to move it along an axis or rings to rotate it by any angle.",
|
||||||
|
"Ui.About.Release030.Title": "3-Axis Gizmo",
|
||||||
|
"Ui.About.Release031.Change1": "• The 3-axis gizmo stays above the model and remains accessible from any angle.",
|
||||||
|
"Ui.About.Release031.Title": "Gizmo Always Visible",
|
||||||
|
"Ui.About.Release032.Change1": "• Rotating a large assembly remains smooth while measuring.",
|
||||||
|
"Ui.About.Release032.Title": "Smooth Measurement of Large Assemblies",
|
||||||
|
"Ui.About.Release040.Change1": "• Moving the section slider on a large file no longer freezes the window.",
|
||||||
|
"Ui.About.Release040.Change2": "• Placement after dragging or using the gizmo is much faster.",
|
||||||
|
"Ui.About.Release040.Change3": "• Errors now show a message and let the application keep running.",
|
||||||
|
"Ui.About.Release040.Title": "Faster and More Reliable",
|
||||||
|
"Ui.About.Release050.Change1": "• Distance measurement snaps to the center of a circular edge, making hole pitch easy to measure.",
|
||||||
|
"Ui.About.Release050.Change2": "• Measurement shortcuts: P point, D distance, E edge, F face, C circle, A angle, M face distance; Esc cancels.",
|
||||||
|
"Ui.About.Release050.Change3": "• Search part names in the model tree; right-click to isolate a node or measure volume and centroid.",
|
||||||
|
"Ui.About.Release050.Change4": "• Standard views and orthographic projection make dimensions easier to inspect without perspective distortion.",
|
||||||
|
"Ui.About.Release050.Change5": "• A three-point section mode cuts along any plane defined by three points on the model.",
|
||||||
|
"Ui.About.Release050.Change6": "• Export aligned parts to a new STEP file for use in CAD.",
|
||||||
|
"Ui.About.Release050.Change7": "• Window size, unit preferences, and the ten most recently opened files are remembered.",
|
||||||
|
"Ui.About.Release050.Title": "Major Improvements to Measurement, Views, and the Model Tree",
|
||||||
|
"Ui.About.Release060.Change1": "• Export visible parts, including their aligned and rotated positions, to STL for 3D printing or other software.",
|
||||||
|
"Ui.About.Release060.Change2": "• Choose one merged file or one file per model, Binary or ASCII, mm or inches, and mesh quality before exporting.",
|
||||||
|
"Ui.About.Release060.Change3": "• The settings dialog shows triangle count and estimated file size immediately.",
|
||||||
|
"Ui.About.Release060.Title": "Export STL",
|
||||||
|
"Ui.About.Release070.Change1": "• A new menu bar groups all features under File, View, Measure, Transform, and Tools.",
|
||||||
|
"Ui.About.Release070.Change2": "• Choose your favorite Quick Bar buttons from Quick Bar → Customize Buttons; your selection is saved.",
|
||||||
|
"Ui.About.Release070.Change3": "• Section controls appear only when section view is active.",
|
||||||
|
"Ui.About.Release070.Change4": "• A new About window shows the release history.",
|
||||||
|
"Ui.About.Release070.Title": "Redesigned Interface with Menus and a Customizable Quick Bar",
|
||||||
|
"Ui.About.Release071.Change1": "• Large STL mesh files with millions of triangles now open almost instantly instead of taking several minutes.",
|
||||||
|
"Ui.About.Release071.Change2": "• For example, a 75 MB STL with 1.5 million triangles now appears in under a second.",
|
||||||
|
"Ui.About.Release071.Title": "Open Large STL Files in Seconds",
|
||||||
|
"Ui.About.Release080.Change1": "• Choose English, 繁體中文 or 简体中文 from View → Language or the quick toolbar. The whole interface switches instantly — including measurements already on screen — and your choice is remembered.",
|
||||||
|
"Ui.About.Release080.Change2": "• Languages are plain files in the i18n folder next to the program: drop in a new one and it shows up in the menu, no update needed. Anything not yet translated falls back to English.",
|
||||||
|
"Ui.About.Release080.Change3": "• Thanks to @CarmJos for contributing this feature.",
|
||||||
|
"Ui.About.Release080.Title": "Switch Languages: English, Traditional & Simplified Chinese",
|
||||||
|
"Ui.About.Technology": ".NET 8 · WPF · C#; CADability (STEP / B-rep geometry); HelixToolkit (3D display and camera)",
|
||||||
|
"Ui.About.TechnologyTitle": "🔧 Technology",
|
||||||
|
"Ui.About.Title": "About STPViewer",
|
||||||
|
"Ui.Language": "Language",
|
||||||
|
"Ui.Main.3AxisGizmo": "⊹ 3-Axis Gizmo",
|
||||||
|
"Ui.Main.3AxisGizmoUseXYZArrows": "3-axis gizmo: use XYZ arrows to move and rings to rotate the selected model; release to place it",
|
||||||
|
"Ui.Main.3Point": "🎯 3-Point",
|
||||||
|
"Ui.Main.3Points": "3 Points",
|
||||||
|
"Ui.Main.Align": "🎯 Align",
|
||||||
|
"Ui.Main.Angle": "∠ Angle",
|
||||||
|
"Ui.Main.AngleBetweenFacesStraightEdgesShortcut": "Angle between faces or straight edges — shortcut A",
|
||||||
|
"Ui.Main.AngleBetweenTwoFacesStraightEdges": "Angle between two faces or straight edges",
|
||||||
|
"Ui.Main.ApplicationInformationReleaseHistory": "Application information and release history",
|
||||||
|
"Ui.Main.AreaSurfaceTypeNormalVector": "Area, surface type, and normal vector",
|
||||||
|
"Ui.Main.AreaSurfaceTypeNormalVectorShortcut": "Area, surface type, and normal vector — shortcut F",
|
||||||
|
"Ui.Main.Capture": "📷 Capture",
|
||||||
|
"Ui.Main.CenterRadiusDiameterCircumference": "Center, radius, diameter, and circumference",
|
||||||
|
"Ui.Main.CenterRadiusDiameterCircumferenceShortcutC": "Center, radius, diameter, and circumference — shortcut C",
|
||||||
|
"Ui.Main.CheckButtonsShowQuickBarSaved": "Check the buttons to show on the Quick Bar (saved in settings)",
|
||||||
|
"Ui.Main.CheckVisibleModelPairsIntersectionsReport": "Check visible model pairs for intersections; report the smallest gap when none intersect",
|
||||||
|
"Ui.Main.ChooseScopeFormatUnitsMeshQuality": "Choose scope, format, units, and mesh quality before exporting",
|
||||||
|
"Ui.Main.Circle": "⭕ Circle",
|
||||||
|
"Ui.Main.Clear": "🧹 Clear",
|
||||||
|
"Ui.Main.ClearAllMeasurements": "🧹 Clear All Measurements",
|
||||||
|
"Ui.Main.ClearAllMeasurementsMarkersEscCancels": "Clear all measurements and markers (Esc cancels the current measurement or mode)",
|
||||||
|
"Ui.Main.ClickChangeColorIncludingChildNodes": "Click to change color (including child nodes)",
|
||||||
|
"Ui.Main.Close": "✕ Close",
|
||||||
|
"Ui.Main.CloseSectionView": "Close section view",
|
||||||
|
"Ui.Main.CustomizeButtons": "Customize Buttons",
|
||||||
|
"Ui.Main.DeleteThisMeasurement": "Delete this measurement",
|
||||||
|
"Ui.Main.DisplayInchesMm": "Display in Inches (mm ⇄ in)",
|
||||||
|
"Ui.Main.Distance": "📏 Distance",
|
||||||
|
"Ui.Main.DistanceXYZCircleCenterSnappingHole": "Distance and ΔXYZ with circle-center snapping for hole pitch — shortcut D",
|
||||||
|
"Ui.Main.DistanceXYZCircularEdgesSnapCenters": "Distance and ΔXYZ; circular edges snap to centers for hole pitch measurement",
|
||||||
|
"Ui.Main.Drag": "🖐 Drag",
|
||||||
|
"Ui.Main.DragMode": "🖐 Drag Mode",
|
||||||
|
"Ui.Main.DragModeHoldLeftMouseButton": "Drag mode: hold the left mouse button to move a part in the screen plane; rotate the view to move in depth",
|
||||||
|
"Ui.Main.DragResizePanel": "Drag to resize the panel",
|
||||||
|
"Ui.Main.Edge": "📐 Edge",
|
||||||
|
"Ui.Main.EdgeLength": "📐 Edge Length",
|
||||||
|
"Ui.Main.Edges": "Edges",
|
||||||
|
"Ui.Main.EscCancelsCurrentMeasurementExitsMode": "Esc cancels the current measurement or exits the mode",
|
||||||
|
"Ui.Main.ExistingMeasurementsUpdateImmediatelyValuesStored": "Existing measurements update immediately; values are stored internally in mm",
|
||||||
|
"Ui.Main.ExportMeasurementResultsCSV": "Export measurement results as CSV",
|
||||||
|
"Ui.Main.ExportMeasurementsCSV": "💾 Export Measurements as CSV…",
|
||||||
|
"Ui.Main.ExportSTEP": "📤 Export STEP…",
|
||||||
|
"Ui.Main.ExportSTL": "📐 Export STL…",
|
||||||
|
"Ui.Main.ExportStepTooltip": "Export visible models at their current positions to a new STEP file; source files stay unchanged",
|
||||||
|
"Ui.Main.Face": "⬛ Face",
|
||||||
|
"Ui.Main.FaceDistance": "⇔ Face Distance",
|
||||||
|
"Ui.Main.FileF": "File (_F)",
|
||||||
|
"Ui.Main.FilterAssemblyTreeByNameDoes": "Filter assembly tree by name (does not change 3D visibility; clear to show all)",
|
||||||
|
"Ui.Main.Fit": "🔍 Fit",
|
||||||
|
"Ui.Main.FitAllModelsView": "Fit all models in view",
|
||||||
|
"Ui.Main.Flip": "Flip",
|
||||||
|
"Ui.Main.Front": "Front",
|
||||||
|
"Ui.Main.FrontView": "Front View",
|
||||||
|
"Ui.Main.FrontViewLookAlongYZ": "Front view (look along −Y, Z up)",
|
||||||
|
"Ui.Main.Gizmo": "⊹ Gizmo",
|
||||||
|
"Ui.Main.HelpH": "Help (_H)",
|
||||||
|
"Ui.Main.HoldLeftMouseButtonMovePart": "Hold the left mouse button to move a part in the screen plane; release to place it",
|
||||||
|
"Ui.Main.IAboutSTPViewer": "ℹ About STPViewer…",
|
||||||
|
"Ui.Main.ImportSTPSTEPSTLDXFFiles": "Import STP/STEP/STL/DXF files (select multiple or drag files into the window)",
|
||||||
|
"Ui.Main.Interference": "🧩 Interference",
|
||||||
|
"Ui.Main.InterferenceCheck": "🧩 Interference Check",
|
||||||
|
"Ui.Main.InterferenceCheckTestVisibleModelPairs": "Interference check: test visible model pairs for intersections or contact; report the smallest gap",
|
||||||
|
"Ui.Main.InvertVisibility": "🔄 Invert Visibility",
|
||||||
|
"Ui.Main.Iso": "Iso",
|
||||||
|
"Ui.Main.IsometricView": "Isometric View",
|
||||||
|
"Ui.Main.LookAlongXZUp": "Look along +X, with Z up",
|
||||||
|
"Ui.Main.LookAlongYZUp": "Look along −Y, with Z up",
|
||||||
|
"Ui.Main.LookDownFromZ": "Look down from +Z",
|
||||||
|
"Ui.Main.MeasureM": "Measure (_M)",
|
||||||
|
"Ui.Main.MeasurePointCoordinatesSnapsVerticesCircle": "Measure point coordinates (snaps to vertices and circle centers)",
|
||||||
|
"Ui.Main.MeasureStraightCurvedCircularEdgeLengths": "Measure straight, curved, and circular edge lengths",
|
||||||
|
"Ui.Main.MeasureVolumeCentroid": "⚖ Measure Volume / Centroid",
|
||||||
|
"Ui.Main.Measurements": "Measurements",
|
||||||
|
"Ui.Main.MenuImport": "📂 Import…",
|
||||||
|
"Ui.Main.ModelsAssemblyTree": "Models / Assembly Tree",
|
||||||
|
"Ui.Main.Ortho": "Ortho",
|
||||||
|
"Ui.Main.OrthographicProjection": "Orthographic Projection",
|
||||||
|
"Ui.Main.OrthographicProjectionAvoidsPerspectiveDistortionClick": "Orthographic projection avoids perspective distortion; click again for perspective",
|
||||||
|
"Ui.Main.ParallelProjectionAvoidsPerspectiveDistortionUncheck": "Parallel projection avoids perspective distortion; uncheck for perspective view",
|
||||||
|
"Ui.Main.PickPointPartMoveThenPick": "Pick a point on the part to move, then pick the target point",
|
||||||
|
"Ui.Main.PickThreePointsMovingModelThen": "Pick three points on the moving model, then three matching points on the target in the same order",
|
||||||
|
"Ui.Main.Point": "📍 Point",
|
||||||
|
"Ui.Main.PointCoordinates": "📍 Point Coordinates",
|
||||||
|
"Ui.Main.PointCoordinatesVertexCircleCenterSnapping": "Point coordinates with vertex/circle-center snapping — shortcut P",
|
||||||
|
"Ui.Main.QuickBarQ": "Quick Bar (_Q)",
|
||||||
|
"Ui.Main.RecentFiles": "Recent Files",
|
||||||
|
"Ui.Main.RecentlyOpenedFiles": "Recently opened files",
|
||||||
|
"Ui.Main.RemoveThisFile": "Remove this file",
|
||||||
|
"Ui.Main.RestoreDefaultButtons": "Restore Default Buttons",
|
||||||
|
"Ui.Main.Right": "Right",
|
||||||
|
"Ui.Main.RightView": "Right View",
|
||||||
|
"Ui.Main.RightViewLookAlongXZ": "Right view (look along +X, Z up)",
|
||||||
|
"Ui.Main.Rotate90AroundX": "↻ Rotate +90° Around X",
|
||||||
|
"Ui.Main.Rotate90AroundY": "↻ Rotate +90° Around Y",
|
||||||
|
"Ui.Main.Rotate90AroundZ": "↻ Rotate +90° Around Z",
|
||||||
|
"Ui.Main.RotateModelSelectedTree90Around": "Rotate the model selected in the tree +90° around X",
|
||||||
|
"Ui.Main.RotateModelSelectedTreeAroundIts": "Rotate the model selected in the tree around its center",
|
||||||
|
"Ui.Main.RotateSelectedModel90AroundY": "Rotate the selected model +90° around Y",
|
||||||
|
"Ui.Main.RotateSelectedModel90AroundZ": "Rotate the selected model +90° around Z",
|
||||||
|
"Ui.Main.SavePNGScreenshot3DView2": "Save a PNG screenshot of the 3D view at 2× resolution",
|
||||||
|
"Ui.Main.SaveViewPNG": "📷 Save View as PNG…",
|
||||||
|
"Ui.Main.SectionAxis3PointsDefineAn": "Section axis (3 points define an arbitrary plane on the model)",
|
||||||
|
"Ui.Main.SectionLabel": "✂ Section:",
|
||||||
|
"Ui.Main.SectionPosition0100EnterValue": "Section position (0–100%; enter a value and leave the field to apply)",
|
||||||
|
"Ui.Main.SectionPositionAlongNormal": "Section position (% along normal)",
|
||||||
|
"Ui.Main.SectionView": "✂ Section View",
|
||||||
|
"Ui.Main.ShortestDistanceBetweenFacesMeshApproximation": "Shortest distance between faces (mesh approximation)",
|
||||||
|
"Ui.Main.ShortestFaceDistanceMeshApproximationShortcut": "Shortest face distance (mesh approximation) — shortcut M",
|
||||||
|
"Ui.Main.ShowAll": "☀ Show All",
|
||||||
|
"Ui.Main.ShowHideIncludingChildNodes": "Show / hide (including child nodes)",
|
||||||
|
"Ui.Main.ShowOnlyThisNode": "👁 Show Only This Node",
|
||||||
|
"Ui.Main.ShowOutlineEdgesManyEdgesCan": "Show outline edges (many edges can slow view rotation)",
|
||||||
|
"Ui.Main.ShowSectionControlsAxisPositionDirection": "Show section controls for axis, position, and direction",
|
||||||
|
"Ui.Main.ShowXYZArrowsRotationRingsSelected": "Show XYZ arrows and rotation rings for the selected model; release to place it",
|
||||||
|
"Ui.Main.StraightCurvedCircularEdgeLengthShortcut": "Straight, curved, or circular edge length — shortcut E",
|
||||||
|
"Ui.Main.SwitchDisplayUnitsBetweenMmInches": "Switch display units between mm and inches (existing measurements update)",
|
||||||
|
"Ui.Main.SwitchRetainedSide": "Switch the retained side",
|
||||||
|
"Ui.Main.ThreePointAlignmentPickThreePoints": "Three-point alignment: pick three points on the moving model, then matching points on the target",
|
||||||
|
"Ui.Main.ThreePointAlignmentRotateTranslate": "🎯 Three-Point Alignment (Rotate + Translate)",
|
||||||
|
"Ui.Main.ToggleSectionViewItsControls": "Toggle section view and its controls",
|
||||||
|
"Ui.Main.ToolbarExportStepTooltip": "Export visible models at their current positions to STEP for CAD; source files stay unchanged",
|
||||||
|
"Ui.Main.ToolbarExportStlTooltip": "Export visible models at their current positions to STL; choose scope, format, units, and quality first",
|
||||||
|
"Ui.Main.ToolbarImport": "📂 Import",
|
||||||
|
"Ui.Main.ToolbarSection": "✂ Section",
|
||||||
|
"Ui.Main.ToolsL": "Tools (_L)",
|
||||||
|
"Ui.Main.Top": "Top",
|
||||||
|
"Ui.Main.TopView": "Top View",
|
||||||
|
"Ui.Main.TopViewLookDownFromZ": "Top view (look down from +Z)",
|
||||||
|
"Ui.Main.TransformT": "Transform (_T)",
|
||||||
|
"Ui.Main.TwoPointAlignmentPickPointMoving": "Two-point alignment: pick a point on the moving part, then the target point",
|
||||||
|
"Ui.Main.TwoPointAlignmentTranslate": "🎯 Two-Point Alignment (Translate)",
|
||||||
|
"Ui.Main.TwoPointDistance": "📏 Two-Point Distance",
|
||||||
|
"Ui.Main.ViewV": "View (_V)",
|
||||||
|
"Ui.Main.WindowTitle": "STPViewer — STP/STEP 3D Viewer",
|
||||||
|
"Ui.Main.ZoomFit": "🔍 Zoom to Fit",
|
||||||
|
"Ui.Main.ZoomThisNode": "Zoom to this node",
|
||||||
|
"Ui.StlExport.Cancel": "Cancel",
|
||||||
|
"Ui.StlExport.CustomScaleTooltip": "Multiply coordinates by this factor (for example, 0.001 converts mm to m)",
|
||||||
|
"Ui.StlExport.EstimatedSize": "Estimated {0}",
|
||||||
|
"Ui.StlExport.EstimatedSizeFine": "Current mesh estimate {0} (actual size depends on re-triangulation)",
|
||||||
|
"Ui.StlExport.Export": "Export…",
|
||||||
|
"Ui.StlExport.Format": "Format",
|
||||||
|
"Ui.StlExport.FormatAscii": "ASCII (readable text; about 4× larger)",
|
||||||
|
"Ui.StlExport.FormatBinary": "Binary (recommended; smaller file)",
|
||||||
|
"Ui.StlExport.MeshQuality": "Mesh quality",
|
||||||
|
"Ui.StlExport.QualityCurrent": "Current display mesh (fast; matches the view)",
|
||||||
|
"Ui.StlExport.QualityFine": "Fine — re-triangulate for smoother surfaces (slower; larger file)",
|
||||||
|
"Ui.StlExport.QualityTooltip": "Fine quality re-triangulates STEP B-rep geometry; STL/DXF keeps the current mesh. Large assemblies take time",
|
||||||
|
"Ui.StlExport.Scope": "Export scope",
|
||||||
|
"Ui.StlExport.ScopeMerge": "All visible models → one STL",
|
||||||
|
"Ui.StlExport.ScopePerFile": "Each visible model → separate STL",
|
||||||
|
"Ui.StlExport.ScopeTooltip": "STL has no assembly structure; merge puts all visible parts in one file",
|
||||||
|
"Ui.StlExport.Summary": "{0} visible models · {1} triangles · {2}",
|
||||||
|
"Ui.StlExport.Title": "Export STL — Settings",
|
||||||
|
"Ui.StlExport.UnitCustom": "Custom scale factor…",
|
||||||
|
"Ui.StlExport.UnitInch": "inch (coordinates ÷ 25.4)",
|
||||||
|
"Ui.StlExport.UnitMillimeter": "mm (1:1; default)",
|
||||||
|
"Ui.StlExport.UnitScale": "Unit scale",
|
||||||
|
"Ui.StlExport.UnitTooltip": "STL files have no units; internal geometry is in mm, and inches divide coordinates by 25.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
{
|
||||||
|
"key": "简体中文",
|
||||||
|
"data": {
|
||||||
|
"App.ErrorTitle": "STPViewer 错误",
|
||||||
|
"App.NoRecentFiles": "(没有最近打开的文件)",
|
||||||
|
"App.UnexpectedError": "发生意外错误,操作已中止(程序将尝试继续运行)。\n\n{0}\n\n详细记录:{1}",
|
||||||
|
"App.WindowTitle": "STPViewer V{0} — STP/STEP 3D 查看器",
|
||||||
|
"QuickBar.Align": "🎯 两点对齐",
|
||||||
|
"QuickBar.Align3": "🎯 三点对齐",
|
||||||
|
"QuickBar.Angle": "∠ 角度",
|
||||||
|
"QuickBar.Circle": "⭕ 圆",
|
||||||
|
"QuickBar.Clear": "🧹 清除测量",
|
||||||
|
"QuickBar.Distance": "📏 距离",
|
||||||
|
"QuickBar.Drag": "🖐 拖动",
|
||||||
|
"QuickBar.Edge": "📐 边",
|
||||||
|
"QuickBar.ExportCsv": "💾 导出 CSV",
|
||||||
|
"QuickBar.ExportStep": "📤 导出 STEP",
|
||||||
|
"QuickBar.ExportStl": "📐 导出 STL",
|
||||||
|
"QuickBar.Face": "⬛ 面",
|
||||||
|
"QuickBar.FaceDistance": "⇔ 面距",
|
||||||
|
"QuickBar.Gizmo": "⊹ 操作器",
|
||||||
|
"QuickBar.Import": "📂 导入",
|
||||||
|
"QuickBar.Interference": "🧩 干涉",
|
||||||
|
"QuickBar.Language": "语言",
|
||||||
|
"QuickBar.Ortho": "正交投影",
|
||||||
|
"QuickBar.Point": "📍 点",
|
||||||
|
"QuickBar.Recent": "▾ 最近文件",
|
||||||
|
"QuickBar.RotateX": "旋转 ↻X",
|
||||||
|
"QuickBar.RotateY": "旋转 ↻Y",
|
||||||
|
"QuickBar.RotateZ": "旋转 ↻Z",
|
||||||
|
"QuickBar.Screenshot": "📷 截图",
|
||||||
|
"QuickBar.Section": "✂ 剖面",
|
||||||
|
"QuickBar.Unit": "mm ⇄ in",
|
||||||
|
"QuickBar.ViewFront": "视图:前",
|
||||||
|
"QuickBar.ViewIso": "视图:等轴测",
|
||||||
|
"QuickBar.ViewRight": "视图:右",
|
||||||
|
"QuickBar.ViewTop": "视图:上",
|
||||||
|
"QuickBar.ZoomAll": "🔍 显示全部",
|
||||||
|
"Runtime.Align3Completed": "已三点对齐“{0}”(旋转+平移;点 1 精确贴合,测量已清空)",
|
||||||
|
"Runtime.Align3Failed": "三点对齐失败(点位退化),请重新选点",
|
||||||
|
"Runtime.Align3OtherFile": "目标点必须位于另一个文件上",
|
||||||
|
"Runtime.Align3SameSource": "3 个来源点必须位于同一个文件(要移动:{0})",
|
||||||
|
"Runtime.Align3SourceCollinear": "三点共线,请重新选择不共线的第 3 点",
|
||||||
|
"Runtime.Align3SourceComplete": "已选完 3 个来源点 — 请在目标文件上选择对应点 1/3",
|
||||||
|
"Runtime.Align3SourcePoint": "来源点 {0}/3 — 继续选择“{1}”上的点",
|
||||||
|
"Runtime.Align3TargetCollinear": "目标三点共线,请重新选择不共线的第 3 点",
|
||||||
|
"Runtime.Align3TargetPoint": "目标点 {0}/3 — 继续选择目标文件上的对应点",
|
||||||
|
"Runtime.AlignMoved": "已移动“{0}” Δ({1:F3}, {2:F3}, {3:F3}) mm(测量已清空;可用面距或干涉验证配合)",
|
||||||
|
"Runtime.AlignMoving": "将移动“{0}” — 请在另一个文件上选择目标点",
|
||||||
|
"Runtime.AlignOtherFile": "目标点必须位于另一个文件上",
|
||||||
|
"Runtime.AllVisible": "已全部显示",
|
||||||
|
"Runtime.AngleDetail": "夹角 = {0:F2}°(补角 {1:F2}°)\n方向 1:{2} ({3:F3}, {4:F3}, {5:F3})\n方向 2:{6} ({7:F3}, {8:F3}, {9:F3})\n(两面夹角按法向量计算,互补角请参考补角值)",
|
||||||
|
"Runtime.AngleFirstDirection": "已选择第 1 个方向({0})— 选择第 2 个面或边",
|
||||||
|
"Runtime.ArcLength": "弧长",
|
||||||
|
"Runtime.Axis": "{0} 轴",
|
||||||
|
"Runtime.CircleDetail": "直径 = {0}\n半径 = {1}\n{2} = {3}\n圆心 {4}",
|
||||||
|
"Runtime.Circumference": "周长",
|
||||||
|
"Runtime.ComponentName": "组件",
|
||||||
|
"Runtime.ConicalSurface": "圆锥面",
|
||||||
|
"Runtime.CsvEmpty": "没有可导出的测量结果",
|
||||||
|
"Runtime.CsvExported": "已导出 {0} 条测量结果 → {1}",
|
||||||
|
"Runtime.CsvFilter": "CSV 文件 (*.csv)|*.csv",
|
||||||
|
"Runtime.CsvHeader": "标签,类型,明细",
|
||||||
|
"Runtime.CsvTitle": "导出测量结果",
|
||||||
|
"Runtime.CurveDetail": "曲线长度 = {0}\n类型 = {1}",
|
||||||
|
"Runtime.CurveName": "曲线",
|
||||||
|
"Runtime.CustomPlane": "自定义平面",
|
||||||
|
"Runtime.CylinderSurface": "类型 = 圆柱面\n半径 = {0}\n轴向 ({1:F3}, {2:F3}, {3:F3})",
|
||||||
|
"Runtime.DirectionEdge": "边方向",
|
||||||
|
"Runtime.DirectionFace": "面法向量",
|
||||||
|
"Runtime.DirectionMeshFace": "面法向量(网格)",
|
||||||
|
"Runtime.DistanceDetail": "距离 = {0}\nΔX = {1}\nΔY = {2}\nΔZ = {3}\nP1 {4}\nP2 {5}",
|
||||||
|
"Runtime.DistanceFirstPoint": "第 1 点 ({0:F2}, {1:F2}, {2:F2}) — 选择第 2 点",
|
||||||
|
"Runtime.Dragged": "已拖动“{0}” Δ({1:F2}, {2:F2}, {3:F2}) mm(可用干涉检查验证)",
|
||||||
|
"Runtime.Dragging": "正在拖动“{0}”…(松开定位)",
|
||||||
|
"Runtime.DxfParseFailed": "无法解析 DXF 文件",
|
||||||
|
"Runtime.ExtrusionSurface": "拉伸曲面",
|
||||||
|
"Runtime.FaceDetail": "面积 ≈ {0}(网格近似)\n{1}\n三角形数 = {2:N0}",
|
||||||
|
"Runtime.FaceDistanceDetail": "最短距离 ≈ {0}(网格近似)\nΔX = {1}\nΔY = {2}\nΔZ = {3}\n点 1 {4}\n点 2 {5}",
|
||||||
|
"Runtime.FaceDistanceFirstFace": "已选择第 1 个面 — 请选择另一个面",
|
||||||
|
"Runtime.FaceName": "面",
|
||||||
|
"Runtime.FileNotFound": "找不到文件",
|
||||||
|
"Runtime.GapLabel": "间隙 {0}",
|
||||||
|
"Runtime.GeometryProcessed": "几何处理 {0:N0} ms(三角化 {1:N0} ms、边采样 {2:N0} ms{3})",
|
||||||
|
"Runtime.GizmoApplied": "已对“{0}”应用操作器变换(可用干涉检查验证)",
|
||||||
|
"Runtime.GizmoHint": "操作器:拖动箭头沿轴移动“{0}”,拖动圆环绕轴旋转;松开即可定位",
|
||||||
|
"Runtime.GizmoSelectFile": "操作器:请先在树面板选择要操作的文件",
|
||||||
|
"Runtime.ImportBusy": "正在导入或计算,请稍后再导入",
|
||||||
|
"Runtime.ImportCompleted": "导入完成:成功 {0} 个文件{1}",
|
||||||
|
"Runtime.ImportFailed": "导入失败:{0} — {1}",
|
||||||
|
"Runtime.ImportFailedCount": "、失败 {0} 个文件",
|
||||||
|
"Runtime.ImportFailedTitle": "导入失败",
|
||||||
|
"Runtime.ImportFilter": "CAD 文件 (*.stp;*.step;*.stl;*.dxf)|*.stp;*.step;*.stl;*.dxf|STEP (*.stp;*.step)|*.stp;*.step|STL (*.stl)|*.stl|DXF (*.dxf)|*.dxf|所有文件 (*.*)|*.*",
|
||||||
|
"Runtime.ImportProgress": "导入 {0}:{1}",
|
||||||
|
"Runtime.ImportTitle": "导入 CAD 文件(可多选)",
|
||||||
|
"Runtime.Importing": "正在导入:{0} …",
|
||||||
|
"Runtime.InterferenceChecking": "正在检查干涉(第 {0} 组):{1} ⟷ {2} …",
|
||||||
|
"Runtime.InterferenceClearDetail": "{0} ⟷ {1} 无相交\n最小间隙 ≈ {2}(网格近似)\n间隙 ≈ 0 表示贴合\n点 1 {3}\n点 2 {4}",
|
||||||
|
"Runtime.InterferenceClearTitle": "🧩 无干涉 间隙 ≈ {0}",
|
||||||
|
"Runtime.InterferenceCompleted": "干涉检查完成:{0} 组配对、{1} 组相交",
|
||||||
|
"Runtime.InterferenceFailed": "干涉检查失败:{0}",
|
||||||
|
"Runtime.InterferenceHitDetail": "零件相交(非贴合)\n相交三角形对 = {0:N0}\n红色线 = 干涉交线({1:N0} 段)\n(共面贴合不计为干涉)",
|
||||||
|
"Runtime.InterferenceHitTitle": "🧩 发生干涉!{0} ⟷ {1}",
|
||||||
|
"Runtime.InterferenceLabel": "干涉",
|
||||||
|
"Runtime.InterferenceNeedTwo": "干涉检查至少需要 2 个可见文件(当前 {0} 个),请在树面板勾选",
|
||||||
|
"Runtime.Isolated": "只显示“{0}”(树右键 → 全部显示 可恢复)",
|
||||||
|
"Runtime.KindAlign": "对齐",
|
||||||
|
"Runtime.KindAlign3": "三点对齐",
|
||||||
|
"Runtime.KindAngle": "角度",
|
||||||
|
"Runtime.KindCircle": "圆",
|
||||||
|
"Runtime.KindDistance": "距离",
|
||||||
|
"Runtime.KindDrag": "拖动",
|
||||||
|
"Runtime.KindEdge": "边",
|
||||||
|
"Runtime.KindFace": "面",
|
||||||
|
"Runtime.KindFaceDistance": "面距",
|
||||||
|
"Runtime.KindInterference": "干涉",
|
||||||
|
"Runtime.KindNone": "无",
|
||||||
|
"Runtime.KindPoint": "点",
|
||||||
|
"Runtime.KindVolume": "体积",
|
||||||
|
"Runtime.LineDetail": "长度 = {0}\nΔX = {1} ΔY = {2} ΔZ = {3}\n起点 {4}\n终点 {5}",
|
||||||
|
"Runtime.MeasureFailed": "测量失败:{0}",
|
||||||
|
"Runtime.MeasurementStartCancelled": "已取消当前测量起点(再次按 Esc 退出测量模式)",
|
||||||
|
"Runtime.MeasurementsCleared": "已清除全部测量",
|
||||||
|
"Runtime.MeshMeasurementLimited": "此模型无 B-rep(STL/网格),仅支持点、距离、角度和面距测量",
|
||||||
|
"Runtime.MeshName": "网格",
|
||||||
|
"Runtime.MeshNameNumbered": "网格 {0}",
|
||||||
|
"Runtime.MeshSurface": "类型 = 网格(无 B-rep)",
|
||||||
|
"Runtime.ModeAlign": "两点对齐:先点要移动零件上的点,再点目标点(仅平移,测量将清空)",
|
||||||
|
"Runtime.ModeAlign3": "三点对齐:先在要移动的文件中选 3 个特征点,再在目标文件中选 3 个对应点(旋转+平移)",
|
||||||
|
"Runtime.ModeAngle": "角度测量:选择第 1 个面(或直线边附近)",
|
||||||
|
"Runtime.ModeBrowse": "浏览模式(右键旋转、滚轮缩放、中键或 Shift+左键拖动平移)",
|
||||||
|
"Runtime.ModeCircle": "圆测量:点击圆孔内壁或圆边附近",
|
||||||
|
"Runtime.ModeDistance": "距离测量:选择第 1 点",
|
||||||
|
"Runtime.ModeDrag": "拖动模式:按住左键沿屏幕平面移动零件,松开定位;需改变深度时请先旋转视角",
|
||||||
|
"Runtime.ModeEdge": "边测量:点击要测量的边附近",
|
||||||
|
"Runtime.ModeFace": "面测量:点击要测量的面",
|
||||||
|
"Runtime.ModeFaceDistance": "面距测量:选择第 1 个面",
|
||||||
|
"Runtime.ModePoint": "点测量:点击模型表面(自动吸附邻近顶点)",
|
||||||
|
"Runtime.NoBrep": " · 无 B-rep",
|
||||||
|
"Runtime.NoCircularEdge": "此面没有圆形边(请点击圆孔内壁或圆边附近)",
|
||||||
|
"Runtime.NoGeometry": "文件中没有可显示的几何",
|
||||||
|
"Runtime.NoMeasurableEdge": "此面没有可测量的边",
|
||||||
|
"Runtime.NoSurfaceHit": "未命中模型表面",
|
||||||
|
"Runtime.NodeStats": "{0} 实体 · {1} 面 · {2:N0} △",
|
||||||
|
"Runtime.NurbsSurface": "NURBS 曲面",
|
||||||
|
"Runtime.Orthographic": "正交投影(平行投影,适合尺寸确认)",
|
||||||
|
"Runtime.OutlineSize": "\n外形 {0}",
|
||||||
|
"Runtime.ParsingStep": "解析 STEP(大文件可能需要数分钟)…",
|
||||||
|
"Runtime.ParsingStl": "解析 STL(三角形直读)…",
|
||||||
|
"Runtime.Perspective": "透视投影",
|
||||||
|
"Runtime.PlaneSurface": "类型 = 平面\n法向量 ({0:F3}, {1:F3}, {2:F3})",
|
||||||
|
"Runtime.Ready": "就绪 — 导入 STP/STEP/STL/DXF(也可拖放)。右键旋转、滚轮缩放、中键或 Shift+左键平移",
|
||||||
|
"Runtime.Removed": "已移除:{0}",
|
||||||
|
"Runtime.RetriedFaces": "、重试补回 {0} 面",
|
||||||
|
"Runtime.Reversed": "(反向)",
|
||||||
|
"Runtime.RevolutionSurface": "旋转曲面",
|
||||||
|
"Runtime.RootNotFound": "找不到此零件所属文件",
|
||||||
|
"Runtime.RotateSelectFile": "请先在树面板选择要旋转的文件",
|
||||||
|
"Runtime.Rotated": "已将“{0}”绕 {1} 轴旋转 +90°(再次点击可继续旋转)",
|
||||||
|
"Runtime.SameFace": "两次选中了同一个面,请选择另一个面",
|
||||||
|
"Runtime.ScreenshotFilter": "PNG 图片 (*.png)|*.png",
|
||||||
|
"Runtime.ScreenshotSaved": "已保存截图 → {0}",
|
||||||
|
"Runtime.ScreenshotTitle": "保存视图截图",
|
||||||
|
"Runtime.SectionCancelled": "已取消三点剖面定义",
|
||||||
|
"Runtime.SectionClosed": "剖面已关闭",
|
||||||
|
"Runtime.SectionCollinear": "三点剖面:三点共线,请选择不共线的第 3 点",
|
||||||
|
"Runtime.SectionDefined": "三点剖面:平面已定义(法向 {0:F2}, {1:F2}, {2:F2})— 用位置滑块扫描剖面",
|
||||||
|
"Runtime.SectionDegenerate": "三点剖面:三点退化(共线),请重新选择",
|
||||||
|
"Runtime.SectionNoHit": "三点剖面:未命中模型表面(已选 {0}/3 点,Esc 取消)",
|
||||||
|
"Runtime.SectionPickPrompt": "三点剖面:请在模型上选择 3 个点定义剖切平面(Esc 取消)",
|
||||||
|
"Runtime.SectionPicked": "三点剖面:已选 {0}/3 点",
|
||||||
|
"Runtime.SectionStatus": "剖面:{0} {1:F0}%{2}",
|
||||||
|
"Runtime.ShellName": "壳体",
|
||||||
|
"Runtime.SolidName": "实体",
|
||||||
|
"Runtime.SphericalSurface": "球面",
|
||||||
|
"Runtime.StepExportEmpty": "没有可导出的 B-rep 几何(STEP 仅包含 Solid/Shell;请勾选要导出的 STEP 文件)",
|
||||||
|
"Runtime.StepExportFailed": "STEP 导出失败:{0}",
|
||||||
|
"Runtime.StepExportFilter": "STEP 文件 (*.stp)|*.stp|STEP 文件 (*.step)|*.step",
|
||||||
|
"Runtime.StepExportTitle": "将当前场景导出为 STEP(零件处于当前对齐位置)",
|
||||||
|
"Runtime.StepExported": "已导出 STEP({0} 个实体,当前位置)→ {1}",
|
||||||
|
"Runtime.StepParseFailed": "CADability 无法解析此 STEP 文件",
|
||||||
|
"Runtime.StepParsed": "STEP 解析 {0:N0} ms",
|
||||||
|
"Runtime.StlExportEmpty": "没有可导出的网格(请勾选至少一个含实体网格的文件)",
|
||||||
|
"Runtime.StlExportFailed": "STL 导出失败:{0}",
|
||||||
|
"Runtime.StlExportFilter": "STL 文件 (*.stl)|*.stl",
|
||||||
|
"Runtime.StlExportMergedTitle": "导出 STL(合并为单个文件)",
|
||||||
|
"Runtime.StlExportPerFileTitle": "导出 STL(每个文件分别导出,以此作为基础文件名)",
|
||||||
|
"Runtime.StlExportedMany": "已导出 {0} 个 STL(共 {1:N0} 个三角形)→ {2}",
|
||||||
|
"Runtime.StlExportedOne": "已导出 STL({0:N0} 个三角形)→ {1}",
|
||||||
|
"Runtime.StlExporting": "正在导出 STL…",
|
||||||
|
"Runtime.StlNoTriangles": "STL 文件中没有三角形",
|
||||||
|
"Runtime.StlParsed": "STL 解析 {0:N0} ms({1} 个网格)",
|
||||||
|
"Runtime.StlRetriangulating": "正在重新三角化并导出 STL…(大型装配需要时间)",
|
||||||
|
"Runtime.SurfaceType": "类型 = {0}",
|
||||||
|
"Runtime.ToroidalSurface": "环面",
|
||||||
|
"Runtime.Triangulating": "并行三角化 {0} 个零件…",
|
||||||
|
"Runtime.UnitInch": "显示单位:inch",
|
||||||
|
"Runtime.UnitMillimeter": "显示单位:mm",
|
||||||
|
"Runtime.UnsupportedFormat": "不支持的格式(支持 .stp / .step / .stl / .dxf;CADability 无 IGES reader)",
|
||||||
|
"Runtime.View": "视图:{0}",
|
||||||
|
"Runtime.ViewBack": "后",
|
||||||
|
"Runtime.ViewBottom": "下",
|
||||||
|
"Runtime.ViewFront": "前",
|
||||||
|
"Runtime.ViewIso": "等角",
|
||||||
|
"Runtime.ViewLeft": "左",
|
||||||
|
"Runtime.ViewRight": "右",
|
||||||
|
"Runtime.ViewTop": "上",
|
||||||
|
"Runtime.VisibilityInverted": "已反转显示",
|
||||||
|
"Runtime.VolumeDetail": "体积 ≈ {0}(网格近似)\n质心 {1}\n外形(AABB){2} × {3} × {4} {5}",
|
||||||
|
"Runtime.VolumeNoMesh": "“{0}”没有可计算的面网格(线框无体积)",
|
||||||
|
"Runtime.VolumeOpenMesh": "“{0}”不是封闭网格,无法可靠计算体积",
|
||||||
|
"Ui.About.Changelog": "📋 版本纪录",
|
||||||
|
"Ui.About.Close": "关闭",
|
||||||
|
"Ui.About.Description": "STP / STEP 3D 查看器。免开重量级 CAD,快速打开文件、测量、对齐插合、干涉检查、剖面、导出。",
|
||||||
|
"Ui.About.Developer": "👤 开发者",
|
||||||
|
"Ui.About.Email": "邮箱",
|
||||||
|
"Ui.About.Name": "姓名",
|
||||||
|
"Ui.About.Release010.Change1": "• STEP/STL/DXF 导入与装配树、点/距离/边/面/圆/角度/面距测量、两点与三点对齐、干涉检查、剖面、mm ⇄ inch、CSV 与截图导出。",
|
||||||
|
"Ui.About.Release010.Title": "首次发布",
|
||||||
|
"Ui.About.Release020.Change1": "• 添加拖曳模式:左键按住零件直接拖到想要的位置,放开就定位。",
|
||||||
|
"Ui.About.Release020.Change2": "• 0.2.1 修正拖曳过的零件再点击会当掉的问题。",
|
||||||
|
"Ui.About.Release020.Title": "拖曳模式",
|
||||||
|
"Ui.About.Release030.Change1": "• 添加操作器:选取文件后出现 XYZ 三色箭头与旋转环,箭头沿轴精确移动、旋转环绕轴转任意角度。",
|
||||||
|
"Ui.About.Release030.Title": "三轴操作器",
|
||||||
|
"Ui.About.Release031.Change1": "• 三轴操作器改成永远浮在画面最上层,不会被零件挡住,任何角度都抓得到。",
|
||||||
|
"Ui.About.Release031.Title": "操作器永远可见",
|
||||||
|
"Ui.About.Release032.Change1": "• 修正大型组件(数万个面)进入测量模式后,转动视角严重卡顿的问题 — 现在测量跟浏览一样顺。",
|
||||||
|
"Ui.About.Release032.Title": "大组件测量不再卡",
|
||||||
|
"Ui.About.Release040.Change1": "• 大文件拉剖面滑杆不再卡住画面(改到背景计算)。",
|
||||||
|
"Ui.About.Release040.Change2": "• 拖曳/操作器放开零件后的定位计算大幅加速。",
|
||||||
|
"Ui.About.Release040.Change3": "• 程序遇到错误会提示消息并继续运作,不再整个闪退。",
|
||||||
|
"Ui.About.Release040.Title": "更快、更稳",
|
||||||
|
"Ui.About.Release050.Change1": "• 量距离时点到圆孔边缘会自动吸到圆心 — 量两孔中心距(pitch)超方便。",
|
||||||
|
"Ui.About.Release050.Change2": "• 测量快速键:P 点、D 距离、E 边、F 面、C 圆、A 角度、M 面距;Esc 取消。",
|
||||||
|
"Ui.About.Release050.Change3": "• 树状面板可搜索零件名称;右键可「只显示此节点」隔离观察,也可以量体积与质心。",
|
||||||
|
"Ui.About.Release050.Change4": "• 添加标准视图(等角/前/上/右)与正交投影,量尺寸不受透视变形。",
|
||||||
|
"Ui.About.Release050.Change5": "• 剖面添加「3点」模式:在模型上点 3 个点,就能沿任意斜面剖开。",
|
||||||
|
"Ui.About.Release050.Change6": "• 添加导出 STEP:把对齐好的零件位置存成新 STEP 文件,交接给正式 CAD。",
|
||||||
|
"Ui.About.Release050.Change7": "• 会记住窗口大小、单位设置与最近打开的 10 个文件。",
|
||||||
|
"Ui.About.Release050.Title": "测量、视图、树状面板大升级",
|
||||||
|
"Ui.About.Release060.Change1": "• 添加导出 STL:把画面上的零件(含对齐、旋转后的位置)存成 STL 网格文件,可拿去 3D 打印或给其他软件用。",
|
||||||
|
"Ui.About.Release060.Change2": "• 导出前会先跳出设置窗口:全部合并成一个文件或每个文件各存一个、Binary 或文本格式、mm 或 inch、网格精细度 — 确认后才导出。",
|
||||||
|
"Ui.About.Release060.Change3": "• 设置窗口会即时显示三角形数量与预估文件大小。",
|
||||||
|
"Ui.About.Release060.Title": "导出 STL",
|
||||||
|
"Ui.About.Release070.Change1": "• 上方添加菜单栏,全部功能依「文件/视图/测量/变换/工具」分类,不用再在一长排按钮里找。",
|
||||||
|
"Ui.About.Release070.Change2": "• 原本的工具栏变成「快捷栏」:只放常用按钮。想放哪些自己决定 — 到「快捷栏 → 自定义按钮」勾选,下次打开会记住。",
|
||||||
|
"Ui.About.Release070.Change3": "• 剖面的轴向/位置控制列改成只在打开剖面时出现,平常不占空间。",
|
||||||
|
"Ui.About.Release070.Change4": "• 添加「关于」窗口(就是这个画面),可以看版本纪录。",
|
||||||
|
"Ui.About.Release070.Title": "界面大改版 — 菜单栏+可自定义快捷栏",
|
||||||
|
"Ui.About.Release071.Change1": "• 打开大型 STL 网格文件(数十 MB、上百万三角形)现在几乎瞬间完成,不用再等好几分钟。",
|
||||||
|
"Ui.About.Release071.Change2": "• 例如一个 75 MB、150 万个三角形的 STL,以前要等到天荒地老,现在不到 1 秒就显示出来。",
|
||||||
|
"Ui.About.Release071.Title": "大型 STL 秒开",
|
||||||
|
"Ui.About.Release080.Change1": "• 在「视图 → 语言」或快速栏即可切换 English、繁體中文、简体中文,整个界面立即更换(连画面上已经测量好的结果也会跟着换),下次打开会记住。",
|
||||||
|
"Ui.About.Release080.Change2": "• 语言是程序旁 i18n 文件夹里的普通文件:放一个新的进去,菜单就多一种语言,无需更新程序。尚未翻译的文字会先以英文显示。",
|
||||||
|
"Ui.About.Release080.Change3": "• 感谢 @CarmJos 贡献这项功能。",
|
||||||
|
"Ui.About.Release080.Title": "语言切换:英文/繁体中文/简体中文",
|
||||||
|
"Ui.About.Technology": ".NET 8 · WPF · C#;CADability(STEP / B-rep 几何内核);HelixToolkit(3D 显示与相机)",
|
||||||
|
"Ui.About.TechnologyTitle": "🔧 技术栈",
|
||||||
|
"Ui.About.Title": "关于 STPViewer",
|
||||||
|
"Ui.Language": "语言",
|
||||||
|
"Ui.Main.3AxisGizmo": "⊹ 三轴操作器",
|
||||||
|
"Ui.Main.3AxisGizmoUseXYZArrows": "三轴操作器:对树面板选取的文件显示 XYZ 箭头(沿轴移动)与旋转环(绕轴转任意角度),每次放开即定位",
|
||||||
|
"Ui.Main.3Point": "🎯 三点",
|
||||||
|
"Ui.Main.3Points": "3点",
|
||||||
|
"Ui.Main.Align": "🎯 对齐",
|
||||||
|
"Ui.Main.Angle": "∠ 角度",
|
||||||
|
"Ui.Main.AngleBetweenFacesStraightEdgesShortcut": "两面/两边夹角(点两个面或靠近直线边)— 快速键 A",
|
||||||
|
"Ui.Main.AngleBetweenTwoFacesStraightEdges": "两面/两边夹角(点两个面或靠近直线边)",
|
||||||
|
"Ui.Main.ApplicationInformationReleaseHistory": "程序信息与版本纪录",
|
||||||
|
"Ui.Main.AreaSurfaceTypeNormalVector": "面积 + 曲面类型/法矢量",
|
||||||
|
"Ui.Main.AreaSurfaceTypeNormalVectorShortcut": "面积 + 曲面类型/法矢量 — 快速键 F",
|
||||||
|
"Ui.Main.Capture": "📷 截图",
|
||||||
|
"Ui.Main.CenterRadiusDiameterCircumference": "圆心/半径/直径/周长",
|
||||||
|
"Ui.Main.CenterRadiusDiameterCircumferenceShortcutC": "圆心/半径/直径/周长 — 快速键 C",
|
||||||
|
"Ui.Main.CheckButtonsShowQuickBarSaved": "勾选要显示在快捷栏的常用按钮(随设置保存)",
|
||||||
|
"Ui.Main.CheckVisibleModelPairsIntersectionsReport": "2 个以上可见文件两两检查是否相交;无相交时回报最小间隙",
|
||||||
|
"Ui.Main.ChooseScopeFormatUnitsMeshQuality": "先设置范围/格式/单位/精度,确认再导出",
|
||||||
|
"Ui.Main.Circle": "⭕ 圆",
|
||||||
|
"Ui.Main.Clear": "🧹 清除",
|
||||||
|
"Ui.Main.ClearAllMeasurements": "🧹 清除全部测量",
|
||||||
|
"Ui.Main.ClearAllMeasurementsMarkersEscCancels": "清除全部测量与标记(Esc = 取消进行中测量/退出模式)",
|
||||||
|
"Ui.Main.ClickChangeColorIncludingChildNodes": "点击换色(含子节点)",
|
||||||
|
"Ui.Main.Close": "✕ 关闭",
|
||||||
|
"Ui.Main.CloseSectionView": "关闭剖面查看",
|
||||||
|
"Ui.Main.CustomizeButtons": "自定义按钮",
|
||||||
|
"Ui.Main.DeleteThisMeasurement": "删除此笔测量",
|
||||||
|
"Ui.Main.DisplayInchesMm": "单位显示 inch(mm ⇄ in)",
|
||||||
|
"Ui.Main.Distance": "📏 距离",
|
||||||
|
"Ui.Main.DistanceXYZCircleCenterSnappingHole": "两点距离 + ΔXYZ(点到圆边会吸圆心 → 可量孔对孔 pitch)— 快速键 D",
|
||||||
|
"Ui.Main.DistanceXYZCircularEdgesSnapCenters": "两点距离 + ΔXYZ(点到圆边会吸圆心 → 可量孔对孔 pitch)",
|
||||||
|
"Ui.Main.Drag": "🖐 拖曳",
|
||||||
|
"Ui.Main.DragMode": "🖐 拖曳模式",
|
||||||
|
"Ui.Main.DragModeHoldLeftMouseButton": "拖曳模式:左键按住零件沿屏幕平面拖动,放开定位(深度方向先转视角再拖)",
|
||||||
|
"Ui.Main.DragResizePanel": "拖曳调整面板宽度",
|
||||||
|
"Ui.Main.Edge": "📐 边",
|
||||||
|
"Ui.Main.EdgeLength": "📐 边长",
|
||||||
|
"Ui.Main.Edges": "边",
|
||||||
|
"Ui.Main.EscCancelsCurrentMeasurementExitsMode": "Esc = 取消进行中测量/退出模式",
|
||||||
|
"Ui.Main.ExistingMeasurementsUpdateImmediatelyValuesStored": "既有测量即时换算;内部一律存 mm",
|
||||||
|
"Ui.Main.ExportMeasurementResultsCSV": "测量结果导出 CSV",
|
||||||
|
"Ui.Main.ExportMeasurementsCSV": "💾 导出测量 CSV…",
|
||||||
|
"Ui.Main.ExportSTEP": "📤 导出 STEP…",
|
||||||
|
"Ui.Main.ExportSTL": "📐 导出 STL…",
|
||||||
|
"Ui.Main.ExportStepTooltip": "把可见文件(当前对齐位置)导出成新 STEP 文件;不动源文件",
|
||||||
|
"Ui.Main.Face": "⬛ 面",
|
||||||
|
"Ui.Main.FaceDistance": "⇔ 面距",
|
||||||
|
"Ui.Main.FileF": "文件(_F)",
|
||||||
|
"Ui.Main.FilterAssemblyTreeByNameDoes": "输入名称过滤装配树(只影响树面板显示,不影响 3D 可见性;清空 = 全显)",
|
||||||
|
"Ui.Main.Fit": "🔍 全览",
|
||||||
|
"Ui.Main.FitAllModelsView": "缩放至全部模型",
|
||||||
|
"Ui.Main.Flip": "反向",
|
||||||
|
"Ui.Main.Front": "前",
|
||||||
|
"Ui.Main.FrontView": "前视图",
|
||||||
|
"Ui.Main.FrontViewLookAlongYZ": "前视图(-Y 方向看,Z 向上)",
|
||||||
|
"Ui.Main.Gizmo": "⊹ 操作器",
|
||||||
|
"Ui.Main.HelpH": "说明(_H)",
|
||||||
|
"Ui.Main.HoldLeftMouseButtonMovePart": "左键按住零件沿屏幕平面拖动,放开定位",
|
||||||
|
"Ui.Main.IAboutSTPViewer": "ℹ 关于 STPViewer…",
|
||||||
|
"Ui.Main.ImportSTPSTEPSTLDXFFiles": "导入 STP/STEP/STL/DXF(可多选,也可直接拖放到窗口)",
|
||||||
|
"Ui.Main.Interference": "🧩 干涉",
|
||||||
|
"Ui.Main.InterferenceCheck": "🧩 干涉检查",
|
||||||
|
"Ui.Main.InterferenceCheckTestVisibleModelPairs": "干涉检查:2 个以上可见文件两两检查是否相交(match 判定);无相交时回报最小间隙",
|
||||||
|
"Ui.Main.InvertVisibility": "🔄 反转显示",
|
||||||
|
"Ui.Main.Iso": "等角",
|
||||||
|
"Ui.Main.IsometricView": "等角视图",
|
||||||
|
"Ui.Main.LookAlongXZUp": "+X 方向看,Z 向上",
|
||||||
|
"Ui.Main.LookAlongYZUp": "-Y 方向看,Z 向上",
|
||||||
|
"Ui.Main.LookDownFromZ": "+Z 往下看",
|
||||||
|
"Ui.Main.MeasureM": "测量(_M)",
|
||||||
|
"Ui.Main.MeasurePointCoordinatesSnapsVerticesCircle": "测量点坐标(吸附顶点/圆心)",
|
||||||
|
"Ui.Main.MeasureStraightCurvedCircularEdgeLengths": "边长(直线/曲线/圆弧)",
|
||||||
|
"Ui.Main.MeasureVolumeCentroid": "⚖ 量体积 / 质心",
|
||||||
|
"Ui.Main.Measurements": "测量结果",
|
||||||
|
"Ui.Main.MenuImport": "📂 导入…",
|
||||||
|
"Ui.Main.ModelsAssemblyTree": "模型 / 装配树",
|
||||||
|
"Ui.Main.Ortho": "正交",
|
||||||
|
"Ui.Main.OrthographicProjection": "正交投影",
|
||||||
|
"Ui.Main.OrthographicProjectionAvoidsPerspectiveDistortionClick": "正交投影(平行投影,量尺寸不受透视变形;再按切回透视)",
|
||||||
|
"Ui.Main.ParallelProjectionAvoidsPerspectiveDistortionUncheck": "平行投影,量尺寸不受透视变形;取消勾选切回透视",
|
||||||
|
"Ui.Main.PickPointPartMoveThenPick": "先点要移动零件上的点,再点目标点",
|
||||||
|
"Ui.Main.PickThreePointsMovingModelThen": "先在要移动的文件点 3 个特征点,再到目标文件点 3 个对应点(顺序对应)",
|
||||||
|
"Ui.Main.Point": "📍 点",
|
||||||
|
"Ui.Main.PointCoordinates": "📍 点坐标",
|
||||||
|
"Ui.Main.PointCoordinatesVertexCircleCenterSnapping": "测量点坐标(吸附顶点/圆心)— 快速键 P",
|
||||||
|
"Ui.Main.QuickBarQ": "快捷栏(_Q)",
|
||||||
|
"Ui.Main.RecentFiles": "最近文件",
|
||||||
|
"Ui.Main.RecentlyOpenedFiles": "最近打开的文件",
|
||||||
|
"Ui.Main.RemoveThisFile": "移除此文件",
|
||||||
|
"Ui.Main.RestoreDefaultButtons": "恢复缺省按钮",
|
||||||
|
"Ui.Main.Right": "右",
|
||||||
|
"Ui.Main.RightView": "右视图",
|
||||||
|
"Ui.Main.RightViewLookAlongXZ": "右视图(+X 方向看,Z 向上)",
|
||||||
|
"Ui.Main.Rotate90AroundX": "↻ 绕 X 轴 +90°",
|
||||||
|
"Ui.Main.Rotate90AroundY": "↻ 绕 Y 轴 +90°",
|
||||||
|
"Ui.Main.Rotate90AroundZ": "↻ 绕 Z 轴 +90°",
|
||||||
|
"Ui.Main.RotateModelSelectedTree90Around": "选取的文件绕 X 轴 +90°(先在树面板点击文件)",
|
||||||
|
"Ui.Main.RotateModelSelectedTreeAroundIts": "对树面板选取的文件,绕其中心旋转(先在树面板点击文件)",
|
||||||
|
"Ui.Main.RotateSelectedModel90AroundY": "选取的文件绕 Y 轴 +90°",
|
||||||
|
"Ui.Main.RotateSelectedModel90AroundZ": "选取的文件绕 Z 轴 +90°",
|
||||||
|
"Ui.Main.SavePNGScreenshot3DView2": "3D 视图截图存 PNG(2x 分辨率)",
|
||||||
|
"Ui.Main.SaveViewPNG": "📷 视图截图 PNG…",
|
||||||
|
"Ui.Main.SectionAxis3PointsDefineAn": "剖切轴向(3点 = 在模型上点 3 点定义任意平面)",
|
||||||
|
"Ui.Main.SectionLabel": "✂ 剖面:",
|
||||||
|
"Ui.Main.SectionPosition0100EnterValue": "剖面位置 %(0–100,可直接输入,离开字段生效)",
|
||||||
|
"Ui.Main.SectionPositionAlongNormal": "剖面位置(沿法向 %)",
|
||||||
|
"Ui.Main.SectionView": "✂ 剖面查看",
|
||||||
|
"Ui.Main.ShortestDistanceBetweenFacesMeshApproximation": "面到面最短距离(网格近似)",
|
||||||
|
"Ui.Main.ShortestFaceDistanceMeshApproximationShortcut": "面到面最短距离(网格近似)— 快速键 M",
|
||||||
|
"Ui.Main.ShowAll": "☀ 全部显示",
|
||||||
|
"Ui.Main.ShowHideIncludingChildNodes": "显示 / 隐藏(含子节点)",
|
||||||
|
"Ui.Main.ShowOnlyThisNode": "👁 只显示此节点",
|
||||||
|
"Ui.Main.ShowOutlineEdgesManyEdgesCan": "显示轮廓边线(边线量大时转动视角会变慢)",
|
||||||
|
"Ui.Main.ShowSectionControlsAxisPositionDirection": "打开后下方出现剖面参数表(轴向/位置/反向)",
|
||||||
|
"Ui.Main.ShowXYZArrowsRotationRingsSelected": "对树面板选取的文件显示 XYZ 箭头与旋转环,每次放开即定位",
|
||||||
|
"Ui.Main.StraightCurvedCircularEdgeLengthShortcut": "边长(直线/曲线/圆弧)— 快速键 E",
|
||||||
|
"Ui.Main.SwitchDisplayUnitsBetweenMmInches": "切换显示单位 mm ⇄ inch(既有测量即时换算)",
|
||||||
|
"Ui.Main.SwitchRetainedSide": "切换保留侧",
|
||||||
|
"Ui.Main.ThreePointAlignmentPickThreePoints": "三点对齐(旋转+平移):先在要移动的文件点 3 个特征点,再到目标文件点 3 个对应点(顺序对应)",
|
||||||
|
"Ui.Main.ThreePointAlignmentRotateTranslate": "🎯 三点对齐(旋转+平移)",
|
||||||
|
"Ui.Main.ToggleSectionViewItsControls": "打开/关闭剖面查看(打开后下方出现参数表)",
|
||||||
|
"Ui.Main.ToolbarExportStepTooltip": "把可见文件(当前对齐位置)导出成新 STEP 文件 — 对齐插合结果可交接 CAD;不动源文件",
|
||||||
|
"Ui.Main.ToolbarExportStlTooltip": "把可见文件(当前位置)导出成 STL 网格文件 — 先设置范围/格式/单位/精度,确认再导出",
|
||||||
|
"Ui.Main.ToolbarImport": "📂 导入",
|
||||||
|
"Ui.Main.ToolbarSection": "✂ 剖面",
|
||||||
|
"Ui.Main.ToolsL": "工具(_L)",
|
||||||
|
"Ui.Main.Top": "上",
|
||||||
|
"Ui.Main.TopView": "上视图",
|
||||||
|
"Ui.Main.TopViewLookDownFromZ": "上视图(+Z 往下看)",
|
||||||
|
"Ui.Main.TransformT": "变换(_T)",
|
||||||
|
"Ui.Main.TwoPointAlignmentPickPointMoving": "两点对齐(平移零件):先点要移动零件上的点,再点目标点",
|
||||||
|
"Ui.Main.TwoPointAlignmentTranslate": "🎯 两点对齐(平移)",
|
||||||
|
"Ui.Main.TwoPointDistance": "📏 两点距离",
|
||||||
|
"Ui.Main.ViewV": "视图(_V)",
|
||||||
|
"Ui.Main.WindowTitle": "STPViewer — STP/STEP 3D 查看器",
|
||||||
|
"Ui.Main.ZoomFit": "🔍 缩放至全部",
|
||||||
|
"Ui.Main.ZoomThisNode": "缩放至此节点",
|
||||||
|
"Ui.StlExport.Cancel": "取消",
|
||||||
|
"Ui.StlExport.CustomScaleTooltip": "坐标乘上此系数(例:0.001 = mm→m)",
|
||||||
|
"Ui.StlExport.EstimatedSize": "预估 {0}",
|
||||||
|
"Ui.StlExport.EstimatedSizeFine": "以当前网格预估 {0}(实际依重新三角化结果)",
|
||||||
|
"Ui.StlExport.Export": "导出…",
|
||||||
|
"Ui.StlExport.Format": "格式",
|
||||||
|
"Ui.StlExport.FormatAscii": "ASCII(文本可读,文件约 4 倍大)",
|
||||||
|
"Ui.StlExport.FormatBinary": "Binary(建议,文件小)",
|
||||||
|
"Ui.StlExport.MeshQuality": "网格精度",
|
||||||
|
"Ui.StlExport.QualityCurrent": "当前显示网格(快,与画面一致)",
|
||||||
|
"Ui.StlExport.QualityFine": "精细 — 重新三角化,曲面更平滑(慢、文件大)",
|
||||||
|
"Ui.StlExport.QualityTooltip": "精细会由 B-rep 重新三角化(仅 STEP 来源;STL/DXF 维持现有网格),大组件需要时间",
|
||||||
|
"Ui.StlExport.Scope": "导出范围",
|
||||||
|
"Ui.StlExport.ScopeMerge": "全部可见文件 → 合并成单一 STL",
|
||||||
|
"Ui.StlExport.ScopePerFile": "每个可见文件 → 各一个 STL",
|
||||||
|
"Ui.StlExport.ScopeTooltip": "STL 无装配结构;合并 = 所有可见零件进同一个文件",
|
||||||
|
"Ui.StlExport.Summary": "{0} 个可见文件 · {1} 三角形 · {2}",
|
||||||
|
"Ui.StlExport.Title": "导出 STL — 参数设置",
|
||||||
|
"Ui.StlExport.UnitCustom": "自定义缩放系数…",
|
||||||
|
"Ui.StlExport.UnitInch": "inch(坐标 ÷ 25.4)",
|
||||||
|
"Ui.StlExport.UnitMillimeter": "mm(1:1,缺省)",
|
||||||
|
"Ui.StlExport.UnitScale": "单位缩放",
|
||||||
|
"Ui.StlExport.UnitTooltip": "STL 文件本身无单位;内部几何为 mm,选 inch 会把坐标 ÷25.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
{
|
||||||
|
"key": "繁體中文",
|
||||||
|
"data": {
|
||||||
|
"App.ErrorTitle": "STPViewer 錯誤",
|
||||||
|
"App.NoRecentFiles": "(沒有最近開啟的檔案)",
|
||||||
|
"App.UnexpectedError": "發生未預期的錯誤,操作已中止(程式將嘗試繼續運作)。\n\n{0}\n\n詳細記錄:{1}",
|
||||||
|
"App.WindowTitle": "STPViewer V{0} — STP/STEP 3D 檢視器",
|
||||||
|
"QuickBar.Align": "🎯 兩點對齊",
|
||||||
|
"QuickBar.Align3": "🎯 三點對齊",
|
||||||
|
"QuickBar.Angle": "∠ 角度",
|
||||||
|
"QuickBar.Circle": "⭕ 圓",
|
||||||
|
"QuickBar.Clear": "🧹 清除量測",
|
||||||
|
"QuickBar.Distance": "📏 距離",
|
||||||
|
"QuickBar.Drag": "🖐 拖曳",
|
||||||
|
"QuickBar.Edge": "📐 邊",
|
||||||
|
"QuickBar.ExportCsv": "💾 匯出 CSV",
|
||||||
|
"QuickBar.ExportStep": "📤 匯出 STEP",
|
||||||
|
"QuickBar.ExportStl": "📐 匯出 STL",
|
||||||
|
"QuickBar.Face": "⬛ 面",
|
||||||
|
"QuickBar.FaceDistance": "⇔ 面距",
|
||||||
|
"QuickBar.Gizmo": "⊹ 操作器",
|
||||||
|
"QuickBar.Import": "📂 匯入",
|
||||||
|
"QuickBar.Interference": "🧩 干涉",
|
||||||
|
"QuickBar.Language": "語言",
|
||||||
|
"QuickBar.Ortho": "正交投影",
|
||||||
|
"QuickBar.Point": "📍 點",
|
||||||
|
"QuickBar.Recent": "▾ 最近檔案",
|
||||||
|
"QuickBar.RotateX": "旋轉 ↻X",
|
||||||
|
"QuickBar.RotateY": "旋轉 ↻Y",
|
||||||
|
"QuickBar.RotateZ": "旋轉 ↻Z",
|
||||||
|
"QuickBar.Screenshot": "📷 截圖",
|
||||||
|
"QuickBar.Section": "✂ 剖面",
|
||||||
|
"QuickBar.Unit": "mm ⇄ in",
|
||||||
|
"QuickBar.ViewFront": "視圖:前",
|
||||||
|
"QuickBar.ViewIso": "視圖:等角",
|
||||||
|
"QuickBar.ViewRight": "視圖:右",
|
||||||
|
"QuickBar.ViewTop": "視圖:上",
|
||||||
|
"QuickBar.ZoomAll": "🔍 全覽",
|
||||||
|
"Runtime.Align3Completed": "已三點對齊「{0}」(旋轉+平移;點1精確貼合,量測已清空,可用 🧩干涉 驗證)",
|
||||||
|
"Runtime.Align3Failed": "三點對齊失敗(點位退化),請重新選點",
|
||||||
|
"Runtime.Align3OtherFile": "目標點要點在另一個檔案上",
|
||||||
|
"Runtime.Align3SameSource": "來源 3 點要同一個檔案(要移動:{0})",
|
||||||
|
"Runtime.Align3SourceCollinear": "三點共線,請改選不共線的第 3 點",
|
||||||
|
"Runtime.Align3SourceComplete": "來源 3 點完成 — 換點「目標檔案」上的對應點 1/3",
|
||||||
|
"Runtime.Align3SourcePoint": "來源點 {0}/3 — 繼續點「{1}」上的點",
|
||||||
|
"Runtime.Align3TargetCollinear": "目標三點共線,請改選不共線的第 3 點",
|
||||||
|
"Runtime.Align3TargetPoint": "目標點 {0}/3 — 繼續點目標檔案上的對應點",
|
||||||
|
"Runtime.AlignMoved": "已移動「{0}」 Δ({1:F3}, {2:F3}, {3:F3}) mm(量測已清空;可用 ⇔面距 / 🧩干涉 驗證配合)",
|
||||||
|
"Runtime.AlignMoving": "將移動「{0}」— 點選目標點(另一個檔案上)",
|
||||||
|
"Runtime.AlignOtherFile": "目標點要點在另一個檔案上(兩點同屬一個檔案無法對齊)",
|
||||||
|
"Runtime.AllVisible": "已全部顯示",
|
||||||
|
"Runtime.AngleDetail": "夾角 = {0:F2}°(補角 {1:F2}°)\n方向 1:{2} ({3:F3}, {4:F3}, {5:F3})\n方向 2:{6} ({7:F3}, {8:F3}, {9:F3})\n(兩面夾角以法向量計,互補角請參考補角值)",
|
||||||
|
"Runtime.AngleFirstDirection": "已選第 1 個方向({0})— 點選第 2 個面或邊",
|
||||||
|
"Runtime.ArcLength": "弧長",
|
||||||
|
"Runtime.Axis": "{0} 軸",
|
||||||
|
"Runtime.CircleDetail": "直徑 = {0}\n半徑 = {1}\n{2} = {3}\n圓心 {4}",
|
||||||
|
"Runtime.Circumference": "周長",
|
||||||
|
"Runtime.ComponentName": "組件",
|
||||||
|
"Runtime.ConicalSurface": "圓錐面",
|
||||||
|
"Runtime.CsvEmpty": "沒有量測結果可匯出",
|
||||||
|
"Runtime.CsvExported": "已匯出 {0} 筆量測 → {1}",
|
||||||
|
"Runtime.CsvFilter": "CSV 檔案 (*.csv)|*.csv",
|
||||||
|
"Runtime.CsvHeader": "標籤,類型,明細",
|
||||||
|
"Runtime.CsvTitle": "匯出量測結果",
|
||||||
|
"Runtime.CurveDetail": "曲線長 = {0}\n類型 = {1}",
|
||||||
|
"Runtime.CurveName": "曲線",
|
||||||
|
"Runtime.CustomPlane": "自訂平面",
|
||||||
|
"Runtime.CylinderSurface": "類型 = 圓柱面\n半徑 = {0}\n軸向 ({1:F3}, {2:F3}, {3:F3})",
|
||||||
|
"Runtime.DirectionEdge": "邊方向",
|
||||||
|
"Runtime.DirectionFace": "面法向量",
|
||||||
|
"Runtime.DirectionMeshFace": "面法向量(網格)",
|
||||||
|
"Runtime.DistanceDetail": "距離 = {0}\nΔX = {1}\nΔY = {2}\nΔZ = {3}\nP1 {4}\nP2 {5}",
|
||||||
|
"Runtime.DistanceFirstPoint": "第 1 點 ({0:F2}, {1:F2}, {2:F2}) — 點選第 2 點",
|
||||||
|
"Runtime.Dragged": "已拖曳「{0}」 Δ({1:F2}, {2:F2}, {3:F2}) mm(可用 🧩干涉 驗證)",
|
||||||
|
"Runtime.Dragging": "拖曳「{0}」中…(放開定位)",
|
||||||
|
"Runtime.DxfParseFailed": "無法解析 DXF 檔",
|
||||||
|
"Runtime.ExtrusionSurface": "拉伸曲面",
|
||||||
|
"Runtime.FaceDetail": "面積 ≈ {0}(網格近似)\n{1}\n三角形數 = {2:N0}",
|
||||||
|
"Runtime.FaceDistanceDetail": "最短距離 ≈ {0}(網格近似)\nΔX = {1}\nΔY = {2}\nΔZ = {3}\n點 1 {4}\n點 2 {5}",
|
||||||
|
"Runtime.FaceDistanceFirstFace": "已選第 1 個面 — 點選另一個面",
|
||||||
|
"Runtime.FaceName": "面",
|
||||||
|
"Runtime.FileNotFound": "找不到檔案",
|
||||||
|
"Runtime.GapLabel": "gap {0}",
|
||||||
|
"Runtime.GeometryProcessed": "幾何處理 {0:N0} ms(三角化 {1:N0} ms、邊取樣 {2:N0} ms{3})",
|
||||||
|
"Runtime.GizmoApplied": "已套用操作器變換「{0}」(可用 🧩干涉 驗證)",
|
||||||
|
"Runtime.GizmoHint": "操作器:拖箭頭沿軸移動「{0}」、拖環繞軸旋轉;放開即定位",
|
||||||
|
"Runtime.GizmoSelectFile": "操作器:請先在樹面板點選要操作的檔案",
|
||||||
|
"Runtime.ImportBusy": "忙碌中(匯入或運算進行中),請稍候再匯入",
|
||||||
|
"Runtime.ImportCompleted": "匯入完成:成功 {0} 檔{1}",
|
||||||
|
"Runtime.ImportFailed": "匯入失敗:{0} — {1}",
|
||||||
|
"Runtime.ImportFailedCount": "、失敗 {0} 檔",
|
||||||
|
"Runtime.ImportFailedTitle": "匯入失敗",
|
||||||
|
"Runtime.ImportFilter": "CAD 檔案 (*.stp;*.step;*.stl;*.dxf)|*.stp;*.step;*.stl;*.dxf|STEP (*.stp;*.step)|*.stp;*.step|STL (*.stl)|*.stl|DXF (*.dxf)|*.dxf|所有檔案 (*.*)|*.*",
|
||||||
|
"Runtime.ImportProgress": "匯入 {0}:{1}",
|
||||||
|
"Runtime.ImportTitle": "匯入 CAD 檔案(可複選)",
|
||||||
|
"Runtime.Importing": "匯入中:{0} …",
|
||||||
|
"Runtime.InterferenceChecking": "干涉檢查中(第 {0} 組):{1} ⟷ {2} …",
|
||||||
|
"Runtime.InterferenceClearDetail": "{0} ⟷ {1} 無相交\n最小間隙 ≈ {2}(網格近似)\ngap ≈ 0 即為貼合(match)\n點 1 {3}\n點 2 {4}",
|
||||||
|
"Runtime.InterferenceClearTitle": "🧩 無干涉 gap ≈ {0}",
|
||||||
|
"Runtime.InterferenceCompleted": "干涉檢查完成:{0} 組配對、{1} 組相交",
|
||||||
|
"Runtime.InterferenceFailed": "干涉檢查失敗:{0}",
|
||||||
|
"Runtime.InterferenceHitDetail": "兩零件相交(不 match)\n相交三角形對 = {0:N0}\n紅色線 = 干涉交線({1:N0} 段)\n(共面貼合不會被算為干涉)",
|
||||||
|
"Runtime.InterferenceHitTitle": "🧩 干涉!{0} ⟷ {1}",
|
||||||
|
"Runtime.InterferenceLabel": "干涉",
|
||||||
|
"Runtime.InterferenceNeedTwo": "干涉檢查需要至少 2 個可見檔案(目前 {0} 個)— 用樹面板勾選",
|
||||||
|
"Runtime.Isolated": "只顯示「{0}」(樹右鍵 → 全部顯示 復原)",
|
||||||
|
"Runtime.KindAlign": "對齊",
|
||||||
|
"Runtime.KindAlign3": "三點對齊",
|
||||||
|
"Runtime.KindAngle": "角度",
|
||||||
|
"Runtime.KindCircle": "圓",
|
||||||
|
"Runtime.KindDistance": "距離",
|
||||||
|
"Runtime.KindDrag": "拖曳",
|
||||||
|
"Runtime.KindEdge": "邊",
|
||||||
|
"Runtime.KindFace": "面",
|
||||||
|
"Runtime.KindFaceDistance": "面距",
|
||||||
|
"Runtime.KindInterference": "干涉",
|
||||||
|
"Runtime.KindNone": "無",
|
||||||
|
"Runtime.KindPoint": "點",
|
||||||
|
"Runtime.KindVolume": "體積",
|
||||||
|
"Runtime.LineDetail": "長度 = {0}\nΔX = {1} ΔY = {2} ΔZ = {3}\n起點 {4}\n終點 {5}",
|
||||||
|
"Runtime.MeasureFailed": "量測失敗:{0}",
|
||||||
|
"Runtime.MeasurementStartCancelled": "已取消目前量測起點(再按 Esc 退出量測模式)",
|
||||||
|
"Runtime.MeasurementsCleared": "已清除全部量測",
|
||||||
|
"Runtime.MeshMeasurementLimited": "此模型無 B-rep(STL/網格),僅支援 點 / 距離 / 角度 / 面距 量測",
|
||||||
|
"Runtime.MeshName": "網格",
|
||||||
|
"Runtime.MeshNameNumbered": "網格 {0}",
|
||||||
|
"Runtime.MeshSurface": "類型 = 網格(無 B-rep)",
|
||||||
|
"Runtime.ModeAlign": "兩點對齊:先點「要移動零件」上的點,再點目標點(純平移,量測會清空)",
|
||||||
|
"Runtime.ModeAlign3": "三點對齊:先在「要移動的檔案」點 3 個特徵點,再到目標檔案點 3 個對應點(旋轉+平移)",
|
||||||
|
"Runtime.ModeAngle": "角度量測:點選第 1 個面(或靠近直線邊)",
|
||||||
|
"Runtime.ModeBrowse": "瀏覽模式(右鍵旋轉、滾輪縮放、中鍵或 Shift+左鍵拖曳平移)",
|
||||||
|
"Runtime.ModeCircle": "圓量測:點擊圓孔內壁或圓邊附近",
|
||||||
|
"Runtime.ModeDistance": "距離量測:點選第 1 點",
|
||||||
|
"Runtime.ModeDrag": "拖曳模式:左鍵按住零件拖動(沿螢幕平面),放開定位;深度方向先轉視角再拖。右鍵仍可轉視角",
|
||||||
|
"Runtime.ModeEdge": "邊量測:點擊靠近要量的邊",
|
||||||
|
"Runtime.ModeFace": "面量測:點擊要量的面",
|
||||||
|
"Runtime.ModeFaceDistance": "面距量測:點選第 1 個面",
|
||||||
|
"Runtime.ModePoint": "點量測:點擊模型表面(自動吸附鄰近頂點)",
|
||||||
|
"Runtime.NoBrep": " · 無 B-rep",
|
||||||
|
"Runtime.NoCircularEdge": "此面沒有圓形邊(請點圓孔內壁或圓邊附近)",
|
||||||
|
"Runtime.NoGeometry": "檔案內沒有可顯示的幾何",
|
||||||
|
"Runtime.NoMeasurableEdge": "此面沒有可量測的邊",
|
||||||
|
"Runtime.NoSurfaceHit": "未命中模型表面",
|
||||||
|
"Runtime.NodeStats": "{0} 實體 · {1} 面 · {2:N0} △",
|
||||||
|
"Runtime.NurbsSurface": "NURBS 曲面",
|
||||||
|
"Runtime.Orthographic": "正交投影(平行投影,適合尺寸確認)",
|
||||||
|
"Runtime.OutlineSize": "\n外形 {0}",
|
||||||
|
"Runtime.ParsingStep": "解析 STEP(大檔可能需數分鐘)…",
|
||||||
|
"Runtime.ParsingStl": "解析 STL(直接讀三角形)…",
|
||||||
|
"Runtime.Perspective": "透視投影",
|
||||||
|
"Runtime.PlaneSurface": "類型 = 平面\n法向量 ({0:F3}, {1:F3}, {2:F3})",
|
||||||
|
"Runtime.Ready": "就緒 — 匯入 STP/STEP/STL/DXF(拖放亦可)。右鍵旋轉、滾輪縮放、中鍵或 Shift+左鍵平移",
|
||||||
|
"Runtime.Removed": "已移除:{0}",
|
||||||
|
"Runtime.RetriedFaces": "、重試補回 {0} 面",
|
||||||
|
"Runtime.Reversed": "(反向)",
|
||||||
|
"Runtime.RevolutionSurface": "旋轉曲面",
|
||||||
|
"Runtime.RootNotFound": "找不到此零件所屬檔案",
|
||||||
|
"Runtime.RotateSelectFile": "請先在樹面板點選要旋轉的檔案(任一節點即可)",
|
||||||
|
"Runtime.Rotated": "已旋轉「{0}」繞 {1} 軸 +90°(再按可繼續轉)",
|
||||||
|
"Runtime.SameFace": "兩次點到同一個面,請點選另一個面",
|
||||||
|
"Runtime.ScreenshotFilter": "PNG 圖片 (*.png)|*.png",
|
||||||
|
"Runtime.ScreenshotSaved": "已儲存截圖 → {0}",
|
||||||
|
"Runtime.ScreenshotTitle": "儲存視圖截圖",
|
||||||
|
"Runtime.SectionCancelled": "已取消 3 點剖面定義",
|
||||||
|
"Runtime.SectionClosed": "剖面已關閉",
|
||||||
|
"Runtime.SectionCollinear": "3點剖面:三點共線,請改選不共線的第 3 點",
|
||||||
|
"Runtime.SectionDefined": "3點剖面:平面已定義(法向 {0:F2}, {1:F2}, {2:F2})— 用位置滑桿掃剖面",
|
||||||
|
"Runtime.SectionDegenerate": "3點剖面:三點退化(共線),請重新點選",
|
||||||
|
"Runtime.SectionNoHit": "3點剖面:未命中模型表面(已選 {0}/3 點,Esc 取消)",
|
||||||
|
"Runtime.SectionPickPrompt": "3點剖面:請在模型上點 3 個點定義剖切平面(Esc 取消)",
|
||||||
|
"Runtime.SectionPicked": "3點剖面:已選 {0}/3 點",
|
||||||
|
"Runtime.SectionStatus": "剖面:{0} {1:F0}%{2}",
|
||||||
|
"Runtime.ShellName": "殼體",
|
||||||
|
"Runtime.SolidName": "實體",
|
||||||
|
"Runtime.SphericalSurface": "球面",
|
||||||
|
"Runtime.StepExportEmpty": "沒有可匯出的 B-rep 幾何(STEP 匯出僅含 Solid/Shell;請勾選要匯出的 STEP 檔案)",
|
||||||
|
"Runtime.StepExportFailed": "STEP 匯出失敗:{0}",
|
||||||
|
"Runtime.StepExportFilter": "STEP 檔案 (*.stp)|*.stp|STEP 檔案 (*.step)|*.step",
|
||||||
|
"Runtime.StepExportTitle": "匯出目前場景為 STEP(零件在目前對齊位置)",
|
||||||
|
"Runtime.StepExported": "已匯出 STEP({0} 個實體,目前位置)→ {1}",
|
||||||
|
"Runtime.StepParseFailed": "CADability 無法解析此 STEP 檔",
|
||||||
|
"Runtime.StepParsed": "STEP 解析 {0:N0} ms",
|
||||||
|
"Runtime.StlExportEmpty": "沒有可匯出的網格(請勾選至少一個含實體網格的檔案)",
|
||||||
|
"Runtime.StlExportFailed": "STL 匯出失敗:{0}",
|
||||||
|
"Runtime.StlExportFilter": "STL 檔案 (*.stl)|*.stl",
|
||||||
|
"Runtime.StlExportMergedTitle": "匯出 STL(合併單一檔)",
|
||||||
|
"Runtime.StlExportPerFileTitle": "匯出 STL(每個檔案各一個,以此為基底檔名)",
|
||||||
|
"Runtime.StlExportedMany": "已匯出 {0} 個 STL(共 {1:N0} 三角形)→ {2}",
|
||||||
|
"Runtime.StlExportedOne": "已匯出 STL({0:N0} 三角形)→ {1}",
|
||||||
|
"Runtime.StlExporting": "匯出 STL 中…",
|
||||||
|
"Runtime.StlNoTriangles": "STL 檔內沒有三角形",
|
||||||
|
"Runtime.StlParsed": "STL 解析 {0:N0} ms({1} 個網格)",
|
||||||
|
"Runtime.StlRetriangulating": "重新三角化 + 匯出 STL 中…(大組件需要時間)",
|
||||||
|
"Runtime.SurfaceType": "類型 = {0}",
|
||||||
|
"Runtime.ToroidalSurface": "環面",
|
||||||
|
"Runtime.Triangulating": "三角化 {0} 個零件(平行)…",
|
||||||
|
"Runtime.UnitInch": "顯示單位:inch",
|
||||||
|
"Runtime.UnitMillimeter": "顯示單位:mm",
|
||||||
|
"Runtime.UnsupportedFormat": "不支援的格式(支援 .stp / .step / .stl / .dxf;CADability 無 IGES reader)",
|
||||||
|
"Runtime.View": "視圖:{0}",
|
||||||
|
"Runtime.ViewBack": "後",
|
||||||
|
"Runtime.ViewBottom": "下",
|
||||||
|
"Runtime.ViewFront": "前",
|
||||||
|
"Runtime.ViewIso": "等角",
|
||||||
|
"Runtime.ViewLeft": "左",
|
||||||
|
"Runtime.ViewRight": "右",
|
||||||
|
"Runtime.ViewTop": "上",
|
||||||
|
"Runtime.VisibilityInverted": "已反轉顯示",
|
||||||
|
"Runtime.VolumeDetail": "體積 ≈ {0}(網格近似)\n質心 {1}\n外形(AABB){2} × {3} × {4} {5}",
|
||||||
|
"Runtime.VolumeNoMesh": "「{0}」沒有可計算的面網格(線架構無體積)",
|
||||||
|
"Runtime.VolumeOpenMesh": "「{0}」非封閉網格(開放殼),無法可靠計算體積",
|
||||||
|
"Ui.About.Changelog": "📋 版本紀錄",
|
||||||
|
"Ui.About.Close": "關閉",
|
||||||
|
"Ui.About.Description": "STP / STEP 3D 檢視器。免開重量級 CAD,快速開檔、量測、對齊插合、干涉檢查、剖面、匯出。",
|
||||||
|
"Ui.About.Developer": "👤 開發者",
|
||||||
|
"Ui.About.Email": "電子郵件",
|
||||||
|
"Ui.About.Name": "姓名",
|
||||||
|
"Ui.About.Release010.Change1": "• STEP/STL/DXF 匯入與裝配樹、點/距離/邊/面/圓/角度/面距量測、兩點與三點對齊、干涉檢查、剖面、mm ⇄ inch、CSV 與截圖匯出。",
|
||||||
|
"Ui.About.Release010.Title": "首次釋出",
|
||||||
|
"Ui.About.Release020.Change1": "• 新增拖曳模式:左鍵按住零件直接拖到想要的位置,放開就定位。",
|
||||||
|
"Ui.About.Release020.Change2": "• 0.2.1 修正拖曳過的零件再點擊會當掉的問題。",
|
||||||
|
"Ui.About.Release020.Title": "拖曳模式",
|
||||||
|
"Ui.About.Release030.Change1": "• 新增操作器:選取檔案後出現 XYZ 三色箭頭與旋轉環,箭頭沿軸精確移動、旋轉環繞軸轉任意角度。",
|
||||||
|
"Ui.About.Release030.Title": "三軸操作器",
|
||||||
|
"Ui.About.Release031.Change1": "• 三軸操作器改成永遠浮在畫面最上層,不會被零件擋住,任何角度都抓得到。",
|
||||||
|
"Ui.About.Release031.Title": "操作器永遠可見",
|
||||||
|
"Ui.About.Release032.Change1": "• 修正大型組件(數萬個面)進入量測模式後,轉動視角嚴重卡頓的問題 — 現在量測跟瀏覽一樣順。",
|
||||||
|
"Ui.About.Release032.Title": "大組件量測不再卡",
|
||||||
|
"Ui.About.Release040.Change1": "• 大檔案拉剖面滑桿不再卡住畫面(改到背景計算)。",
|
||||||
|
"Ui.About.Release040.Change2": "• 拖曳/操作器放開零件後的定位計算大幅加速。",
|
||||||
|
"Ui.About.Release040.Change3": "• 程式遇到錯誤會提示訊息並繼續運作,不再整個閃退。",
|
||||||
|
"Ui.About.Release040.Title": "更快、更穩",
|
||||||
|
"Ui.About.Release050.Change1": "• 量距離時點到圓孔邊緣會自動吸到圓心 — 量兩孔中心距(pitch)超方便。",
|
||||||
|
"Ui.About.Release050.Change2": "• 量測快速鍵:P 點、D 距離、E 邊、F 面、C 圓、A 角度、M 面距;Esc 取消。",
|
||||||
|
"Ui.About.Release050.Change3": "• 樹狀面板可搜尋零件名稱;右鍵可「只顯示此節點」隔離觀察,也可以量體積與質心。",
|
||||||
|
"Ui.About.Release050.Change4": "• 新增標準視圖(等角/前/上/右)與正交投影,量尺寸不受透視變形。",
|
||||||
|
"Ui.About.Release050.Change5": "• 剖面新增「3點」模式:在模型上點 3 個點,就能沿任意斜面剖開。",
|
||||||
|
"Ui.About.Release050.Change6": "• 新增匯出 STEP:把對齊好的零件位置存成新 STEP 檔,交接給正式 CAD。",
|
||||||
|
"Ui.About.Release050.Change7": "• 會記住視窗大小、單位設定與最近開啟的 10 個檔案。",
|
||||||
|
"Ui.About.Release050.Title": "量測、視圖、樹狀面板大升級",
|
||||||
|
"Ui.About.Release060.Change1": "• 新增匯出 STL:把畫面上的零件(含對齊、旋轉後的位置)存成 STL 網格檔,可拿去 3D 列印或給其他軟體用。",
|
||||||
|
"Ui.About.Release060.Change2": "• 匯出前會先跳出設定視窗:全部合併成一個檔或每個檔案各存一個、Binary 或文字格式、mm 或 inch、網格精細度 — 確認後才匯出。",
|
||||||
|
"Ui.About.Release060.Change3": "• 設定視窗會即時顯示三角形數量與預估檔案大小。",
|
||||||
|
"Ui.About.Release060.Title": "匯出 STL",
|
||||||
|
"Ui.About.Release070.Change1": "• 上方新增選單列,全部功能依「檔案/視圖/量測/變換/工具」分類,不用再在一長排按鈕裡找。",
|
||||||
|
"Ui.About.Release070.Change2": "• 原本的工具列變成「快速列」:只放常用按鈕。想放哪些自己決定 — 到「快速列 → 自訂按鈕」勾選,下次開啟會記住。",
|
||||||
|
"Ui.About.Release070.Change3": "• 剖面的軸向/位置控制列改成只在開啟剖面時出現,平常不佔空間。",
|
||||||
|
"Ui.About.Release070.Change4": "• 新增「關於」視窗(就是這個畫面),可以看版本紀錄。",
|
||||||
|
"Ui.About.Release070.Title": "介面大整理 — 選單列+可自訂快速列",
|
||||||
|
"Ui.About.Release071.Change1": "• 開啟大型 STL 網格檔(數十 MB、上百萬三角形)現在幾乎瞬間完成,不用再等好幾分鐘。",
|
||||||
|
"Ui.About.Release071.Change2": "• 例如一個 75 MB、150 萬個三角形的 STL,以前要等到天荒地老,現在不到 1 秒就顯示出來。",
|
||||||
|
"Ui.About.Release071.Title": "大型 STL 秒開",
|
||||||
|
"Ui.About.Release080.Change1": "• 到「視圖 → 語言」或快速列就能切換 English、繁體中文、简体中文,整個介面立刻換掉(連畫面上已經量好的結果也會跟著換),下次開啟會記住。",
|
||||||
|
"Ui.About.Release080.Change2": "• 語言是程式旁 i18n 資料夾裡的一般檔案:放一個新的進去,選單就多一種語言,不用更新程式。還沒翻譯到的文字會先用英文顯示。",
|
||||||
|
"Ui.About.Release080.Change3": "• 感謝 @CarmJos 貢獻這項功能。",
|
||||||
|
"Ui.About.Release080.Title": "語言切換:英文/繁體中文/簡體中文",
|
||||||
|
"Ui.About.Technology": ".NET 8 · WPF · C#;CADability(STEP / B-rep 幾何核心);HelixToolkit(3D 顯示與相機)",
|
||||||
|
"Ui.About.TechnologyTitle": "🔧 技術棧",
|
||||||
|
"Ui.About.Title": "關於 STPViewer",
|
||||||
|
"Ui.Language": "語言",
|
||||||
|
"Ui.Main.3AxisGizmo": "⊹ 三軸操作器",
|
||||||
|
"Ui.Main.3AxisGizmoUseXYZArrows": "三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位",
|
||||||
|
"Ui.Main.3Point": "🎯 三點",
|
||||||
|
"Ui.Main.3Points": "3點",
|
||||||
|
"Ui.Main.Align": "🎯 對齊",
|
||||||
|
"Ui.Main.Angle": "∠ 角度",
|
||||||
|
"Ui.Main.AngleBetweenFacesStraightEdgesShortcut": "兩面/兩邊夾角(點兩個面或靠近直線邊)— 快速鍵 A",
|
||||||
|
"Ui.Main.AngleBetweenTwoFacesStraightEdges": "兩面/兩邊夾角(點兩個面或靠近直線邊)",
|
||||||
|
"Ui.Main.ApplicationInformationReleaseHistory": "程式資訊與版本紀錄",
|
||||||
|
"Ui.Main.AreaSurfaceTypeNormalVector": "面積 + 曲面類型/法向量",
|
||||||
|
"Ui.Main.AreaSurfaceTypeNormalVectorShortcut": "面積 + 曲面類型/法向量 — 快速鍵 F",
|
||||||
|
"Ui.Main.Capture": "📷 截圖",
|
||||||
|
"Ui.Main.CenterRadiusDiameterCircumference": "圓心/半徑/直徑/周長",
|
||||||
|
"Ui.Main.CenterRadiusDiameterCircumferenceShortcutC": "圓心/半徑/直徑/周長 — 快速鍵 C",
|
||||||
|
"Ui.Main.CheckButtonsShowQuickBarSaved": "勾選要顯示在快速列的常用按鈕(隨設定保存)",
|
||||||
|
"Ui.Main.CheckVisibleModelPairsIntersectionsReport": "2 個以上可見檔案兩兩檢查是否相交;無相交時回報最小間隙",
|
||||||
|
"Ui.Main.ChooseScopeFormatUnitsMeshQuality": "先設定範圍/格式/單位/精度,確認再匯出",
|
||||||
|
"Ui.Main.Circle": "⭕ 圓",
|
||||||
|
"Ui.Main.Clear": "🧹 清除",
|
||||||
|
"Ui.Main.ClearAllMeasurements": "🧹 清除全部量測",
|
||||||
|
"Ui.Main.ClearAllMeasurementsMarkersEscCancels": "清除全部量測與標記(Esc = 取消進行中量測/退出模式)",
|
||||||
|
"Ui.Main.ClickChangeColorIncludingChildNodes": "點擊換色(含子節點)",
|
||||||
|
"Ui.Main.Close": "✕ 關閉",
|
||||||
|
"Ui.Main.CloseSectionView": "關閉剖面檢視",
|
||||||
|
"Ui.Main.CustomizeButtons": "自訂按鈕",
|
||||||
|
"Ui.Main.DeleteThisMeasurement": "刪除此筆量測",
|
||||||
|
"Ui.Main.DisplayInchesMm": "單位顯示 inch(mm ⇄ in)",
|
||||||
|
"Ui.Main.Distance": "📏 距離",
|
||||||
|
"Ui.Main.DistanceXYZCircleCenterSnappingHole": "兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)— 快速鍵 D",
|
||||||
|
"Ui.Main.DistanceXYZCircularEdgesSnapCenters": "兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)",
|
||||||
|
"Ui.Main.Drag": "🖐 拖曳",
|
||||||
|
"Ui.Main.DragMode": "🖐 拖曳模式",
|
||||||
|
"Ui.Main.DragModeHoldLeftMouseButton": "拖曳模式:左鍵按住零件沿螢幕平面拖動,放開定位(深度方向先轉視角再拖)",
|
||||||
|
"Ui.Main.DragResizePanel": "拖曳調整面板寬度",
|
||||||
|
"Ui.Main.Edge": "📐 邊",
|
||||||
|
"Ui.Main.EdgeLength": "📐 邊長",
|
||||||
|
"Ui.Main.Edges": "邊",
|
||||||
|
"Ui.Main.EscCancelsCurrentMeasurementExitsMode": "Esc = 取消進行中量測/退出模式",
|
||||||
|
"Ui.Main.ExistingMeasurementsUpdateImmediatelyValuesStored": "既有量測即時換算;內部一律存 mm",
|
||||||
|
"Ui.Main.ExportMeasurementResultsCSV": "量測結果匯出 CSV",
|
||||||
|
"Ui.Main.ExportMeasurementsCSV": "💾 匯出量測 CSV…",
|
||||||
|
"Ui.Main.ExportSTEP": "📤 匯出 STEP…",
|
||||||
|
"Ui.Main.ExportSTL": "📐 匯出 STL…",
|
||||||
|
"Ui.Main.ExportStepTooltip": "把可見檔案(目前對齊位置)匯出成新 STEP 檔;不動原始檔",
|
||||||
|
"Ui.Main.Face": "⬛ 面",
|
||||||
|
"Ui.Main.FaceDistance": "⇔ 面距",
|
||||||
|
"Ui.Main.FileF": "檔案(_F)",
|
||||||
|
"Ui.Main.FilterAssemblyTreeByNameDoes": "輸入名稱過濾裝配樹(只影響樹面板顯示,不影響 3D 可見性;清空 = 全顯)",
|
||||||
|
"Ui.Main.Fit": "🔍 全覽",
|
||||||
|
"Ui.Main.FitAllModelsView": "縮放至全部模型",
|
||||||
|
"Ui.Main.Flip": "反向",
|
||||||
|
"Ui.Main.Front": "前",
|
||||||
|
"Ui.Main.FrontView": "前視圖",
|
||||||
|
"Ui.Main.FrontViewLookAlongYZ": "前視圖(-Y 方向看,Z 向上)",
|
||||||
|
"Ui.Main.Gizmo": "⊹ 操作器",
|
||||||
|
"Ui.Main.HelpH": "說明(_H)",
|
||||||
|
"Ui.Main.HoldLeftMouseButtonMovePart": "左鍵按住零件沿螢幕平面拖動,放開定位",
|
||||||
|
"Ui.Main.IAboutSTPViewer": "ℹ 關於 STPViewer…",
|
||||||
|
"Ui.Main.ImportSTPSTEPSTLDXFFiles": "匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)",
|
||||||
|
"Ui.Main.Interference": "🧩 干涉",
|
||||||
|
"Ui.Main.InterferenceCheck": "🧩 干涉檢查",
|
||||||
|
"Ui.Main.InterferenceCheckTestVisibleModelPairs": "干涉檢查:2 個以上可見檔案兩兩檢查是否相交(match 判定);無相交時回報最小間隙",
|
||||||
|
"Ui.Main.InvertVisibility": "🔄 反轉顯示",
|
||||||
|
"Ui.Main.Iso": "等角",
|
||||||
|
"Ui.Main.IsometricView": "等角視圖",
|
||||||
|
"Ui.Main.LookAlongXZUp": "+X 方向看,Z 向上",
|
||||||
|
"Ui.Main.LookAlongYZUp": "-Y 方向看,Z 向上",
|
||||||
|
"Ui.Main.LookDownFromZ": "+Z 往下看",
|
||||||
|
"Ui.Main.MeasureM": "量測(_M)",
|
||||||
|
"Ui.Main.MeasurePointCoordinatesSnapsVerticesCircle": "量測點座標(吸附頂點/圓心)",
|
||||||
|
"Ui.Main.MeasureStraightCurvedCircularEdgeLengths": "邊長(直線/曲線/圓弧)",
|
||||||
|
"Ui.Main.MeasureVolumeCentroid": "⚖ 量體積 / 質心",
|
||||||
|
"Ui.Main.Measurements": "量測結果",
|
||||||
|
"Ui.Main.MenuImport": "📂 匯入…",
|
||||||
|
"Ui.Main.ModelsAssemblyTree": "模型 / 裝配樹",
|
||||||
|
"Ui.Main.Ortho": "正交",
|
||||||
|
"Ui.Main.OrthographicProjection": "正交投影",
|
||||||
|
"Ui.Main.OrthographicProjectionAvoidsPerspectiveDistortionClick": "正交投影(平行投影,量尺寸不受透視變形;再按切回透視)",
|
||||||
|
"Ui.Main.ParallelProjectionAvoidsPerspectiveDistortionUncheck": "平行投影,量尺寸不受透視變形;取消勾選切回透視",
|
||||||
|
"Ui.Main.PickPointPartMoveThenPick": "先點要移動零件上的點,再點目標點",
|
||||||
|
"Ui.Main.PickThreePointsMovingModelThen": "先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)",
|
||||||
|
"Ui.Main.Point": "📍 點",
|
||||||
|
"Ui.Main.PointCoordinates": "📍 點座標",
|
||||||
|
"Ui.Main.PointCoordinatesVertexCircleCenterSnapping": "量測點座標(吸附頂點/圓心)— 快速鍵 P",
|
||||||
|
"Ui.Main.QuickBarQ": "快速列(_Q)",
|
||||||
|
"Ui.Main.RecentFiles": "最近檔案",
|
||||||
|
"Ui.Main.RecentlyOpenedFiles": "最近開啟的檔案",
|
||||||
|
"Ui.Main.RemoveThisFile": "移除此檔案",
|
||||||
|
"Ui.Main.RestoreDefaultButtons": "恢復預設按鈕",
|
||||||
|
"Ui.Main.Right": "右",
|
||||||
|
"Ui.Main.RightView": "右視圖",
|
||||||
|
"Ui.Main.RightViewLookAlongXZ": "右視圖(+X 方向看,Z 向上)",
|
||||||
|
"Ui.Main.Rotate90AroundX": "↻ 繞 X 軸 +90°",
|
||||||
|
"Ui.Main.Rotate90AroundY": "↻ 繞 Y 軸 +90°",
|
||||||
|
"Ui.Main.Rotate90AroundZ": "↻ 繞 Z 軸 +90°",
|
||||||
|
"Ui.Main.RotateModelSelectedTree90Around": "選取的檔案繞 X 軸 +90°(先在樹面板點選檔案)",
|
||||||
|
"Ui.Main.RotateModelSelectedTreeAroundIts": "對樹面板選取的檔案,繞其中心旋轉(先在樹面板點選檔案)",
|
||||||
|
"Ui.Main.RotateSelectedModel90AroundY": "選取的檔案繞 Y 軸 +90°",
|
||||||
|
"Ui.Main.RotateSelectedModel90AroundZ": "選取的檔案繞 Z 軸 +90°",
|
||||||
|
"Ui.Main.SavePNGScreenshot3DView2": "3D 視圖截圖存 PNG(2x 解析度)",
|
||||||
|
"Ui.Main.SaveViewPNG": "📷 視圖截圖 PNG…",
|
||||||
|
"Ui.Main.SectionAxis3PointsDefineAn": "剖切軸向(3點 = 在模型上點 3 點定義任意平面)",
|
||||||
|
"Ui.Main.SectionLabel": "✂ 剖面:",
|
||||||
|
"Ui.Main.SectionPosition0100EnterValue": "剖面位置 %(0–100,可直接輸入,離開欄位生效)",
|
||||||
|
"Ui.Main.SectionPositionAlongNormal": "剖面位置(沿法向 %)",
|
||||||
|
"Ui.Main.SectionView": "✂ 剖面檢視",
|
||||||
|
"Ui.Main.ShortestDistanceBetweenFacesMeshApproximation": "面到面最短距離(網格近似)",
|
||||||
|
"Ui.Main.ShortestFaceDistanceMeshApproximationShortcut": "面到面最短距離(網格近似)— 快速鍵 M",
|
||||||
|
"Ui.Main.ShowAll": "☀ 全部顯示",
|
||||||
|
"Ui.Main.ShowHideIncludingChildNodes": "顯示 / 隱藏(含子節點)",
|
||||||
|
"Ui.Main.ShowOnlyThisNode": "👁 只顯示此節點",
|
||||||
|
"Ui.Main.ShowOutlineEdgesManyEdgesCan": "顯示輪廓邊線(邊線量大時轉動視角會變慢)",
|
||||||
|
"Ui.Main.ShowSectionControlsAxisPositionDirection": "開啟後下方出現剖面參數列(軸向/位置/反向)",
|
||||||
|
"Ui.Main.ShowXYZArrowsRotationRingsSelected": "對樹面板選取的檔案顯示 XYZ 箭頭與旋轉環,每次放開即定位",
|
||||||
|
"Ui.Main.StraightCurvedCircularEdgeLengthShortcut": "邊長(直線/曲線/圓弧)— 快速鍵 E",
|
||||||
|
"Ui.Main.SwitchDisplayUnitsBetweenMmInches": "切換顯示單位 mm ⇄ inch(既有量測即時換算)",
|
||||||
|
"Ui.Main.SwitchRetainedSide": "切換保留側",
|
||||||
|
"Ui.Main.ThreePointAlignmentPickThreePoints": "三點對齊(旋轉+平移):先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)",
|
||||||
|
"Ui.Main.ThreePointAlignmentRotateTranslate": "🎯 三點對齊(旋轉+平移)",
|
||||||
|
"Ui.Main.ToggleSectionViewItsControls": "開啟/關閉剖面檢視(開啟後下方出現參數列)",
|
||||||
|
"Ui.Main.ToolbarExportStepTooltip": "把可見檔案(目前對齊位置)匯出成新 STEP 檔 — 對齊插合結果可交接 CAD;不動原始檔",
|
||||||
|
"Ui.Main.ToolbarExportStlTooltip": "把可見檔案(目前位置)匯出成 STL 網格檔 — 先設定範圍/格式/單位/精度,確認再匯出",
|
||||||
|
"Ui.Main.ToolbarImport": "📂 匯入",
|
||||||
|
"Ui.Main.ToolbarSection": "✂ 剖面",
|
||||||
|
"Ui.Main.ToolsL": "工具(_L)",
|
||||||
|
"Ui.Main.Top": "上",
|
||||||
|
"Ui.Main.TopView": "上視圖",
|
||||||
|
"Ui.Main.TopViewLookDownFromZ": "上視圖(+Z 往下看)",
|
||||||
|
"Ui.Main.TransformT": "變換(_T)",
|
||||||
|
"Ui.Main.TwoPointAlignmentPickPointMoving": "兩點對齊(平移零件):先點要移動零件上的點,再點目標點",
|
||||||
|
"Ui.Main.TwoPointAlignmentTranslate": "🎯 兩點對齊(平移)",
|
||||||
|
"Ui.Main.TwoPointDistance": "📏 兩點距離",
|
||||||
|
"Ui.Main.ViewV": "視圖(_V)",
|
||||||
|
"Ui.Main.WindowTitle": "STPViewer — STP/STEP 3D 檢視器",
|
||||||
|
"Ui.Main.ZoomFit": "🔍 縮放至全部",
|
||||||
|
"Ui.Main.ZoomThisNode": "縮放至此節點",
|
||||||
|
"Ui.StlExport.Cancel": "取消",
|
||||||
|
"Ui.StlExport.CustomScaleTooltip": "座標乘上此係數(例:0.001 = mm→m)",
|
||||||
|
"Ui.StlExport.EstimatedSize": "預估 {0}",
|
||||||
|
"Ui.StlExport.EstimatedSizeFine": "以目前網格預估 {0}(實際依重新三角化結果)",
|
||||||
|
"Ui.StlExport.Export": "匯出…",
|
||||||
|
"Ui.StlExport.Format": "格式",
|
||||||
|
"Ui.StlExport.FormatAscii": "ASCII(文字可讀,檔案約 4 倍大)",
|
||||||
|
"Ui.StlExport.FormatBinary": "Binary(建議,檔案小)",
|
||||||
|
"Ui.StlExport.MeshQuality": "網格精度",
|
||||||
|
"Ui.StlExport.QualityCurrent": "目前顯示網格(快,與畫面一致)",
|
||||||
|
"Ui.StlExport.QualityFine": "精細 — 重新三角化,曲面更平滑(慢、檔案大)",
|
||||||
|
"Ui.StlExport.QualityTooltip": "精細會由 B-rep 重新三角化(僅 STEP 來源;STL/DXF 維持現有網格),大組件需要時間",
|
||||||
|
"Ui.StlExport.Scope": "匯出範圍",
|
||||||
|
"Ui.StlExport.ScopeMerge": "全部可見檔案 → 合併成單一 STL",
|
||||||
|
"Ui.StlExport.ScopePerFile": "每個可見檔案 → 各一個 STL",
|
||||||
|
"Ui.StlExport.ScopeTooltip": "STL 無裝配結構;合併 = 所有可見零件進同一檔",
|
||||||
|
"Ui.StlExport.Summary": "{0} 個可見檔案 · {1} 三角形 · {2}",
|
||||||
|
"Ui.StlExport.Title": "匯出 STL — 參數設定",
|
||||||
|
"Ui.StlExport.UnitCustom": "自訂縮放係數…",
|
||||||
|
"Ui.StlExport.UnitInch": "inch(座標 ÷ 25.4)",
|
||||||
|
"Ui.StlExport.UnitMillimeter": "mm(1:1,預設)",
|
||||||
|
"Ui.StlExport.UnitScale": "單位縮放",
|
||||||
|
"Ui.StlExport.UnitTooltip": "STL 檔本身無單位;內部幾何為 mm,選 inch 會把座標 ÷25.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ internal static class AlignTest
|
|||||||
Point3D q1 = truth.Transform(p1), q2 = truth.Transform(p2), q3 = truth.Transform(p3);
|
Point3D q1 = truth.Transform(p1), q2 = truth.Transform(p2), q3 = truth.Transform(p3);
|
||||||
|
|
||||||
bool ok = RigidAlign.TryRigidTransform(p1, p2, p3, q1, q2, q3, out Matrix3D m);
|
bool ok = RigidAlign.TryRigidTransform(p1, p2, p3, q1, q2, q3, out Matrix3D m);
|
||||||
failures += Check("三點求解成功", ok, true);
|
failures += Check("three-point transform solved", ok, true);
|
||||||
|
|
||||||
// 三個對應點要精確貼合
|
// 三個對應點要精確貼合
|
||||||
failures += CheckPoint("p1→q1", m.Transform(p1), q1);
|
failures += CheckPoint("p1→q1", m.Transform(p1), q1);
|
||||||
@@ -31,7 +31,7 @@ internal static class AlignTest
|
|||||||
|
|
||||||
// 剛體變換唯一性:任意第 4 點也應該跟著正確變換
|
// 剛體變換唯一性:任意第 4 點也應該跟著正確變換
|
||||||
var r = new Point3D(-4, 12, 8);
|
var r = new Point3D(-4, 12, 8);
|
||||||
failures += CheckPoint("任意第4點", m.Transform(r), truth.Transform(r));
|
failures += CheckPoint("fourth point", m.Transform(r), truth.Transform(r));
|
||||||
|
|
||||||
// ── (2) ToModOp:CADability ModOp 與 WPF Matrix3D 對同一點要算出同樣結果 ──
|
// ── (2) ToModOp:CADability ModOp 與 WPF Matrix3D 對同一點要算出同樣結果 ──
|
||||||
CADability.ModOp op = RigidAlign.ToModOp(m);
|
CADability.ModOp op = RigidAlign.ToModOp(m);
|
||||||
@@ -42,28 +42,28 @@ internal static class AlignTest
|
|||||||
bool same = Dist(new Point3D(gp.x, gp.y, gp.z), wpf) < 1e-9;
|
bool same = Dist(new Point3D(gp.x, gp.y, gp.z), wpf) < 1e-9;
|
||||||
if (!same)
|
if (!same)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine($" FAIL: ModOp/Matrix3D 不一致 @ ({p.X},{p.Y},{p.Z})");
|
System.Console.WriteLine($" FAIL: ModOp/Matrix3D mismatch at ({p.X},{p.Y},{p.Z})");
|
||||||
failures++;
|
failures++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
System.Console.WriteLine($" {(failures == 0 ? "PASS" : "----")}: ModOp ↔ Matrix3D 一致(5 個測試點)");
|
System.Console.WriteLine($" {(failures == 0 ? "PASS" : "----")}: ModOp/Matrix3D agree at five points");
|
||||||
|
|
||||||
// ── (3) 共線拒絕 ──
|
// ── (3) 共線拒絕 ──
|
||||||
bool collinearRejected = !RigidAlign.TryRigidTransform(
|
bool collinearRejected = !RigidAlign.TryRigidTransform(
|
||||||
new Point3D(0, 0, 0), new Point3D(1, 0, 0), new Point3D(2, 0, 0), // 共線
|
new Point3D(0, 0, 0), new Point3D(1, 0, 0), new Point3D(2, 0, 0), // Collinear.
|
||||||
q1, q2, q3, out _);
|
q1, q2, q3, out _);
|
||||||
failures += Check("來源共線 → 拒絕", collinearRejected, true);
|
failures += Check("collinear source rejected", collinearRejected, true);
|
||||||
failures += Check("Collinear 判定", RigidAlign.Collinear(
|
failures += Check("collinearity detection", RigidAlign.Collinear(
|
||||||
new Point3D(0, 0, 0), new Point3D(1, 1, 1), new Point3D(3, 3, 3)), true);
|
new Point3D(0, 0, 0), new Point3D(1, 1, 1), new Point3D(3, 3, 3)), true);
|
||||||
|
|
||||||
System.Console.WriteLine(failures == 0 ? "AlignTest: 全部通過" : $"AlignTest: {failures} 項失敗");
|
System.Console.WriteLine(failures == 0 ? "AlignTest: all passed" : $"AlignTest: {failures} failed");
|
||||||
return failures;
|
return failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Check(string name, bool actual, bool expected)
|
private static int Check(string name, bool actual, bool expected)
|
||||||
{
|
{
|
||||||
bool ok = actual == expected;
|
bool ok = actual == expected;
|
||||||
System.Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name}({actual})");
|
System.Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name} ({actual})");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ internal static class AlignTest
|
|||||||
{
|
{
|
||||||
double d = Dist(actual, expected);
|
double d = Dist(actual, expected);
|
||||||
bool ok = d < 1e-9;
|
bool ok = d < 1e-9;
|
||||||
System.Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name} 誤差 {d:E2}");
|
System.Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name} error {d:E2}");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,36 +24,36 @@ internal static class ClipTest
|
|||||||
|
|
||||||
// case 1: 平面 x=20,全保留
|
// case 1: 平面 x=20,全保留
|
||||||
var m1 = SectionService.ClipMesh(mesh, new Point3D(20, 0, 0), new Vector3D(1, 0, 0));
|
var m1 = SectionService.ClipMesh(mesh, new Point3D(20, 0, 0), new Vector3D(1, 0, 0));
|
||||||
failures += Check("全保留", Area(m1), 50.0);
|
failures += Check("fully retained", Area(m1), 50.0);
|
||||||
|
|
||||||
// case 2: 平面 x=-1,全裁掉
|
// case 2: 平面 x=-1,全裁掉
|
||||||
var m2 = SectionService.ClipMesh(mesh, new Point3D(-1, 0, 0), new Vector3D(1, 0, 0));
|
var m2 = SectionService.ClipMesh(mesh, new Point3D(-1, 0, 0), new Vector3D(1, 0, 0));
|
||||||
failures += Check("全裁掉", Area(m2), 0.0);
|
failures += Check("fully removed", Area(m2), 0.0);
|
||||||
|
|
||||||
// case 3: 平面 x=5 → 保留 x≤5:梯形面積 = 50 - 右側小三角形(底5高5)/2 = 50 - 12.5 = 37.5
|
// case 3: 平面 x=5 → 保留 x≤5:梯形面積 = 50 - 右側小三角形(底5高5)/2 = 50 - 12.5 = 37.5
|
||||||
var m3 = SectionService.ClipMesh(mesh, new Point3D(5, 0, 0), new Vector3D(1, 0, 0));
|
var m3 = SectionService.ClipMesh(mesh, new Point3D(5, 0, 0), new Vector3D(1, 0, 0));
|
||||||
failures += Check("半裁(x≤5)", Area(m3), 37.5);
|
failures += Check("half clipped (x<=5)", Area(m3), 37.5);
|
||||||
foreach (Point3D p in m3.Positions)
|
foreach (Point3D p in m3.Positions)
|
||||||
if (p.X > 5 + 1e-9) { Console.WriteLine($" FAIL: 頂點 x={p.X} 超出保留側"); failures++; }
|
if (p.X > 5 + 1e-9) { Console.WriteLine($" FAIL: vertex x={p.X} is outside retained side"); failures++; }
|
||||||
|
|
||||||
// case 4: 反向(保留 x≥5)→ 12.5
|
// case 4: 反向(保留 x≥5)→ 12.5
|
||||||
var m4 = SectionService.ClipMesh(mesh, new Point3D(5, 0, 0), new Vector3D(-1, 0, 0));
|
var m4 = SectionService.ClipMesh(mesh, new Point3D(5, 0, 0), new Vector3D(-1, 0, 0));
|
||||||
failures += Check("半裁(x≥5)", Area(m4), 12.5);
|
failures += Check("half clipped (x>=5)", Area(m4), 12.5);
|
||||||
|
|
||||||
// case 5: 線段裁切
|
// case 5: 線段裁切
|
||||||
var segs = new Point3DCollection { new(0, 0, 0), new(10, 0, 0) };
|
var segs = new Point3DCollection { new(0, 0, 0), new(10, 0, 0) };
|
||||||
Point3DCollection s1 = SectionService.ClipSegments(segs, new Point3D(4, 0, 0), new Vector3D(1, 0, 0));
|
Point3DCollection s1 = SectionService.ClipSegments(segs, new Point3D(4, 0, 0), new Vector3D(1, 0, 0));
|
||||||
double segLen = s1.Count == 2 ? (s1[1] - s1[0]).Length : -1;
|
double segLen = s1.Count == 2 ? (s1[1] - s1[0]).Length : -1;
|
||||||
failures += Check("線段裁切長度", segLen, 4.0);
|
failures += Check("clipped segment length", segLen, 4.0);
|
||||||
|
|
||||||
Console.WriteLine(failures == 0 ? "ClipTest: 全部通過" : $"ClipTest: {failures} 項失敗");
|
Console.WriteLine(failures == 0 ? "ClipTest: all passed" : $"ClipTest: {failures} failed");
|
||||||
return failures;
|
return failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Check(string name, double actual, double expected)
|
private static int Check(string name, double actual, double expected)
|
||||||
{
|
{
|
||||||
bool ok = Math.Abs(actual - expected) < 1e-9;
|
bool ok = Math.Abs(actual - expected) < 1e-9;
|
||||||
Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name} = {actual}(期望 {expected})");
|
Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name} = {actual} (expected {expected})");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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($" Source Solid/Shell objects = {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($" Written ({fi.Length:N0} bytes)");
|
||||||
|
if (fi.Length < 100)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ExportTest: FAIL (output is nearly empty)");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportedFileData back = new StepImportService().Import(outPath);
|
||||||
|
Console.WriteLine($" Reimported 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 (solid counts match)"
|
||||||
|
: $"ExportTest: FAIL (source solids {data.Root.SolidCount} vs reimported {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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,33 +15,33 @@ internal static class InterferenceTest
|
|||||||
MeshGeometry3D a = Tetra(new Vector3D(0, 0, 0));
|
MeshGeometry3D a = Tetra(new Vector3D(0, 0, 0));
|
||||||
MeshGeometry3D b = Tetra(new Vector3D(5, 0, 0));
|
MeshGeometry3D b = Tetra(new Vector3D(5, 0, 0));
|
||||||
InterferenceResult r1 = InterferenceService.Check(new[] { a }, new[] { b });
|
InterferenceResult r1 = InterferenceService.Check(new[] { a }, new[] { b });
|
||||||
failures += Check("互穿 → 相交", r1.Intersects, true);
|
failures += Check("overlap intersects", r1.Intersects, true);
|
||||||
failures += Check("互穿 → 有交線段", r1.Segments.Count > 0, true);
|
failures += Check("overlap has intersection segments", r1.Segments.Count > 0, true);
|
||||||
|
|
||||||
// 分離(B 平移 +30,gap 應 ≈ 30 - 10 = 20)
|
// 分離(B 平移 +30,gap 應 ≈ 30 - 10 = 20)
|
||||||
MeshGeometry3D c = Tetra(new Vector3D(30, 0, 0));
|
MeshGeometry3D c = Tetra(new Vector3D(30, 0, 0));
|
||||||
InterferenceResult r2 = InterferenceService.Check(new[] { a }, new[] { c });
|
InterferenceResult r2 = InterferenceService.Check(new[] { a }, new[] { c });
|
||||||
failures += Check("分離 → 不相交", r2.Intersects, false);
|
failures += Check("separated meshes do not intersect", r2.Intersects, false);
|
||||||
bool gapOk = Math.Abs(r2.GapDistance - 20.0) < 0.01;
|
bool gapOk = Math.Abs(r2.GapDistance - 20.0) < 0.01;
|
||||||
Console.WriteLine($" {(gapOk ? "PASS" : "FAIL")}: 分離 gap = {r2.GapDistance:F4}(期望 ≈ 20)");
|
Console.WriteLine($" {(gapOk ? "PASS" : "FAIL")}: separated gap = {r2.GapDistance:F4} (expected ~20)");
|
||||||
if (!gapOk) failures++;
|
if (!gapOk) failures++;
|
||||||
|
|
||||||
// 貼合(B 平移 +10,頂點剛好接觸 → 不算穿透、gap ≈ 0)
|
// 貼合(B 平移 +10,頂點剛好接觸 → 不算穿透、gap ≈ 0)
|
||||||
MeshGeometry3D d = Tetra(new Vector3D(10, 0, 0));
|
MeshGeometry3D d = Tetra(new Vector3D(10, 0, 0));
|
||||||
InterferenceResult r3 = InterferenceService.Check(new[] { a }, new[] { d });
|
InterferenceResult r3 = InterferenceService.Check(new[] { a }, new[] { d });
|
||||||
failures += Check("貼合 → 不算穿透", r3.Intersects, false);
|
failures += Check("touching is not penetration", r3.Intersects, false);
|
||||||
bool touchOk = r3.GapDistance < 0.01;
|
bool touchOk = r3.GapDistance < 0.01;
|
||||||
Console.WriteLine($" {(touchOk ? "PASS" : "FAIL")}: 貼合 gap = {r3.GapDistance:F6}(期望 ≈ 0)");
|
Console.WriteLine($" {(touchOk ? "PASS" : "FAIL")}: touching gap = {r3.GapDistance:F6} (expected ~0)");
|
||||||
if (!touchOk) failures++;
|
if (!touchOk) failures++;
|
||||||
|
|
||||||
Console.WriteLine(failures == 0 ? "InterferenceTest: 全部通過" : $"InterferenceTest: {failures} 項失敗");
|
Console.WriteLine(failures == 0 ? "InterferenceTest: all passed" : $"InterferenceTest: {failures} failed");
|
||||||
return failures;
|
return failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Check(string name, bool actual, bool expected)
|
private static int Check(string name, bool actual, bool expected)
|
||||||
{
|
{
|
||||||
bool ok = actual == expected;
|
bool ok = actual == expected;
|
||||||
Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name}({actual})");
|
Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name} ({actual})");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using CADability.GeoObject;
|
||||||
|
using STPViewer.Models;
|
||||||
|
using STPViewer.Services;
|
||||||
|
using STPViewer.ViewModels;
|
||||||
|
using IOPath = System.IO.Path;
|
||||||
|
|
||||||
|
namespace SmokeTest;
|
||||||
|
|
||||||
|
internal static class LocalizationTest
|
||||||
|
{
|
||||||
|
private static readonly Regex FormatIndex = new(@"(?<!\{)\{(\d+)(?:[^{}]*)\}(?!\})", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static int Run()
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
Exception? error = null;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try { failures = RunCore(); }
|
||||||
|
catch (Exception ex) { error = ex; }
|
||||||
|
});
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
thread.Join();
|
||||||
|
if (error is not null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"LocalizationTest: {error}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunCore()
|
||||||
|
{
|
||||||
|
int failures = CheckTemporaryPacks() + CheckBundledPacks() + CheckViewModel() + CheckSettings();
|
||||||
|
Console.WriteLine(failures == 0 ? "LocalizationTest: all passed" : $"LocalizationTest: {failures} failed");
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CheckTemporaryPacks()
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
string directory = IOPath.Combine(IOPath.GetTempPath(), "stpviewer_i18n_" + Guid.NewGuid().ToString("N"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
failures += Check("missing directory loads without languages", new LocalizationService(directory).Languages.Count == 0);
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
failures += Check("empty directory loads without languages", new LocalizationService(directory).Languages.Count == 0);
|
||||||
|
WritePack(directory, "en", "English", new()
|
||||||
|
{
|
||||||
|
["Ui.Sample"] = "View", ["Ui.Fallback"] = "Fallback {0}",
|
||||||
|
["Ui.BadBrace"] = "English {0}", ["Ui.BadIndex"] = "English {0}",
|
||||||
|
["Ui.BothBad"] = "English {"
|
||||||
|
});
|
||||||
|
WritePack(directory, "zh_cn", "Simplified", new()
|
||||||
|
{
|
||||||
|
["Ui.Sample"] = "CN_VIEW", ["Ui.BadBrace"] = "Chinese {",
|
||||||
|
["Ui.BadIndex"] = "Chinese {1}", ["Ui.BothBad"] = "Chinese {"
|
||||||
|
});
|
||||||
|
WritePack(directory, "zh_tw", "Traditional", new() { ["Ui.Sample"] = "TW_VIEW" });
|
||||||
|
WritePack(directory, "pirate_dialect", "Pirate", new() { ["Ui.Sample"] = "ARR_VIEW" });
|
||||||
|
File.WriteAllText(IOPath.Combine(directory, "broken.json"), "{invalid json");
|
||||||
|
File.WriteAllText(IOPath.Combine(directory, "no_key.json"), "{\"data\":{\"Ui.Sample\":\"unused\"}}");
|
||||||
|
File.WriteAllText(IOPath.Combine(directory, "no_data.json"), "{\"key\":\"Missing data\"}");
|
||||||
|
File.WriteAllText(IOPath.Combine(directory, "non_string.json"), "{\"key\":\"Invalid\",\"data\":{\"Ui.Sample\":42}}");
|
||||||
|
|
||||||
|
CultureInfo oldUi = CultureInfo.CurrentUICulture;
|
||||||
|
LocalizationService service;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
|
||||||
|
service = new LocalizationService(directory);
|
||||||
|
}
|
||||||
|
finally { CultureInfo.CurrentUICulture = oldUi; }
|
||||||
|
|
||||||
|
failures += Check("valid packs isolated from malformed files", service.Languages.Count == 4 &&
|
||||||
|
service.Languages.Select(l => l.Id).Order().SequenceEqual(new[] { "en", "pirate_dialect", "zh_cn", "zh_tw" }));
|
||||||
|
failures += Check("display names and data loaded", service.Languages.Any(l =>
|
||||||
|
l.Id == "pirate_dialect" && l.Key == "Pirate" && l.Data["Ui.Sample"] == "ARR_VIEW"));
|
||||||
|
failures += Check("English selected from system culture", service.Language == "en");
|
||||||
|
failures += Check("legacy Chinese IDs resolved", service.ResolveLanguage("zh-CN") == "zh_cn" &&
|
||||||
|
service.ResolveLanguage("zh-TW") == "zh_tw");
|
||||||
|
failures += Check("Chinese script and region aliases resolved", service.ResolveLanguage("zh-Hans") == "zh_cn" &&
|
||||||
|
service.ResolveLanguage("zh-SG") == "zh_cn" && service.ResolveLanguage("zh-Hant") == "zh_tw" &&
|
||||||
|
service.ResolveLanguage("zh-HK") == "zh_tw" && service.ResolveLanguage("zh-MO") == "zh_tw");
|
||||||
|
failures += Check("custom language ID resolved", service.ResolveLanguage("pirate-dialect") == "pirate_dialect");
|
||||||
|
failures += Check("unknown language falls back", service.ResolveLanguage("unknown") == "en");
|
||||||
|
failures += Check("invalid custom ID falls back", service.ResolveLanguage("pirate_other") == "en");
|
||||||
|
|
||||||
|
CultureInfo beforeNull = CultureInfo.CurrentUICulture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("zh-HK");
|
||||||
|
failures += Check("null language uses system UI culture", service.ResolveLanguage(null) == "zh_tw");
|
||||||
|
}
|
||||||
|
finally { CultureInfo.CurrentUICulture = beforeNull; }
|
||||||
|
|
||||||
|
LanguageDefinition chinese = service.Languages.Single(l => l.Id == "zh_cn");
|
||||||
|
bool selectedNotified = false, indexerNotified = false;
|
||||||
|
chinese.PropertyChanged += (_, e) => selectedNotified |= e.PropertyName == nameof(LanguageDefinition.IsSelected);
|
||||||
|
((INotifyPropertyChanged)service).PropertyChanged += (_, e) => indexerNotified |= e.PropertyName == "Item[]";
|
||||||
|
service.SetLanguage("zh-CN");
|
||||||
|
failures += Check("selection and indexer notify", service.Language == "zh_cn" &&
|
||||||
|
chinese.IsSelected && selectedNotified && indexerNotified);
|
||||||
|
failures += Check("selected data and fallback", service["Ui.Sample"] == "CN_VIEW" &&
|
||||||
|
service["Ui.Fallback"] == "Fallback {0}" && service["Ui.Missing"] == "Ui.Missing");
|
||||||
|
failures += Check("malformed brace uses English format", service.FormatText("Ui.BadBrace", "value") == "English value");
|
||||||
|
failures += Check("missing format argument uses English format", service.FormatText("Ui.BadIndex", "value") == "English value");
|
||||||
|
failures += Check("invalid English format returns key", service.FormatText("Ui.BothBad", "value") == "Ui.BothBad");
|
||||||
|
service.SetLanguage("pirate_dialect");
|
||||||
|
failures += Check("custom language selected", service.Language == "pirate_dialect" &&
|
||||||
|
service["Ui.Sample"] == "ARR_VIEW");
|
||||||
|
|
||||||
|
WritePack(directory, "new_language", "New language", new() { ["Ui.Sample"] = "NEW_VIEW" });
|
||||||
|
WritePack(directory, "en", "English", new() { ["Ui.Sample"] = "Updated view", ["Ui.Fallback"] = "Fallback {0}" });
|
||||||
|
failures += Check("existing instance does not hot reload", service.Languages.Count == 4 &&
|
||||||
|
service.Languages.Single(l => l.Id == "en").Data["Ui.Sample"] == "View");
|
||||||
|
var reloaded = new LocalizationService(directory);
|
||||||
|
failures += Check("new instance rescans files and values", reloaded.Languages.Count == 5 &&
|
||||||
|
reloaded.Languages.Single(l => l.Id == "en").Data["Ui.Sample"] == "Updated view");
|
||||||
|
|
||||||
|
File.Delete(IOPath.Combine(directory, "zh_tw.json"));
|
||||||
|
var withoutTraditional = new LocalizationService(directory);
|
||||||
|
failures += Check("missing language not listed", withoutTraditional.Languages.All(l => l.Id != "zh_tw"));
|
||||||
|
failures += Check("missing legacy ID falls back", withoutTraditional.ResolveLanguage("zh-TW") == "en");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
|
||||||
|
}
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WritePack(string directory, string id, string name, Dictionary<string, string> data) =>
|
||||||
|
File.WriteAllText(IOPath.Combine(directory, id + ".json"), JsonSerializer.Serialize(new { key = name, data }));
|
||||||
|
|
||||||
|
private static int CheckBundledPacks()
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
LocalizationService service = LocalizationService.Instance;
|
||||||
|
failures += Check("bundled languages available", new[] { "en", "zh_cn", "zh_tw" }
|
||||||
|
.All(id => service.Languages.Any(l => l.Id == id)));
|
||||||
|
LanguageDefinition? english = service.Languages.FirstOrDefault(l => l.Id == "en");
|
||||||
|
if (english is null) return failures;
|
||||||
|
failures += Check("bundled English data nonempty", english.Data.Count > 0 &&
|
||||||
|
english.Data.Values.All(v => !string.IsNullOrWhiteSpace(v)));
|
||||||
|
int formatFailures = 0;
|
||||||
|
foreach (LanguageDefinition language in service.Languages)
|
||||||
|
foreach ((string key, string value) in language.Data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) formatFailures++;
|
||||||
|
int[] indices = Indices(value);
|
||||||
|
if (english.Data.TryGetValue(key, out string? fallback) && !Indices(fallback).SequenceEqual(indices))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" FAIL: {language.Id} format arguments differ for {key}");
|
||||||
|
formatFailures++;
|
||||||
|
}
|
||||||
|
object[] args = Enumerable.Range(0, indices.DefaultIfEmpty(-1).Max() + 1)
|
||||||
|
.Select(i => (object)i).ToArray();
|
||||||
|
try { _ = string.Format(CultureInfo.InvariantCulture, value, args); }
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" FAIL: {language.Id} format syntax for {key}");
|
||||||
|
formatFailures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failures += Check("bundled data and format strings valid", formatFailures == 0);
|
||||||
|
failures += Check("missing key returns key", service["__missing_localization_key__"] == "__missing_localization_key__");
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CheckViewModel()
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
LocalizationService service = LocalizationService.Instance;
|
||||||
|
string originalLanguage = service.Language;
|
||||||
|
CultureInfo numericCulture = CultureInfo.CurrentCulture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
service.SetLanguage("en");
|
||||||
|
var vm = new MainViewModel();
|
||||||
|
vm.InitializeLocalization();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string oldLabel = vm.QuickBar["Import"].Label;
|
||||||
|
bool labelChanged = false;
|
||||||
|
vm.QuickBar["Import"].PropertyChanged += (_, e) =>
|
||||||
|
labelChanged |= e.PropertyName == nameof(QuickBarItemViewModel.Label);
|
||||||
|
|
||||||
|
MeasurementResult result = new MeasurementService().MeasureDistance(
|
||||||
|
new Point3D(0, 0, 0), new Point3D(25.4, 0, 0), "D1", 0.2);
|
||||||
|
vm.Measurements.Add(result);
|
||||||
|
vm.UseInch = true;
|
||||||
|
string oneInch = 1d.ToString("F4", numericCulture) + " in";
|
||||||
|
string beforeTitle = result.Title, beforeDetail = result.Detail;
|
||||||
|
string beforeLabel = result.DynamicLabels.Single().Label.Text;
|
||||||
|
var (distanceLabel, distanceTextFor) = result.DynamicLabels.Single();
|
||||||
|
int labelRefreshes = 0;
|
||||||
|
result.DynamicLabels[0] = (distanceLabel, u =>
|
||||||
|
{
|
||||||
|
labelRefreshes++;
|
||||||
|
return distanceTextFor(u);
|
||||||
|
});
|
||||||
|
bool titleChanged = false, detailChanged = false;
|
||||||
|
result.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
titleChanged |= e.PropertyName == nameof(MeasurementResult.Title);
|
||||||
|
detailChanged |= e.PropertyName == nameof(MeasurementResult.Detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.ChangeLanguageCommand.Execute("zh_tw");
|
||||||
|
failures += Check("VM selects language", service.Language == "zh_tw" &&
|
||||||
|
service.Languages.Single(l => l.Id == "zh_tw").IsSelected);
|
||||||
|
failures += Check("existing distance detail translated", beforeDetail != result.Detail &&
|
||||||
|
titleChanged && detailChanged && result.Detail.Contains(oneInch, StringComparison.Ordinal));
|
||||||
|
failures += Check("distance and number culture preserved", vm.UseInch &&
|
||||||
|
CultureInfo.CurrentCulture.Equals(numericCulture) && beforeTitle == result.Title &&
|
||||||
|
result.Title.Contains(oneInch, StringComparison.Ordinal));
|
||||||
|
failures += Check("3D distance label refreshed", labelRefreshes == 1 &&
|
||||||
|
beforeLabel == oneInch && result.DynamicLabels.Single().Label.Text == beforeLabel);
|
||||||
|
failures += Check("quick bar label refreshed", labelChanged && vm.QuickBar["Import"].Label != oldLabel);
|
||||||
|
|
||||||
|
LanguageDefinition selected = service.Languages.Single(l => l.Id == "zh_tw");
|
||||||
|
bool reselected = false;
|
||||||
|
selected.PropertyChanged += (_, e) => reselected |= e.PropertyName == nameof(LanguageDefinition.IsSelected);
|
||||||
|
vm.ChangeLanguageCommand.Execute("zh_tw");
|
||||||
|
failures += Check("reselecting current language restores check", reselected && selected.IsSelected);
|
||||||
|
}
|
||||||
|
finally { vm.DisposeLocalization(); }
|
||||||
|
failures += CheckImportedNames(service);
|
||||||
|
}
|
||||||
|
finally { service.SetLanguage(originalLanguage); }
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CheckImportedNames(LocalizationService service)
|
||||||
|
{
|
||||||
|
int failures = 0;
|
||||||
|
MethodInfo blockNode = typeof(StepImportService).GetMethod("BlockNode", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
|
var importer = new StepImportService();
|
||||||
|
service.SetLanguage("en");
|
||||||
|
|
||||||
|
ImportedNode Import(Block block) => (ImportedNode)blockNode.Invoke(importer, [block])!;
|
||||||
|
var namedSolidBlock = Block.Construct();
|
||||||
|
namedSolidBlock.Name = "Solid";
|
||||||
|
namedSolidBlock.Add(Solid.Construct());
|
||||||
|
ImportedNode namedSolid = Import(namedSolidBlock);
|
||||||
|
var namedShellBlock = Block.Construct();
|
||||||
|
namedShellBlock.Name = "Shell";
|
||||||
|
namedShellBlock.Add(Shell.Construct());
|
||||||
|
ImportedNode namedShell = Import(namedShellBlock);
|
||||||
|
var defaultSolidBlock = Block.Construct();
|
||||||
|
defaultSolidBlock.Add(Solid.Construct());
|
||||||
|
ImportedNode defaultSolid = Import(defaultSolidBlock);
|
||||||
|
var defaultShellBlock = Block.Construct();
|
||||||
|
defaultShellBlock.Add(Shell.Construct());
|
||||||
|
ImportedNode defaultShellLeaf = Import(defaultShellBlock).Children.Single();
|
||||||
|
var collisionBlock = Block.Construct();
|
||||||
|
var realComponentShell = Shell.Construct();
|
||||||
|
realComponentShell.Name = "Component";
|
||||||
|
collisionBlock.Add(realComponentShell);
|
||||||
|
ImportedNode collision = Import(collisionBlock);
|
||||||
|
|
||||||
|
ModelNodeViewModel ViewModel(ImportedNode node) => new(0)
|
||||||
|
{
|
||||||
|
Name = node.Name,
|
||||||
|
NameKey = node.NameKey,
|
||||||
|
NameNumber = node.NameNumber
|
||||||
|
};
|
||||||
|
var namedSolidVm = ViewModel(namedSolid);
|
||||||
|
var namedShellVm = ViewModel(namedShell);
|
||||||
|
var defaultSolidVm = ViewModel(defaultSolid);
|
||||||
|
var defaultShellVm = ViewModel(defaultShellLeaf);
|
||||||
|
var collisionVm = ViewModel(collision);
|
||||||
|
service.SetLanguage("zh_cn");
|
||||||
|
failures += Check("explicit solid name retained", namedSolid.NameKey is null && namedSolidVm.Name == "Solid");
|
||||||
|
failures += Check("real block name retained", namedShell.NameKey is null && namedShellVm.Name == "Shell");
|
||||||
|
failures += Check("default solid name translated", defaultSolid.NameKey == "Runtime.SolidName" &&
|
||||||
|
defaultSolidVm.Name == service["Runtime.SolidName"]);
|
||||||
|
failures += Check("default shell name translated", defaultShellLeaf.NameKey == "Runtime.ShellName" &&
|
||||||
|
defaultShellVm.Name == service["Runtime.ShellName"]);
|
||||||
|
failures += Check("real child name wins collision", collision.NameKey is null && collisionVm.Name == "Component");
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CheckSettings()
|
||||||
|
{
|
||||||
|
var settings = new AppSettings
|
||||||
|
{
|
||||||
|
Language = "zh-CN", WindowWidth = 1200, WindowHeight = 800, WindowLeft = 100, WindowTop = 100
|
||||||
|
};
|
||||||
|
AppSettings? restored = JsonSerializer.Deserialize<AppSettings>(JsonSerializer.Serialize(settings));
|
||||||
|
return Check("legacy ID round trips through settings JSON", restored?.Language == "zh-CN" &&
|
||||||
|
LocalizationService.Instance.ResolveLanguage(restored.Language) == "zh_cn");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] Indices(string format) => FormatIndex.Matches(format)
|
||||||
|
.Select(m => int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture))
|
||||||
|
.Distinct().Order().ToArray();
|
||||||
|
|
||||||
|
private static int Check(string name, bool ok)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name}");
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,6 @@ internal static class MakeDxf
|
|||||||
doc.Entities.Add(new Line(new Vector3(0, 0, 0), new Vector3(100, 50, 0)));
|
doc.Entities.Add(new Line(new Vector3(0, 0, 0), new Vector3(100, 50, 0)));
|
||||||
doc.Entities.Add(new Circle(new Vector3(50, 25, 0), 10));
|
doc.Entities.Add(new Circle(new Vector3(50, 25, 0), 10));
|
||||||
doc.Save(path);
|
doc.Save(path);
|
||||||
Console.WriteLine($"已產生 {path}");
|
Console.WriteLine($"Generated {path}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SmokeTest;
|
||||||
|
|
||||||
|
/// <summary>產生測試用 binary STL:一片 gridN×gridN 的網格,每格 2 個三角形(波浪起伏,避免全共面)。</summary>
|
||||||
|
internal static class MakeStl
|
||||||
|
{
|
||||||
|
public static void Run(string path, int triCount)
|
||||||
|
{
|
||||||
|
// 每格 2 三角,格數 = triCount/2;邊長 grid = ceil(sqrt(cells))
|
||||||
|
int cells = System.Math.Max(1, triCount / 2);
|
||||||
|
int grid = (int)System.Math.Ceiling(System.Math.Sqrt(cells));
|
||||||
|
int tris = grid * grid * 2;
|
||||||
|
|
||||||
|
using var fs = File.Create(path);
|
||||||
|
using var bw = new BinaryWriter(fs);
|
||||||
|
bw.Write(new byte[80]); // header
|
||||||
|
bw.Write((uint)tris); // triangle count
|
||||||
|
|
||||||
|
static void WriteTri(BinaryWriter w, float ax, float ay, float az,
|
||||||
|
float bx, float by, float bz, float cx, float cy, float cz)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3; i++) w.Write(0f); // normal (0,0,0)
|
||||||
|
w.Write(ax); w.Write(ay); w.Write(az);
|
||||||
|
w.Write(bx); w.Write(by); w.Write(bz);
|
||||||
|
w.Write(cx); w.Write(cy); w.Write(cz);
|
||||||
|
w.Write((ushort)0); // attribute byte count
|
||||||
|
}
|
||||||
|
|
||||||
|
float Z(int x, int y) => (float)(System.Math.Sin(x * 0.1) * System.Math.Cos(y * 0.1) * 5.0);
|
||||||
|
for (int y = 0; y < grid; y++)
|
||||||
|
for (int x = 0; x < grid; x++)
|
||||||
|
{
|
||||||
|
float x0 = x, x1 = x + 1, y0 = y, y1 = y + 1;
|
||||||
|
WriteTri(bw, x0, y0, Z(x, y), x1, y0, Z(x + 1, y), x1, y1, Z(x + 1, y + 1));
|
||||||
|
WriteTri(bw, x0, y0, Z(x, y), x1, y1, Z(x + 1, y + 1), x0, y1, Z(x, y + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Wrote {tris:N0} triangles to {path} ({new FileInfo(path).Length:N0} bytes)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ Console.OutputEncoding = System.Text.Encoding.UTF8;
|
|||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("用法: SmokeTest [--tree] <file.stp|.stl|.dxf> [...]");
|
Console.WriteLine("Usage: SmokeTest [--tree] <file.stp|.stl|.dxf> [...]");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@ if (args[0] == "--make-dxf")
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args[0] == "--make-stl")
|
||||||
|
{
|
||||||
|
SmokeTest.MakeStl.Run(args[1], args.Length > 2 ? int.Parse(args[2]) : 1_500_000);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (args[0] == "--clip-test")
|
if (args[0] == "--clip-test")
|
||||||
return SmokeTest.ClipTest.Run() == 0 ? 0 : 2;
|
return SmokeTest.ClipTest.Run() == 0 ? 0 : 2;
|
||||||
|
|
||||||
@@ -32,10 +38,19 @@ if (args[0] == "--interference-test")
|
|||||||
if (args[0] == "--align-test")
|
if (args[0] == "--align-test")
|
||||||
return SmokeTest.AlignTest.Run() == 0 ? 0 : 2;
|
return SmokeTest.AlignTest.Run() == 0 ? 0 : 2;
|
||||||
|
|
||||||
|
if (args[0] == "--localization-test")
|
||||||
|
return SmokeTest.LocalizationTest.Run() == 0 ? 0 : 2;
|
||||||
|
|
||||||
if (args[0] == "--bbox")
|
if (args[0] == "--bbox")
|
||||||
return SmokeTest.BBoxDump.Run(args[1]);
|
return SmokeTest.BBoxDump.Run(args[1]);
|
||||||
|
|
||||||
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
|
if (args[0] == "--export-test")
|
||||||
|
return SmokeTest.ExportTest.Run(args[1], args[2]) == 0 ? 0 : 2;
|
||||||
|
|
||||||
|
if (args[0] == "--stl-export-test")
|
||||||
|
return SmokeTest.StlExportTest.Run(args.Length > 1 ? args[1] : null) == 0 ? 0 : 2;
|
||||||
|
|
||||||
|
var service = new StepImportService { Progress = s => Console.WriteLine($" [stage] {s}") };
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
|
|
||||||
foreach (string path in args)
|
foreach (string path in args)
|
||||||
@@ -52,7 +67,7 @@ foreach (string path in args)
|
|||||||
Console.WriteLine($" Triangles : {r.TriangleCount:N0}");
|
Console.WriteLine($" Triangles : {r.TriangleCount:N0}");
|
||||||
Console.WriteLine($" Bounds : {r.Bounds.SizeX:F2} x {r.Bounds.SizeY:F2} x {r.Bounds.SizeZ:F2} mm");
|
Console.WriteLine($" Bounds : {r.Bounds.SizeX:F2} x {r.Bounds.SizeY:F2} x {r.Bounds.SizeZ:F2} mm");
|
||||||
Console.WriteLine($" Time : {sw.ElapsedMilliseconds} ms");
|
Console.WriteLine($" Time : {sw.ElapsedMilliseconds} ms");
|
||||||
Console.WriteLine(" --- 裝配樹 ---");
|
Console.WriteLine(" --- assembly tree ---");
|
||||||
PrintNode(r, 1);
|
PrintNode(r, 1);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -69,6 +84,6 @@ static void PrintNode(ImportedNode n, int indent)
|
|||||||
{
|
{
|
||||||
string seg = n.EdgeSegments is null ? "" : $", edgeSegs={n.EdgeSegments.Count / 2:N0}";
|
string seg = n.EdgeSegments is null ? "" : $", edgeSegs={n.EdgeSegments.Count / 2:N0}";
|
||||||
Console.WriteLine($"{new string(' ', indent * 2)}{n.Name} " +
|
Console.WriteLine($"{new string(' ', indent * 2)}{n.Name} " +
|
||||||
$"[solids={n.SolidCount}, faces={n.FaceCount}, tris={n.TriangleCount:N0}{seg}{(n.HasBrep ? "" : ", 無B-rep")}]");
|
$"[solids={n.SolidCount}, faces={n.FaceCount}, tris={n.TriangleCount:N0}{seg}{(n.HasBrep ? "" : ", no B-rep")}]");
|
||||||
foreach (ImportedNode c in n.Children) PrintNode(c, indent + 1);
|
foreach (ImportedNode c in n.Children) PrintNode(c, indent + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
using STPViewer.Services;
|
||||||
|
using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
|
||||||
|
|
||||||
|
namespace SmokeTest;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// STL 匯出驗證(無 UI):
|
||||||
|
/// 1) binary — 已知立方體網格寫出 → 解析 header/三角形數/座標範圍比對(含縮放)
|
||||||
|
/// 2) ASCII — facet 數與 solid/endsolid 結構
|
||||||
|
/// 3) 退化三角形(零面積)自動濾除
|
||||||
|
/// </summary>
|
||||||
|
public static class StlExportTest
|
||||||
|
{
|
||||||
|
public static int Run(string? stepPath = null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== StlExportTest ===");
|
||||||
|
int failed = 0;
|
||||||
|
string dir = IOPath.Combine(IOPath.GetTempPath(), "stpviewer_stl_test");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
MeshGeometry3D cube = BuildCube(10); // 10×10×10 mm、12 三角形
|
||||||
|
|
||||||
|
// 1) binary:三角形數 + 縮放後座標範圍
|
||||||
|
{
|
||||||
|
string f = IOPath.Combine(dir, "cube_bin.stl");
|
||||||
|
double scale = 1.0 / 25.4; // mm to inches.
|
||||||
|
int written = StlExportService.Write(f, new[] { cube }, ascii: false, scale);
|
||||||
|
(uint count, float min, float max) = ReadBinary(f);
|
||||||
|
bool ok = written == 12 && count == 12
|
||||||
|
&& Math.Abs(min - 0f) < 1e-5 && Math.Abs(max - 10f / 25.4f) < 1e-4;
|
||||||
|
Report(ref failed, ok, $"binary round trip: 12 triangles, inch range [{min:F4}, {max:F4}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) ASCII:facet 數 + 結構
|
||||||
|
{
|
||||||
|
string f = IOPath.Combine(dir, "cube_ascii.stl");
|
||||||
|
int written = StlExportService.Write(f, new[] { cube }, ascii: true, scale: 1.0);
|
||||||
|
string text = File.ReadAllText(f);
|
||||||
|
int facets = CountOccurrences(text, "facet normal");
|
||||||
|
bool ok = written == 12 && facets == 12
|
||||||
|
&& text.StartsWith("solid ", StringComparison.Ordinal)
|
||||||
|
&& text.Contains("endsolid");
|
||||||
|
Report(ref failed, ok, $"ASCII structure valid, facets = {facets}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 退化三角形濾除:加一個零面積三角形 → 仍是 12
|
||||||
|
{
|
||||||
|
var degenerate = new MeshGeometry3D();
|
||||||
|
degenerate.Positions.Add(new Point3D(0, 0, 0));
|
||||||
|
degenerate.Positions.Add(new Point3D(1, 1, 1));
|
||||||
|
degenerate.Positions.Add(new Point3D(2, 2, 2)); // 共線 → 面積 0
|
||||||
|
degenerate.TriangleIndices.Add(0);
|
||||||
|
degenerate.TriangleIndices.Add(1);
|
||||||
|
degenerate.TriangleIndices.Add(2);
|
||||||
|
degenerate.Freeze();
|
||||||
|
|
||||||
|
string f = IOPath.Combine(dir, "cube_degen.stl");
|
||||||
|
int written = StlExportService.Write(f, new[] { cube, degenerate }, ascii: false, scale: 1.0);
|
||||||
|
(uint count, _, _) = ReadBinary(f);
|
||||||
|
bool ok = written == 12 && count == 12;
|
||||||
|
Report(ref failed, ok, $"degenerate triangle filtered ({written} written)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4)(可選)真實 STEP:B-rep 精細重新三角化 → 寫 binary STL 驗證
|
||||||
|
//(不測「較粗」:CADability GetTriangulation 對比快取粗的精度回傳既有快取,粗化無效 → 功能已移除)
|
||||||
|
if (stepPath is not null)
|
||||||
|
{
|
||||||
|
var data = new StepImportService().Import(stepPath);
|
||||||
|
var geos = Collect(data.Root).ToList();
|
||||||
|
int importTris = data.Root.TriangleCount;
|
||||||
|
Console.WriteLine($" Source {IOPath.GetFileName(stepPath)}: {geos.Count} B-rep objects, {importTris:N0} imported triangles");
|
||||||
|
|
||||||
|
var meshes = StlExportService.Tessellate(
|
||||||
|
geos, StlExportService.PrecisionFactor(StlMeshQuality.Fine));
|
||||||
|
string f = IOPath.Combine(dir, "step_fine.stl");
|
||||||
|
int written = StlExportService.Write(f, meshes, ascii: false, scale: 1.0);
|
||||||
|
(uint count, _, _) = ReadBinary(f);
|
||||||
|
Report(ref failed, meshes.Count > 0 && written > 0 && count == written,
|
||||||
|
$"fine retessellation: {meshes.Count} faces, {written:N0} triangles");
|
||||||
|
// CADability 三角形數對精度非單調(0.4× 反而比 1× 少),Fine=0.15× 實測明顯更細
|
||||||
|
Report(ref failed, written > importTris,
|
||||||
|
$"fine ({written:N0}) > imported default ({importTris:N0})");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(failed == 0 ? "StlExportTest: PASS" : $"StlExportTest: FAIL ({failed} checks)");
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<CADability.GeoObject.IGeoObject> Collect(ImportedNode n)
|
||||||
|
{
|
||||||
|
foreach (CADability.GeoObject.IGeoObject g in n.SourceGeos) yield return g;
|
||||||
|
foreach (ImportedNode c in n.Children)
|
||||||
|
foreach (CADability.GeoObject.IGeoObject g in Collect(c))
|
||||||
|
yield return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Report(ref int failed, bool ok, string what)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" [{(ok ? "PASS" : "FAIL")}] {what}");
|
||||||
|
if (!ok) failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>0..size 的軸對齊立方體,12 個三角形(法向不講究,STL 寫出時自算)</summary>
|
||||||
|
private static MeshGeometry3D BuildCube(double s)
|
||||||
|
{
|
||||||
|
var m = new MeshGeometry3D();
|
||||||
|
Point3D[] v =
|
||||||
|
{
|
||||||
|
new(0, 0, 0), new(s, 0, 0), new(s, s, 0), new(0, s, 0),
|
||||||
|
new(0, 0, s), new(s, 0, s), new(s, s, s), new(0, s, s),
|
||||||
|
};
|
||||||
|
foreach (Point3D p in v) m.Positions.Add(p);
|
||||||
|
int[] quads = { 0,3,2,1, 4,5,6,7, 0,1,5,4, 1,2,6,5, 2,3,7,6, 3,0,4,7 };
|
||||||
|
for (int q = 0; q < quads.Length; q += 4)
|
||||||
|
{
|
||||||
|
m.TriangleIndices.Add(quads[q]); m.TriangleIndices.Add(quads[q + 1]); m.TriangleIndices.Add(quads[q + 2]);
|
||||||
|
m.TriangleIndices.Add(quads[q]); m.TriangleIndices.Add(quads[q + 2]); m.TriangleIndices.Add(quads[q + 3]);
|
||||||
|
}
|
||||||
|
m.Freeze();
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (uint Count, float Min, float Max) ReadBinary(string path)
|
||||||
|
{
|
||||||
|
using var br = new BinaryReader(File.OpenRead(path));
|
||||||
|
byte[] header = br.ReadBytes(80);
|
||||||
|
if (Encoding.ASCII.GetString(header, 0, 5) == "solid")
|
||||||
|
throw new InvalidDataException("Binary STL header must not begin with solid");
|
||||||
|
uint count = br.ReadUInt32();
|
||||||
|
float min = float.MaxValue, max = float.MinValue;
|
||||||
|
for (uint t = 0; t < count; t++)
|
||||||
|
{
|
||||||
|
br.ReadBytes(12); // normal
|
||||||
|
for (int k = 0; k < 9; k++)
|
||||||
|
{
|
||||||
|
float f = br.ReadSingle();
|
||||||
|
min = Math.Min(min, f);
|
||||||
|
max = Math.Max(max, f);
|
||||||
|
}
|
||||||
|
br.ReadUInt16(); // attribute
|
||||||
|
}
|
||||||
|
return (count, min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountOccurrences(string text, string token)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (int i = text.IndexOf(token, StringComparison.Ordinal); i >= 0;
|
||||||
|
i = text.IndexOf(token, i + token.Length, StringComparison.Ordinal)) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user