Compare commits
5
Commits
v0.7.1
...
2caa95a69d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2caa95a69d | ||
|
|
fb46da29cf | ||
|
|
c65fc4e67f | ||
|
|
6d48d7f852 | ||
|
|
39ed6d7bba |
+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/
|
||||||
|
|
||||||
|
|||||||
+31
-1
@@ -100,8 +100,12 @@ 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/Volume
|
│ ├── MeasureMode.cs # enum: None/Point/Distance/Edge/Face/Circle/Angle/FaceDistance/Align/Align3/Drag/Interference/Volume
|
||||||
│ ├── MeasurementResult.cs # 量測結果(雙單位 lambda、3D 標籤同步)
|
│ ├── MeasurementResult.cs # 量測結果(雙單位 lambda、3D 標籤同步)
|
||||||
@@ -112,6 +116,8 @@ STPViewer/
|
|||||||
│
|
│
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
||||||
|
│ ├── StlMeshReader.cs # STL 直接解析(binary/ASCII,不經 CADability;v0.7.1)
|
||||||
|
│ ├── LocalizationService.cs # 語言包載入、目前語言、[key] 查詢(缺字回退英文)、LanguageChanged
|
||||||
│ ├── MeasurementService.cs # 點/線/面/圓/角度/面距/體積質心 幾何計算(吸附含圓心)
|
│ ├── MeasurementService.cs # 點/線/面/圓/角度/面距/體積質心 幾何計算(吸附含圓心)
|
||||||
│ ├── InterferenceService.cs # 干涉檢查:三角形-三角形相交(區間法)+均勻網格加速;無干涉時近似最小間隙
|
│ ├── InterferenceService.cs # 干涉檢查:三角形-三角形相交(區間法)+均勻網格加速;無干涉時近似最小間隙
|
||||||
│ ├── RigidAlign.cs # 三點對齊/旋轉的剛體變換數學(Matrix3D列向量 ↔ ModOp行向量 轉換)
|
│ ├── RigidAlign.cs # 三點對齊/旋轉的剛體變換數學(Matrix3D列向量 ↔ ModOp行向量 轉換)
|
||||||
@@ -126,6 +132,8 @@ STPViewer/
|
|||||||
├── MainViewModel.Section.cs # 剖面(v0.4.0 起背景平行裁切)
|
├── MainViewModel.Section.cs # 剖面(v0.4.0 起背景平行裁切)
|
||||||
├── MainViewModel.Interference.cs # 干涉檢查指令
|
├── MainViewModel.Interference.cs # 干涉檢查指令
|
||||||
├── MainViewModel.Export.cs # CSV / 截圖 / STEP / STL 匯出
|
├── MainViewModel.Export.cs # CSV / 截圖 / STEP / STL 匯出
|
||||||
|
├── MainViewModel.Localization.cs # 語言切換指令 + 切換後刷新量測/樹/狀態列文字
|
||||||
|
├── QuickBarViewModel.cs # 快速列按鈕註冊表(key/文字 key/預設值)
|
||||||
├── StlExportViewModel.cs # STL 匯出對話框 VM(選項 + 即時摘要)
|
├── StlExportViewModel.cs # STL 匯出對話框 VM(選項 + 即時摘要)
|
||||||
└── ModelNodeViewModel.cs # 裝配樹節點(可見性/顏色 cascade)
|
└── ModelNodeViewModel.cs # 裝配樹節點(可見性/顏色 cascade)
|
||||||
```
|
```
|
||||||
@@ -201,7 +209,8 @@ class MeasurementResult
|
|||||||
| 快速鍵 | P 點 / D 距離 / E 邊 / F 面 / C 圓 / A 角度 / M 面距;同鍵再按=退出;Esc=取消進行中量測→退出模式 | |
|
| 快速鍵 | P 點 / D 距離 / E 邊 / F 面 / C 圓 / A 角度 / M 面距;同鍵再按=退出;Esc=取消進行中量測→退出模式 | |
|
||||||
| 樹操作 | 搜尋框過濾節點名稱;右鍵:只顯示此節點 / 反轉顯示 / 全部顯示 / 量體積質心 | tooltip 含外形尺寸 L×W×H |
|
| 樹操作 | 搜尋框過濾節點名稱;右鍵:只顯示此節點 / 反轉顯示 / 全部顯示 / 量體積質心 | tooltip 含外形尺寸 L×W×H |
|
||||||
| 體積/質心 | 樹節點右鍵「⚖ 量體積/質心」 | signed volume(封閉實體)+ 質心標記 + AABB 外形尺寸 |
|
| 體積/質心 | 樹節點右鍵「⚖ 量體積/質心」 | signed volume(封閉實體)+ 質心標記 + AABB 外形尺寸 |
|
||||||
| 設定/MRU | 自動記住視窗位置大小、單位;「▾」最近 10 個檔案 | %LOCALAPPDATA%\STPViewer\settings.json |
|
| 語言 | 視圖 → 語言 / 快速列下拉: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)。
|
||||||
@@ -489,6 +498,27 @@ WPF retained-mode 每幀重走 visual tree → frame rate 崩。瀏覽模式因
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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(下一輪)
|
||||||
|
|
||||||
- **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證),
|
- **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
CAD 3D 檢視器(Windows 桌面 WPF, .NET 8):STEP/STL/DXF 匯入、STEP 裝配樹(搜尋/隔離顯示)、
|
CAD 3D 檢視器(Windows 桌面 WPF, .NET 8):STEP/STL/DXF 匯入、STEP 裝配樹(搜尋/隔離顯示)、
|
||||||
點/距離/邊/面/圓/角度/面距/體積質心量測(吸附含圓心)、兩點對齊(平移)、三點對齊(旋轉+平移)、
|
點/距離/邊/面/圓/角度/面距/體積質心量測(吸附含圓心)、兩點對齊(平移)、三點對齊(旋轉+平移)、
|
||||||
軸向旋轉 90°、拖曳模式、干涉檢查(≥2 檔兩兩配對)、剖面(X/Y/Z + 3點任意平面)、
|
軸向旋轉 90°、拖曳模式、干涉檢查(≥2 檔兩兩配對)、剖面(X/Y/Z + 3點任意平面)、
|
||||||
標準視圖+正交投影、量測快速鍵+Esc、mm⇄inch、CSV/截圖/STEP/STL 匯出(STL 有參數對話框)、設定與 MRU 保存。
|
標準視圖+正交投影、量測快速鍵+Esc、mm⇄inch、多語系(英/繁/簡,執行期語言包)、CSV/截圖/STEP/STL 匯出(STL 有參數對話框)、設定與 MRU 保存。
|
||||||
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||||
|
|
||||||
## 技術棧
|
## 技術棧
|
||||||
@@ -49,8 +49,8 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
(樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理
|
(樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理
|
||||||
- UI 三層(v0.7.0):**選單列**(全部功能,分類下拉)+ **快速列**(`QuickBarViewModel` 註冊表,
|
- UI 三層(v0.7.0):**選單列**(全部功能,分類下拉)+ **快速列**(`QuickBarViewModel` 註冊表,
|
||||||
使用者勾選常用按鈕,XAML 索引子綁定 `QuickBar[Key].IsChecked` 控 Visibility,按鈕本體維持靜態綁定)+
|
使用者勾選常用按鈕,XAML 索引子綁定 `QuickBar[Key].IsChecked` 控 Visibility,按鈕本體維持靜態綁定)+
|
||||||
**剖面參數列**(只在 SectionEnabled 顯示)。新增功能要三處都接:選單分類、快速列註冊表
|
**剖面參數列**(只在 SectionEnabled 顯示)。新增功能要四處都接:選單分類、快速列註冊表
|
||||||
(含預設值)+ 對應 XAML 按鈕;勾選清單存 settings.json `QuickBarKeys`(null = 預設)
|
(含預設值)+ 對應 XAML 按鈕、**三個語言包的文字 key**;勾選清單存 settings.json `QuickBarKeys`(null = 預設)
|
||||||
- 使用者設定 `SettingsService`(%LOCALAPPDATA%\STPViewer\settings.json):視窗(MainWindow 管)+
|
- 使用者設定 `SettingsService`(%LOCALAPPDATA%\STPViewer\settings.json):視窗(MainWindow 管)+
|
||||||
單位/MRU(VM `LoadSettings`/`SaveSettingsInto`);壞檔回預設、儲存失敗靜默
|
單位/MRU(VM `LoadSettings`/`SaveSettingsInto`);壞檔回預設、儲存失敗靜默
|
||||||
- 匯出 STEP 走 `CADability.ExportStep.WriteToFile(file, Project.CreateSimpleProject()+Model.Add)`;
|
- 匯出 STEP 走 `CADability.ExportStep.WriteToFile(file, Project.CreateSimpleProject()+Model.Add)`;
|
||||||
@@ -65,6 +65,13 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
OnViewportClick 最前面攔點擊);換軸/關剖面/Esc 會重置。平面位置一律用「AABB 8 角投影到法向」內插,
|
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`
|
||||||
@@ -98,7 +105,8 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
- 發新版:升 csproj `<Version>` 之外,記得在 `AboutDialog.xaml.cs` 的 `Changelog` 陣列**頂端**加一筆
|
- 發新版:升 csproj `<Version>` 之外,記得在 `AboutDialog.xaml.cs` 的 `Changelog` 陣列**頂端**加一筆
|
||||||
(口吻寫給一般使用者:「你會感覺到什麼」,不是技術 changelog)。
|
(口吻寫給一般使用者:「你會感覺到什麼」,不是技術 changelog)。v0.8.0 起內容是 key
|
||||||
|
(`Ui.About.ReleaseXYZ.Title` / `.ChangeN`),**三個語言包都要寫**。
|
||||||
⚠️ WPF **頂層 MenuItem 直接掛 Click 不會觸發**(實測滑鼠/鍵盤都沒反應),要開視窗的入口一律放子選單(如 說明 → 關於)
|
⚠️ WPF **頂層 MenuItem 直接掛 Click 不會觸發**(實測滑鼠/鍵盤都沒反應),要開視窗的入口一律放子選單(如 說明 → 關於)
|
||||||
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
||||||
|
|
||||||
@@ -133,7 +141,9 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)、
|
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)、
|
||||||
`--export-test <in.stp> <out.stp>`(STEP 匯出往返:寫出→回讀比對實體數)、
|
`--export-test <in.stp> <out.stp>`(STEP 匯出往返:寫出→回讀比對實體數)、
|
||||||
`--stl-export-test [file.stp]`(STL binary/ASCII 往返 + 退化濾除;給 stp 加測 B-rep 精細重算)、
|
`--stl-export-test [file.stp]`(STL binary/ASCII 往返 + 退化濾除;給 stp 加測 B-rep 精細重算)、
|
||||||
`--make-stl <out.stl> [triCount]`(產測試用 binary STL,預設 150 萬三角形,量 STL 匯入效能用)
|
`--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,6 +1,6 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@ Open mechanical STEP parts for quick review and measurement without a heavyweigh
|
|||||||
- **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)
|
- **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 **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)
|
- **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
|
||||||
|
- **Language** — choose a language from **View → Language**; available languages come from the `i18n/` JSON files at startup, and your choice is remembered
|
||||||
- **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)
|
- **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
|
- **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
|
- **Remembers your setup** — window size/position, unit choice and recent files persist across sessions; side panels are resizable
|
||||||
@@ -118,6 +119,7 @@ dotnet run --project tools/SmokeTest -- "path\to\model.stp" # import + assembl
|
|||||||
dotnet run --project tools/SmokeTest -- --clip-test # section clipping math
|
dotnet run --project tools/SmokeTest -- --clip-test # section clipping math
|
||||||
dotnet run --project tools/SmokeTest -- --interference-test # interference: intersect / separate / contact
|
dotnet run --project tools/SmokeTest -- --interference-test # interference: intersect / separate / contact
|
||||||
dotnet run --project tools/SmokeTest -- --align-test # 3-point rigid-transform math
|
dotnet run --project tools/SmokeTest -- --align-test # 3-point rigid-transform math
|
||||||
|
dotnet run --project tools/SmokeTest -- --localization-test # language resources and live switching
|
||||||
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP export round-trip
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP export round-trip
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -135,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
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -145,6 +165,7 @@ 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, Settings
|
│ ├── Services/ # StepImport, Measurement, Interference, Section, RigidAlign, Settings
|
||||||
│ └── ViewModels/ # MainViewModel (partial: Drag/Gizmo/Section/Interference/Export), ModelNodeViewModel
|
│ └── ViewModels/ # MainViewModel (partial: Drag/Gizmo/Section/Interference/Export), ModelNodeViewModel
|
||||||
|
|||||||
+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)。
|
||||||
+22
-1
@@ -1,6 +1,6 @@
|
|||||||
# 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)— 多檔匯入、裝配樹、點/距離/邊/面/圓量測。
|
||||||
|
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
- **干涉檢查** 🧩 — 2 個以上可見檔案**兩兩配對**檢查:相交時顯示紅色干涉交線 + 相交三角形對數;無相交時回報最小間隙 gap(gap≈0 即為配合 match,共面貼合不算干涉)
|
- **干涉檢查** 🧩 — 2 個以上可見檔案**兩兩配對**檢查:相交時顯示紅色干涉交線 + 相交三角形對數;無相交時回報最小間隙 gap(gap≈0 即為配合 match,共面貼合不算干涉)
|
||||||
- **剖面** ✂ — X/Y/Z 軸或**在模型上點 3 點定義任意平面**;位置滑桿 + 數值輸入 + 反向;CPU 網格裁切(背景平行運算),原始幾何保留(量測仍精確)
|
- **剖面** ✂ — X/Y/Z 軸或**在模型上點 3 點定義任意平面**;位置滑桿 + 數值輸入 + 反向;CPU 網格裁切(背景平行運算),原始幾何保留(量測仍精確)
|
||||||
- **單位** — mm ⇄ inch 一鍵切換,既有量測(清單與 3D 標籤)即時換算
|
- **單位** — mm ⇄ inch 一鍵切換,既有量測(清單與 3D 標籤)即時換算
|
||||||
|
- **語言** — 從 **視圖 → 語言** 選擇語言;啟動時從 `i18n/` 的 JSON 檔案讀取可用語言,並記住選擇
|
||||||
- **匯出** — 量測結果 CSV(UTF-8 BOM,Excel 中文不亂碼)、3D 視圖 2× PNG 截圖、📤 **STEP 匯出**:可見零件以目前對齊位置寫成新 STEP 檔(原始檔不動)
|
- **匯出** — 量測結果 CSV(UTF-8 BOM,Excel 中文不亂碼)、3D 視圖 2× PNG 截圖、📤 **STEP 匯出**:可見零件以目前對齊位置寫成新 STEP 檔(原始檔不動)
|
||||||
- **視角** — 右鍵旋轉、滾輪縮放、中鍵平移、ViewCube;一鍵 **等角/前/上/右** 標準視圖與 **正交⇄透視** 切換
|
- **視角** — 右鍵旋轉、滾輪縮放、中鍵平移、ViewCube;一鍵 **等角/前/上/右** 標準視圖與 **正交⇄透視** 切換
|
||||||
- **記住設定** — 視窗位置大小、單位、最近檔案自動保存;左右面板可拖曳調寬
|
- **記住設定** — 視窗位置大小、單位、最近檔案自動保存;左右面板可拖曳調寬
|
||||||
@@ -118,6 +119,7 @@ dotnet run --project tools/SmokeTest -- "path\to\model.stp" # 匯入 + 裝配
|
|||||||
dotnet run --project tools/SmokeTest -- --clip-test # 剖切裁切數學
|
dotnet run --project tools/SmokeTest -- --clip-test # 剖切裁切數學
|
||||||
dotnet run --project tools/SmokeTest -- --interference-test # 干涉 相交/分離/貼合
|
dotnet run --project tools/SmokeTest -- --interference-test # 干涉 相交/分離/貼合
|
||||||
dotnet run --project tools/SmokeTest -- --align-test # 三點對齊剛體變換數學
|
dotnet run --project tools/SmokeTest -- --align-test # 三點對齊剛體變換數學
|
||||||
|
dotnet run --project tools/SmokeTest -- --localization-test # 語言資源與即時切換
|
||||||
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP 匯出往返驗證
|
dotnet run --project tools/SmokeTest -- --export-test in.stp out.stp # STEP 匯出往返驗證
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -135,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 後,新語言便會出現在選單與快速列中,不需重新編譯。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 專案結構
|
## 📁 專案結構
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -145,6 +165,7 @@ 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、Settings
|
│ ├── Services/ # StepImport、Measurement、Interference、Section、RigidAlign、Settings
|
||||||
│ └── ViewModels/ # MainViewModel(partial:Drag/Gizmo/Section/Interference/Export)、ModelNodeViewModel
|
│ └── ViewModels/ # MainViewModel(partial:Drag/Gizmo/Section/Interference/Export)、ModelNodeViewModel
|
||||||
|
|||||||
@@ -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,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,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)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<Window x:Class="STPViewer.AboutDialog"
|
<Window x:Class="STPViewer.AboutDialog"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="關於 STPViewer"
|
xmlns:loc="clr-namespace:STPViewer.Localization"
|
||||||
|
Title="{loc:Loc Ui.About.Title}"
|
||||||
Width="860" Height="600"
|
Width="860" Height="600"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
ResizeMode="CanResize" MinWidth="700" MinHeight="420"
|
ResizeMode="CanResize" MinWidth="700" MinHeight="420"
|
||||||
@@ -42,17 +43,14 @@
|
|||||||
Foreground="#1565C0" HorizontalAlignment="Center" />
|
Foreground="#1565C0" HorizontalAlignment="Center" />
|
||||||
<TextBlock x:Name="VersionText" Text="v0.0.0" Style="{StaticResource DimText}"
|
<TextBlock x:Name="VersionText" Text="v0.0.0" Style="{StaticResource DimText}"
|
||||||
HorizontalAlignment="Center" Margin="0,2,0,8" />
|
HorizontalAlignment="Center" Margin="0,2,0,8" />
|
||||||
<TextBlock TextAlignment="Center" FontSize="12" LineHeight="18">
|
<TextBlock Text="{loc:Loc Ui.About.Description}" TextAlignment="Center"
|
||||||
STP / STEP 3D 檢視器<LineBreak />
|
TextWrapping="Wrap" Width="260" FontSize="12" LineHeight="18" />
|
||||||
免開重量級 CAD,快速開檔、量測、<LineBreak />
|
|
||||||
對齊插合、干涉檢查、剖面、匯出
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Style="{StaticResource Card}">
|
<Border Style="{StaticResource Card}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="👤 開發者" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="{loc:Loc Ui.About.Developer}" Style="{StaticResource CardTitle}" />
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="60" />
|
<ColumnDefinition Width="60" />
|
||||||
@@ -61,9 +59,9 @@
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition /><RowDefinition /><RowDefinition />
|
<RowDefinition /><RowDefinition /><RowDefinition />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name" Style="{StaticResource DimText}" />
|
<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="0" Grid.Column="1" Text="ET Wen" FontSize="12" />
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Email" Style="{StaticResource DimText}" />
|
<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="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="0" Text="GitHub" Style="{StaticResource DimText}" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="github.com/ETWen" FontSize="12" />
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="github.com/ETWen" FontSize="12" />
|
||||||
@@ -73,12 +71,9 @@
|
|||||||
|
|
||||||
<Border Style="{StaticResource Card}">
|
<Border Style="{StaticResource Card}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="🔧 技術棧" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="{loc:Loc Ui.About.TechnologyTitle}" Style="{StaticResource CardTitle}" />
|
||||||
<TextBlock Style="{StaticResource DimText}" LineHeight="18">
|
<TextBlock Text="{loc:Loc Ui.About.Technology}" Style="{StaticResource DimText}"
|
||||||
.NET 8 · WPF · C#<LineBreak />
|
TextWrapping="Wrap" Width="260" LineHeight="18" />
|
||||||
CADability(STEP / B-rep 幾何核心)<LineBreak />
|
|
||||||
HelixToolkit(3D 顯示與相機)
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -86,14 +81,14 @@
|
|||||||
<!-- ─── 右:Change log ─── -->
|
<!-- ─── 右:Change log ─── -->
|
||||||
<Border Grid.Column="1" Grid.Row="0" Background="#FAFBFC" CornerRadius="6" Padding="4">
|
<Border Grid.Column="1" Grid.Row="0" Background="#FAFBFC" CornerRadius="6" Padding="4">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock DockPanel.Dock="Top" Text="📋 版本紀錄" FontWeight="Bold" FontSize="14"
|
<TextBlock DockPanel.Dock="Top" Text="{loc:Loc Ui.About.Changelog}" FontWeight="Bold" FontSize="14"
|
||||||
Margin="12,8,0,4" />
|
Margin="12,8,0,4" />
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="12,0,12,8">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="12,0,12,8">
|
||||||
<ItemsControl x:Name="ChangelogList">
|
<ItemsControl x:Name="ChangelogList">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Margin="0,8,0,4">
|
<StackPanel Margin="0,8,0,4">
|
||||||
<TextBlock FontWeight="Bold" Foreground="#1565C0" FontSize="13">
|
<TextBlock FontWeight="Bold" Foreground="#1565C0" FontSize="13" TextWrapping="Wrap">
|
||||||
<Run Text="●" /> <Run Text="{Binding Header, Mode=OneWay}" />
|
<Run Text="●" /> <Run Text="{Binding Header, Mode=OneWay}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<ItemsControl ItemsSource="{Binding Changes}" Margin="16,3,0,0">
|
<ItemsControl ItemsSource="{Binding Changes}" Margin="16,3,0,0">
|
||||||
@@ -112,7 +107,7 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Button Grid.Column="1" Grid.Row="1" Content="關閉" Width="90" Margin="0,10,0,0"
|
<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" />
|
HorizontalAlignment="Right" IsCancel="True" IsDefault="True" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
namespace STPViewer;
|
namespace STPViewer;
|
||||||
|
|
||||||
@@ -18,8 +19,8 @@ public partial class AboutDialog : Window
|
|||||||
VersionText.Text = $"v{v.Major}.{v.Minor}.{v.Build}";
|
VersionText.Text = $"v{v.Major}.{v.Minor}.{v.Build}";
|
||||||
ChangelogList.ItemsSource = Changelog.Select(e => new
|
ChangelogList.ItemsSource = Changelog.Select(e => new
|
||||||
{
|
{
|
||||||
Header = $"v{e.Version} · {e.Title} ({e.Date:yyyy-MM-dd})",
|
Header = $"v{e.Version} · {LocalizationService.Get(e.Title)} ({e.Date:yyyy-MM-dd})",
|
||||||
e.Changes,
|
Changes = e.Changes.Select(LocalizationService.Get).ToArray(),
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,70 +29,77 @@ public partial class AboutDialog : Window
|
|||||||
// ── 版本紀錄(新版加最上面;口吻:一般使用者看得懂,講「你會感覺到什麼」而非實作細節)──
|
// ── 版本紀錄(新版加最上面;口吻:一般使用者看得懂,講「你會感覺到什麼」而非實作細節)──
|
||||||
private static readonly ChangelogEntry[] Changelog =
|
private static readonly ChangelogEntry[] Changelog =
|
||||||
{
|
{
|
||||||
new("0.7.1", new DateOnly(2026, 7, 12), "大型 STL 秒開",
|
new("0.8.0", new DateOnly(2026, 10, 20), "Ui.About.Release080.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 開啟大型 STL 網格檔(數十 MB、上百萬三角形)現在幾乎瞬間完成,不用再等好幾分鐘。",
|
"Ui.About.Release080.Change1",
|
||||||
"• 例如一個 75 MB、150 萬個三角形的 STL,以前要等到天荒地老,現在不到 1 秒就顯示出來。",
|
"Ui.About.Release080.Change2",
|
||||||
|
"Ui.About.Release080.Change3",
|
||||||
}),
|
}),
|
||||||
new("0.7.0", new DateOnly(2026, 7, 4), "介面大整理 — 選單列+可自訂快速列",
|
new("0.7.1", new DateOnly(2026, 7, 12), "Ui.About.Release071.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 上方新增選單列,全部功能依「檔案/視圖/量測/變換/工具」分類,不用再在一長排按鈕裡找。",
|
"Ui.About.Release071.Change1",
|
||||||
"• 原本的工具列變成「快速列」:只放常用按鈕。想放哪些自己決定 — 到「快速列 → 自訂按鈕」勾選,下次開啟會記住。",
|
"Ui.About.Release071.Change2",
|
||||||
"• 剖面的軸向/位置控制列改成只在開啟剖面時出現,平常不佔空間。",
|
|
||||||
"• 新增「關於」視窗(就是這個畫面),可以看版本紀錄。",
|
|
||||||
}),
|
}),
|
||||||
new("0.6.0", new DateOnly(2026, 7, 4), "匯出 STL",
|
new("0.7.0", new DateOnly(2026, 7, 4), "Ui.About.Release070.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 新增匯出 STL:把畫面上的零件(含對齊、旋轉後的位置)存成 STL 網格檔,可拿去 3D 列印或給其他軟體用。",
|
"Ui.About.Release070.Change1",
|
||||||
"• 匯出前會先跳出設定視窗:全部合併成一個檔或每個檔案各存一個、Binary 或文字格式、mm 或 inch、網格精細度 — 確認後才匯出。",
|
"Ui.About.Release070.Change2",
|
||||||
"• 設定視窗會即時顯示三角形數量與預估檔案大小。",
|
"Ui.About.Release070.Change3",
|
||||||
|
"Ui.About.Release070.Change4",
|
||||||
}),
|
}),
|
||||||
new("0.5.0", new DateOnly(2026, 7, 3), "量測、視圖、樹狀面板大升級",
|
new("0.6.0", new DateOnly(2026, 7, 4), "Ui.About.Release060.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 量距離時點到圓孔邊緣會自動吸到圓心 — 量兩孔中心距(pitch)超方便。",
|
"Ui.About.Release060.Change1",
|
||||||
"• 量測快速鍵:P 點、D 距離、E 邊、F 面、C 圓、A 角度、M 面距;Esc 取消。",
|
"Ui.About.Release060.Change2",
|
||||||
"• 樹狀面板可搜尋零件名稱;右鍵可「只顯示此節點」隔離觀察,也可以量體積與質心。",
|
"Ui.About.Release060.Change3",
|
||||||
"• 新增標準視圖(等角/前/上/右)與正交投影,量尺寸不受透視變形。",
|
|
||||||
"• 剖面新增「3點」模式:在模型上點 3 個點,就能沿任意斜面剖開。",
|
|
||||||
"• 新增匯出 STEP:把對齊好的零件位置存成新 STEP 檔,交接給正式 CAD。",
|
|
||||||
"• 會記住視窗大小、單位設定與最近開啟的 10 個檔案。",
|
|
||||||
}),
|
}),
|
||||||
new("0.4.0", new DateOnly(2026, 7, 2), "更快、更穩",
|
new("0.5.0", new DateOnly(2026, 7, 3), "Ui.About.Release050.Title",
|
||||||
new[]
|
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.3.2", new DateOnly(2026, 6, 18), "大組件量測不再卡",
|
new("0.4.0", new DateOnly(2026, 7, 2), "Ui.About.Release040.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 修正大型組件(數萬個面)進入量測模式後,轉動視角嚴重卡頓的問題 — 現在量測跟瀏覽一樣順。",
|
"Ui.About.Release040.Change1",
|
||||||
|
"Ui.About.Release040.Change2",
|
||||||
|
"Ui.About.Release040.Change3",
|
||||||
}),
|
}),
|
||||||
new("0.3.1", new DateOnly(2026, 6, 13), "操作器永遠可見",
|
new("0.3.2", new DateOnly(2026, 6, 18), "Ui.About.Release032.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 三軸操作器改成永遠浮在畫面最上層,不會被零件擋住,任何角度都抓得到。",
|
"Ui.About.Release032.Change1",
|
||||||
}),
|
}),
|
||||||
new("0.3.0", new DateOnly(2026, 6, 13), "三軸操作器",
|
new("0.3.1", new DateOnly(2026, 6, 13), "Ui.About.Release031.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 新增操作器:選取檔案後出現 XYZ 三色箭頭與旋轉環,箭頭沿軸精確移動、旋轉環繞軸轉任意角度。",
|
"Ui.About.Release031.Change1",
|
||||||
}),
|
}),
|
||||||
new("0.2.0", new DateOnly(2026, 6, 13), "拖曳模式",
|
new("0.3.0", new DateOnly(2026, 6, 13), "Ui.About.Release030.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• 新增拖曳模式:左鍵按住零件直接拖到想要的位置,放開就定位。",
|
"Ui.About.Release030.Change1",
|
||||||
"• 0.2.1 修正拖曳過的零件再點擊會當掉的問題。",
|
|
||||||
}),
|
}),
|
||||||
new("0.1.0", new DateOnly(2026, 6, 13), "首次釋出",
|
new("0.2.0", new DateOnly(2026, 6, 13), "Ui.About.Release020.Title",
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"• STEP/STL/DXF 匯入與裝配樹、點/距離/邊/面/圓/角度/面距量測、兩點與三點對齊、干涉檢查、剖面、mm ⇄ inch、CSV 與截圖匯出。",
|
"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,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);
|
||||||
|
}
|
||||||
+149
-131
@@ -4,7 +4,8 @@
|
|||||||
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"
|
||||||
@@ -29,10 +30,10 @@
|
|||||||
<DockPanel>
|
<DockPanel>
|
||||||
<!-- ─── 選單列(全部功能,分類下拉)─── -->
|
<!-- ─── 選單列(全部功能,分類下拉)─── -->
|
||||||
<Menu DockPanel.Dock="Top">
|
<Menu DockPanel.Dock="Top">
|
||||||
<MenuItem Header="檔案(_F)">
|
<MenuItem Header="{loc:Loc Ui.Main.FileF}">
|
||||||
<MenuItem Header="📂 匯入…" Command="{Binding ImportCommand}"
|
<MenuItem Header="{loc:Loc Ui.Main.MenuImport}" Command="{Binding ImportCommand}"
|
||||||
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)" />
|
ToolTip="{loc:Loc Ui.Main.ImportSTPSTEPSTLDXFFiles}" />
|
||||||
<MenuItem Header="最近檔案" ItemsSource="{Binding RecentFiles}">
|
<MenuItem Header="{loc:Loc Ui.Main.RecentFiles}" ItemsSource="{Binding RecentFiles}">
|
||||||
<MenuItem.ItemContainerStyle>
|
<MenuItem.ItemContainerStyle>
|
||||||
<Style TargetType="MenuItem">
|
<Style TargetType="MenuItem">
|
||||||
<Setter Property="Header" Value="{Binding Converter={StaticResource FileNameConv}}" />
|
<Setter Property="Header" Value="{Binding Converter={StaticResource FileNameConv}}" />
|
||||||
@@ -44,83 +45,96 @@
|
|||||||
</MenuItem.ItemContainerStyle>
|
</MenuItem.ItemContainerStyle>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="💾 匯出量測 CSV…" Command="{Binding ExportCsvCommand}" />
|
<MenuItem Header="{loc:Loc Ui.Main.ExportMeasurementsCSV}" Command="{Binding ExportCsvCommand}" />
|
||||||
<MenuItem Header="📷 視圖截圖 PNG…" Command="{Binding SaveScreenshotCommand}"
|
<MenuItem Header="{loc:Loc Ui.Main.SaveViewPNG}" Command="{Binding SaveScreenshotCommand}"
|
||||||
ToolTip="3D 視圖截圖存 PNG(2x 解析度)" />
|
ToolTip="{loc:Loc Ui.Main.SavePNGScreenshot3DView2}" />
|
||||||
<MenuItem Header="📤 匯出 STEP…" Command="{Binding ExportStepFileCommand}"
|
<MenuItem Header="{loc:Loc Ui.Main.ExportSTEP}" Command="{Binding ExportStepFileCommand}"
|
||||||
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔;不動原始檔" />
|
ToolTip="{loc:Loc Ui.Main.ExportStepTooltip}" />
|
||||||
<MenuItem Header="📐 匯出 STL…" Command="{Binding ExportStlFileCommand}"
|
<MenuItem Header="{loc:Loc Ui.Main.ExportSTL}" Command="{Binding ExportStlFileCommand}"
|
||||||
ToolTip="先設定範圍/格式/單位/精度,確認再匯出" />
|
ToolTip="{loc:Loc Ui.Main.ChooseScopeFormatUnitsMeshQuality}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="視圖(_V)">
|
<MenuItem Header="{loc:Loc Ui.Main.ViewV}">
|
||||||
<MenuItem Header="🔍 縮放至全部" Command="{Binding ZoomAllCommand}" />
|
<MenuItem Header="{loc:Loc Ui.Main.ZoomFit}" Command="{Binding ZoomAllCommand}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="等角視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" />
|
<MenuItem Header="{loc:Loc Ui.Main.IsometricView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" />
|
||||||
<MenuItem Header="前視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
<MenuItem Header="{loc:Loc Ui.Main.FrontView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
||||||
ToolTip="-Y 方向看,Z 向上" />
|
ToolTip="{loc:Loc Ui.Main.LookAlongYZUp}" />
|
||||||
<MenuItem Header="上視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
<MenuItem Header="{loc:Loc Ui.Main.TopView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
||||||
ToolTip="+Z 往下看" />
|
ToolTip="{loc:Loc Ui.Main.LookDownFromZ}" />
|
||||||
<MenuItem Header="右視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
<MenuItem Header="{loc:Loc Ui.Main.RightView}" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
||||||
ToolTip="+X 方向看,Z 向上" />
|
ToolTip="{loc:Loc Ui.Main.LookAlongXZUp}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="正交投影" IsCheckable="True" IsChecked="{Binding OrthographicView}"
|
<MenuItem Header="{loc:Loc Ui.Main.OrthographicProjection}" IsCheckable="True" IsChecked="{Binding OrthographicView}"
|
||||||
ToolTip="平行投影,量尺寸不受透視變形;取消勾選切回透視" />
|
ToolTip="{loc:Loc Ui.Main.ParallelProjectionAvoidsPerspectiveDistortionUncheck}" />
|
||||||
<MenuItem Header="單位顯示 inch(mm ⇄ in)" IsCheckable="True" IsChecked="{Binding UseInch}"
|
<MenuItem Header="{loc:Loc Ui.Main.DisplayInchesMm}" IsCheckable="True" IsChecked="{Binding UseInch}"
|
||||||
ToolTip="既有量測即時換算;內部一律存 mm" />
|
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>
|
||||||
<MenuItem Header="量測(_M)">
|
<MenuItem Header="{loc:Loc Ui.Main.MeasureM}">
|
||||||
<MenuItem Header="📍 點座標" IsCheckable="True" InputGestureText="P"
|
<MenuItem Header="{loc:Loc Ui.Main.PointCoordinates}" IsCheckable="True" InputGestureText="P"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}"
|
||||||
ToolTip="量測點座標(吸附頂點/圓心)" />
|
ToolTip="{loc:Loc Ui.Main.MeasurePointCoordinatesSnapsVerticesCircle}" />
|
||||||
<MenuItem Header="📏 兩點距離" IsCheckable="True" InputGestureText="D"
|
<MenuItem Header="{loc:Loc Ui.Main.TwoPointDistance}" IsCheckable="True" InputGestureText="D"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}"
|
||||||
ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)" />
|
ToolTip="{loc:Loc Ui.Main.DistanceXYZCircularEdgesSnapCenters}" />
|
||||||
<MenuItem Header="📐 邊長" IsCheckable="True" InputGestureText="E"
|
<MenuItem Header="{loc:Loc Ui.Main.EdgeLength}" IsCheckable="True" InputGestureText="E"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}"
|
||||||
ToolTip="邊長(直線/曲線/圓弧)" />
|
ToolTip="{loc:Loc Ui.Main.MeasureStraightCurvedCircularEdgeLengths}" />
|
||||||
<MenuItem Header="⬛ 面" IsCheckable="True" InputGestureText="F"
|
<MenuItem Header="{loc:Loc Ui.Main.Face}" IsCheckable="True" InputGestureText="F"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}"
|
||||||
ToolTip="面積 + 曲面類型/法向量" />
|
ToolTip="{loc:Loc Ui.Main.AreaSurfaceTypeNormalVector}" />
|
||||||
<MenuItem Header="⭕ 圓" IsCheckable="True" InputGestureText="C"
|
<MenuItem Header="{loc:Loc Ui.Main.Circle}" IsCheckable="True" InputGestureText="C"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}"
|
||||||
ToolTip="圓心/半徑/直徑/周長" />
|
ToolTip="{loc:Loc Ui.Main.CenterRadiusDiameterCircumference}" />
|
||||||
<MenuItem Header="∠ 角度" IsCheckable="True" InputGestureText="A"
|
<MenuItem Header="{loc:Loc Ui.Main.Angle}" IsCheckable="True" InputGestureText="A"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}"
|
||||||
ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)" />
|
ToolTip="{loc:Loc Ui.Main.AngleBetweenTwoFacesStraightEdges}" />
|
||||||
<MenuItem Header="⇔ 面距" IsCheckable="True" InputGestureText="M"
|
<MenuItem Header="{loc:Loc Ui.Main.FaceDistance}" IsCheckable="True" InputGestureText="M"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}"
|
||||||
ToolTip="面到面最短距離(網格近似)" />
|
ToolTip="{loc:Loc Ui.Main.ShortestDistanceBetweenFacesMeshApproximation}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="🧹 清除全部量測" Command="{Binding ClearMeasurementsCommand}"
|
<MenuItem Header="{loc:Loc Ui.Main.ClearAllMeasurements}" Command="{Binding ClearMeasurementsCommand}"
|
||||||
InputGestureText="Esc" ToolTip="Esc = 取消進行中量測/退出模式" />
|
InputGestureText="Esc" ToolTip="{loc:Loc Ui.Main.EscCancelsCurrentMeasurementExitsMode}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="變換(_T)">
|
<MenuItem Header="{loc:Loc Ui.Main.TransformT}">
|
||||||
<MenuItem Header="🎯 兩點對齊(平移)" IsCheckable="True"
|
<MenuItem Header="{loc:Loc Ui.Main.TwoPointAlignmentTranslate}" IsCheckable="True"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}"
|
||||||
ToolTip="先點要移動零件上的點,再點目標點" />
|
ToolTip="{loc:Loc Ui.Main.PickPointPartMoveThenPick}" />
|
||||||
<MenuItem Header="🎯 三點對齊(旋轉+平移)" IsCheckable="True"
|
<MenuItem Header="{loc:Loc Ui.Main.ThreePointAlignmentRotateTranslate}" IsCheckable="True"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}"
|
||||||
ToolTip="先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)" />
|
ToolTip="{loc:Loc Ui.Main.PickThreePointsMovingModelThen}" />
|
||||||
<MenuItem Header="🖐 拖曳模式" IsCheckable="True"
|
<MenuItem Header="{loc:Loc Ui.Main.DragMode}" IsCheckable="True"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}"
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}"
|
||||||
ToolTip="左鍵按住零件沿螢幕平面拖動,放開定位" />
|
ToolTip="{loc:Loc Ui.Main.HoldLeftMouseButtonMovePart}" />
|
||||||
<MenuItem Header="⊹ 三軸操作器" IsCheckable="True" IsChecked="{Binding GizmoEnabled}"
|
<MenuItem Header="{loc:Loc Ui.Main.3AxisGizmo}" IsCheckable="True" IsChecked="{Binding GizmoEnabled}"
|
||||||
ToolTip="對樹面板選取的檔案顯示 XYZ 箭頭與旋轉環,每次放開即定位" />
|
ToolTip="{loc:Loc Ui.Main.ShowXYZArrowsRotationRingsSelected}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="↻ 繞 X 軸 +90°" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
<MenuItem Header="{loc:Loc Ui.Main.Rotate90AroundX}" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
||||||
ToolTip="對樹面板選取的檔案,繞其中心旋轉(先在樹面板點選檔案)" />
|
ToolTip="{loc:Loc Ui.Main.RotateModelSelectedTreeAroundIts}" />
|
||||||
<MenuItem Header="↻ 繞 Y 軸 +90°" Command="{Binding RotateRootCommand}" CommandParameter="Y" />
|
<MenuItem Header="{loc:Loc Ui.Main.Rotate90AroundY}" Command="{Binding RotateRootCommand}" CommandParameter="Y" />
|
||||||
<MenuItem Header="↻ 繞 Z 軸 +90°" Command="{Binding RotateRootCommand}" CommandParameter="Z" />
|
<MenuItem Header="{loc:Loc Ui.Main.Rotate90AroundZ}" Command="{Binding RotateRootCommand}" CommandParameter="Z" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="工具(_L)">
|
<MenuItem Header="{loc:Loc Ui.Main.ToolsL}">
|
||||||
<MenuItem Header="🧩 干涉檢查" Command="{Binding CheckInterferenceCommand}"
|
<MenuItem Header="{loc:Loc Ui.Main.InterferenceCheck}" Command="{Binding CheckInterferenceCommand}"
|
||||||
ToolTip="2 個以上可見檔案兩兩檢查是否相交;無相交時回報最小間隙" />
|
ToolTip="{loc:Loc Ui.Main.CheckVisibleModelPairsIntersectionsReport}" />
|
||||||
<MenuItem Header="✂ 剖面檢視" IsCheckable="True" IsChecked="{Binding SectionEnabled}"
|
<MenuItem Header="{loc:Loc Ui.Main.SectionView}" IsCheckable="True" IsChecked="{Binding SectionEnabled}"
|
||||||
ToolTip="開啟後下方出現剖面參數列(軸向/位置/反向)" />
|
ToolTip="{loc:Loc Ui.Main.ShowSectionControlsAxisPositionDirection}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="快速列(_Q)">
|
<MenuItem Header="{loc:Loc Ui.Main.QuickBarQ}">
|
||||||
<MenuItem Header="自訂按鈕" ItemsSource="{Binding QuickBar.Items}"
|
<MenuItem Header="{loc:Loc Ui.Main.CustomizeButtons}" ItemsSource="{Binding QuickBar.Items}"
|
||||||
ToolTip="勾選要顯示在快速列的常用按鈕(隨設定保存)">
|
ToolTip="{loc:Loc Ui.Main.CheckButtonsShowQuickBarSaved}">
|
||||||
<MenuItem.ItemContainerStyle>
|
<MenuItem.ItemContainerStyle>
|
||||||
<Style TargetType="MenuItem">
|
<Style TargetType="MenuItem">
|
||||||
<Setter Property="Header" Value="{Binding Label}" />
|
<Setter Property="Header" Value="{Binding Label}" />
|
||||||
@@ -131,11 +145,11 @@
|
|||||||
</MenuItem.ItemContainerStyle>
|
</MenuItem.ItemContainerStyle>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="恢復預設按鈕" Command="{Binding QuickBar.ResetDefaultsCommand}" />
|
<MenuItem Header="{loc:Loc Ui.Main.RestoreDefaultButtons}" Command="{Binding QuickBar.ResetDefaultsCommand}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="說明(_H)">
|
<MenuItem Header="{loc:Loc Ui.Main.HelpH}">
|
||||||
<MenuItem Header="ℹ 關於 STPViewer…" Click="About_Click"
|
<MenuItem Header="{loc:Loc Ui.Main.IAboutSTPViewer}" Click="About_Click"
|
||||||
ToolTip="程式資訊與版本紀錄" />
|
ToolTip="{loc:Loc Ui.Main.ApplicationInformationReleaseHistory}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
@@ -144,97 +158,101 @@
|
|||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding ImportCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ImportCommand}"
|
||||||
Visibility="{Binding QuickBar[Import].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Import].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
ToolTip="{loc:Loc Ui.Main.ImportSTPSTEPSTLDXFFiles}" Content="{loc:Loc Ui.Main.ToolbarImport}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Click="Recent_Click" Padding="4,4"
|
<Button Style="{StaticResource QuickButton}" Click="Recent_Click" Padding="4,4"
|
||||||
Visibility="{Binding QuickBar[Recent].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Recent].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="最近開啟的檔案">▾</Button>
|
ToolTip="{loc:Loc Ui.Main.RecentlyOpenedFiles}">▾</Button>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding ZoomAllCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ZoomAllCommand}"
|
||||||
Visibility="{Binding QuickBar[ZoomAll].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ZoomAll].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
ToolTip="{loc:Loc Ui.Main.FitAllModelsView}" Content="{loc:Loc Ui.Main.Fit}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso"
|
||||||
Visibility="{Binding QuickBar[ViewIso].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ViewIso].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="等角視圖">等角</Button>
|
ToolTip="{loc:Loc Ui.Main.IsometricView}" Content="{loc:Loc Ui.Main.Iso}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
||||||
Visibility="{Binding QuickBar[ViewFront].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ViewFront].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="前視圖(-Y 方向看,Z 向上)">前</Button>
|
ToolTip="{loc:Loc Ui.Main.FrontViewLookAlongYZ}" Content="{loc:Loc Ui.Main.Front}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
||||||
Visibility="{Binding QuickBar[ViewTop].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ViewTop].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="上視圖(+Z 往下看)">上</Button>
|
ToolTip="{loc:Loc Ui.Main.TopViewLookDownFromZ}" Content="{loc:Loc Ui.Main.Top}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
||||||
Visibility="{Binding QuickBar[ViewRight].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ViewRight].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="右視圖(+X 方向看,Z 向上)">右</Button>
|
ToolTip="{loc:Loc Ui.Main.RightViewLookAlongXZ}" Content="{loc:Loc Ui.Main.Right}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding OrthographicView}"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding OrthographicView}"
|
||||||
Visibility="{Binding QuickBar[Ortho].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Ortho].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="正交投影(平行投影,量尺寸不受透視變形;再按切回透視)">正交</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.OrthographicProjectionAvoidsPerspectiveDistortionClick}" Content="{loc:Loc Ui.Main.Ortho}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點/圓心)— 快速鍵 P"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.PointCoordinatesVertexCircleCenterSnapping}"
|
||||||
Visibility="{Binding QuickBar[Point].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Point].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}">📍 點</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}" Content="{loc:Loc Ui.Main.Point}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)— 快速鍵 D"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.DistanceXYZCircleCenterSnappingHole}"
|
||||||
Visibility="{Binding QuickBar[Distance].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Distance].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}">📏 距離</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}" Content="{loc:Loc Ui.Main.Distance}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)— 快速鍵 E"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.StraightCurvedCircularEdgeLengthShortcut}"
|
||||||
Visibility="{Binding QuickBar[Edge].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Edge].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}">📐 邊</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}" Content="{loc:Loc Ui.Main.Edge}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量 — 快速鍵 F"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.AreaSurfaceTypeNormalVectorShortcut}"
|
||||||
Visibility="{Binding QuickBar[Face].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Face].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}">⬛ 面</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}" Content="{loc:Loc Ui.Main.Face}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長 — 快速鍵 C"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.CenterRadiusDiameterCircumferenceShortcutC}"
|
||||||
Visibility="{Binding QuickBar[Circle].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Circle].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}">⭕ 圓</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}" Content="{loc:Loc Ui.Main.Circle}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)— 快速鍵 A"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.AngleBetweenFacesStraightEdgesShortcut}"
|
||||||
Visibility="{Binding QuickBar[Angle].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Angle].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}">∠ 角度</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}" Content="{loc:Loc Ui.Main.Angle}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)— 快速鍵 M"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="{loc:Loc Ui.Main.ShortestFaceDistanceMeshApproximationShortcut}"
|
||||||
Visibility="{Binding QuickBar[FaceDistance].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[FaceDistance].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}">⇔ 面距</ToggleButton>
|
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}"
|
||||||
Visibility="{Binding QuickBar[Align].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Align].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}">🎯 對齊</ToggleButton>
|
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}"
|
||||||
Visibility="{Binding QuickBar[Align3].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Align3].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}">🎯 三點</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}" Content="{loc:Loc Ui.Main.3Point}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
||||||
Visibility="{Binding QuickBar[RotateX].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[RotateX].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="選取的檔案繞 X 軸 +90°(先在樹面板點選檔案)">↻X</Button>
|
ToolTip="{loc:Loc Ui.Main.RotateModelSelectedTree90Around}">↻X</Button>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Y"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Y"
|
||||||
Visibility="{Binding QuickBar[RotateY].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[RotateY].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="選取的檔案繞 Y 軸 +90°">↻Y</Button>
|
ToolTip="{loc:Loc Ui.Main.RotateSelectedModel90AroundY}">↻Y</Button>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Z"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Z"
|
||||||
Visibility="{Binding QuickBar[RotateZ].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[RotateZ].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="選取的檔案繞 Z 軸 +90°">↻Z</Button>
|
ToolTip="{loc:Loc Ui.Main.RotateSelectedModel90AroundZ}">↻Z</Button>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="拖曳模式:左鍵按住零件沿螢幕平面拖動,放開定位(深度方向先轉視角再拖)"
|
ToolTip="{loc:Loc Ui.Main.DragModeHoldLeftMouseButton}"
|
||||||
Visibility="{Binding QuickBar[Drag].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Drag].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}">🖐 拖曳</ToggleButton>
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}" Content="{loc:Loc Ui.Main.Drag}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding GizmoEnabled}"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding GizmoEnabled}"
|
||||||
Visibility="{Binding QuickBar[Gizmo].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Gizmo].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.3AxisGizmoUseXYZArrows}" Content="{loc:Loc Ui.Main.Gizmo}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding CheckInterferenceCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding CheckInterferenceCommand}"
|
||||||
Visibility="{Binding QuickBar[Interference].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Interference].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="干涉檢查:2 個以上可見檔案兩兩檢查是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
ToolTip="{loc:Loc Ui.Main.InterferenceCheckTestVisibleModelPairs}" Content="{loc:Loc Ui.Main.Interference}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding SectionEnabled}"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding SectionEnabled}"
|
||||||
Visibility="{Binding QuickBar[Section].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Section].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="開啟/關閉剖面檢視(開啟後下方出現參數列)">✂ 剖面</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.ToggleSectionViewItsControls}" Content="{loc:Loc Ui.Main.ToolbarSection}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding ClearMeasurementsCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ClearMeasurementsCommand}"
|
||||||
Visibility="{Binding QuickBar[Clear].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Clear].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="清除全部量測與標記(Esc = 取消進行中量測/退出模式)">🧹 清除</Button>
|
ToolTip="{loc:Loc Ui.Main.ClearAllMeasurementsMarkersEscCancels}" Content="{loc:Loc Ui.Main.Clear}" />
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding UseInch}"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding UseInch}"
|
||||||
Visibility="{Binding QuickBar[Unit].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Unit].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
ToolTip="{loc:Loc Ui.Main.SwitchDisplayUnitsBetweenMmInches}">mm ⇄ in</ToggleButton>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportCsvCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportCsvCommand}"
|
||||||
Visibility="{Binding QuickBar[ExportCsv].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ExportCsv].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
ToolTip="{loc:Loc Ui.Main.ExportMeasurementResultsCSV}">💾 CSV</Button>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding SaveScreenshotCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SaveScreenshotCommand}"
|
||||||
Visibility="{Binding QuickBar[Screenshot].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[Screenshot].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
ToolTip="{loc:Loc Ui.Main.SavePNGScreenshot3DView2}" Content="{loc:Loc Ui.Main.Capture}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStepFileCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStepFileCommand}"
|
||||||
Visibility="{Binding QuickBar[ExportStep].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ExportStep].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔 — 對齊插合結果可交接 CAD;不動原始檔">📤 STEP</Button>
|
ToolTip="{loc:Loc Ui.Main.ToolbarExportStepTooltip}">📤 STEP</Button>
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStlFileCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStlFileCommand}"
|
||||||
Visibility="{Binding QuickBar[ExportStl].IsChecked, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding QuickBar[ExportStl].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="把可見檔案(目前位置)匯出成 STL 網格檔 — 先設定範圍/格式/單位/精度,確認再匯出">📐 STL</Button>
|
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>
|
||||||
|
|
||||||
@@ -242,23 +260,23 @@
|
|||||||
<ToolBarTray DockPanel.Dock="Top"
|
<ToolBarTray DockPanel.Dock="Top"
|
||||||
Visibility="{Binding SectionEnabled, Converter={StaticResource BoolToVis}}">
|
Visibility="{Binding SectionEnabled, Converter={StaticResource BoolToVis}}">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<TextBlock Text="✂ 剖面:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
<TextBlock Text="{loc:Loc Ui.Main.SectionLabel}" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="60" Margin="4,0"
|
<ComboBox SelectedIndex="{Binding SectionAxisIndex}" Width="60" Margin="4,0"
|
||||||
VerticalAlignment="Center" ToolTip="剖切軸向(3點 = 在模型上點 3 點定義任意平面)">
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.SectionAxis3PointsDefineAn}">
|
||||||
<ComboBoxItem>X</ComboBoxItem>
|
<ComboBoxItem>X</ComboBoxItem>
|
||||||
<ComboBoxItem>Y</ComboBoxItem>
|
<ComboBoxItem>Y</ComboBoxItem>
|
||||||
<ComboBoxItem>Z</ComboBoxItem>
|
<ComboBoxItem>Z</ComboBoxItem>
|
||||||
<ComboBoxItem>3點</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}" />
|
||||||
<TextBox Text="{Binding SectionPosition, StringFormat={}{0:F0}}" Width="34"
|
<TextBox Text="{Binding SectionPosition, StringFormat={}{0:F0}}" Width="34"
|
||||||
VerticalAlignment="Center" ToolTip="剖面位置 %(0–100,可直接輸入,離開欄位生效)" />
|
VerticalAlignment="Center" ToolTip="{loc:Loc Ui.Main.SectionPosition0100EnterValue}" />
|
||||||
<TextBlock Text="%" VerticalAlignment="Center" Margin="1,0,0,0" />
|
<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="切換保留側" />
|
Margin="4,0" ToolTip="{loc:Loc Ui.Main.SwitchRetainedSide}" />
|
||||||
<Button Style="{StaticResource QuickButton}" Command="{Binding CloseSectionCommand}"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding CloseSectionCommand}"
|
||||||
ToolTip="關閉剖面檢視">✕ 關閉</Button>
|
ToolTip="{loc:Loc Ui.Main.CloseSectionView}" Content="{loc:Loc Ui.Main.Close}" />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
</ToolBarTray>
|
</ToolBarTray>
|
||||||
|
|
||||||
@@ -283,11 +301,11 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- ─── 裝配樹面板 ─── -->
|
<!-- ─── 裝配樹面板 ─── -->
|
||||||
<GroupBox Grid.Column="0" Header="模型 / 裝配樹" Margin="4">
|
<GroupBox Grid.Column="0" Header="{loc:Loc Ui.Main.ModelsAssemblyTree}" Margin="4">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBox DockPanel.Dock="Top" Margin="0,2,0,4"
|
<TextBox DockPanel.Dock="Top" Margin="0,2,0,4"
|
||||||
Text="{Binding TreeFilter, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding TreeFilter, UpdateSourceTrigger=PropertyChanged}"
|
||||||
ToolTip="輸入名稱過濾裝配樹(只影響樹面板顯示,不影響 3D 可見性;清空 = 全顯)" />
|
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"
|
||||||
@@ -308,23 +326,23 @@
|
|||||||
Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
|
Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
|
||||||
<StackPanel.ContextMenu>
|
<StackPanel.ContextMenu>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<MenuItem Header="👁 只顯示此節點"
|
<MenuItem Header="{loc:Loc Ui.Main.ShowOnlyThisNode}"
|
||||||
Command="{Binding PlacementTarget.Tag.IsolateNodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
Command="{Binding PlacementTarget.Tag.IsolateNodeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
CommandParameter="{Binding}" />
|
CommandParameter="{Binding}" />
|
||||||
<MenuItem Header="🔄 反轉顯示"
|
<MenuItem Header="{loc:Loc Ui.Main.InvertVisibility}"
|
||||||
Command="{Binding PlacementTarget.Tag.InvertVisibilityCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
Command="{Binding PlacementTarget.Tag.InvertVisibilityCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
<MenuItem Header="☀ 全部顯示"
|
<MenuItem Header="{loc:Loc Ui.Main.ShowAll}"
|
||||||
Command="{Binding PlacementTarget.Tag.ShowAllCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
Command="{Binding PlacementTarget.Tag.ShowAllCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="⚖ 量體積 / 質心"
|
<MenuItem Header="{loc:Loc Ui.Main.MeasureVolumeCentroid}"
|
||||||
Command="{Binding PlacementTarget.Tag.MeasureVolumeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
Command="{Binding PlacementTarget.Tag.MeasureVolumeCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
|
||||||
CommandParameter="{Binding}" />
|
CommandParameter="{Binding}" />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</StackPanel.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">
|
||||||
@@ -348,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}" />
|
||||||
@@ -370,7 +388,7 @@
|
|||||||
|
|
||||||
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"
|
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
ToolTip="拖曳調整面板寬度" />
|
ToolTip="{loc:Loc Ui.Main.DragResizePanel}" />
|
||||||
|
|
||||||
<!-- ─── 3D 視圖 ─── -->
|
<!-- ─── 3D 視圖 ─── -->
|
||||||
<h:HelixViewport3D x:Name="viewport" Grid.Column="2"
|
<h:HelixViewport3D x:Name="viewport" Grid.Column="2"
|
||||||
@@ -407,10 +425,10 @@
|
|||||||
|
|
||||||
<GridSplitter Grid.Column="3" Width="5" HorizontalAlignment="Stretch"
|
<GridSplitter Grid.Column="3" Width="5" HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
VerticalAlignment="Stretch" Background="#FFD8D8D8"
|
||||||
ToolTip="拖曳調整面板寬度" />
|
ToolTip="{loc:Loc Ui.Main.DragResizePanel}" />
|
||||||
|
|
||||||
<!-- ─── 量測結果面板 ─── -->
|
<!-- ─── 量測結果面板 ─── -->
|
||||||
<GroupBox Grid.Column="4" 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>
|
||||||
@@ -426,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>
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
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);
|
||||||
@@ -31,12 +30,27 @@ public partial class MainWindow : Window
|
|||||||
_settings = SettingsService.Load();
|
_settings = SettingsService.Load();
|
||||||
ApplyWindowSettings();
|
ApplyWindowSettings();
|
||||||
_vm.LoadSettings(_settings);
|
_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()
|
private void ApplyWindowSettings()
|
||||||
{
|
{
|
||||||
if (_settings.WindowWidth >= 400 && _settings.WindowHeight >= 300)
|
if (_settings.WindowWidth >= 400 && _settings.WindowHeight >= 300)
|
||||||
@@ -107,7 +121,7 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
if (_vm.RecentFiles.Count == 0)
|
if (_vm.RecentFiles.Count == 0)
|
||||||
{
|
{
|
||||||
menu.Items.Add(new MenuItem { Header = "(沒有最近開啟的檔案)", IsEnabled = false });
|
menu.Items.Add(new MenuItem { Header = LocalizationService.Get("App.NoRecentFiles"), IsEnabled = false });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Version>0.7.1</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;
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ public class MeasurementService
|
|||||||
if (d < bestD) { bestD = d; best = p; }
|
if (d < bestD) { bestD = d; best = p; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* 頂點存取失敗則不吸附 */ }
|
catch { }
|
||||||
|
|
||||||
// 圓形邊:以「命中點到邊曲線」的距離參與比較,勝出時吸附到圓心
|
// 圓形邊:以「命中點到邊曲線」的距離參與比較,勝出時吸附到圓心
|
||||||
try
|
try
|
||||||
@@ -67,7 +68,7 @@ public class MeasurementService
|
|||||||
if (d < bestD) { bestD = d; best = ToP3(el.Center); }
|
if (d < bestD) { bestD = d; best = ToP3(el.Center); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* 邊存取失敗則維持頂點吸附結果 */ }
|
catch { }
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 點 / 距離 ───────────────────────────────────────────────
|
// ─── 點 / 距離 ───────────────────────────────────────────────
|
||||||
@@ -134,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));
|
||||||
@@ -193,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));
|
||||||
@@ -219,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
|
||||||
{
|
{
|
||||||
@@ -241,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)}");
|
||||||
@@ -257,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 + 標籤
|
||||||
@@ -302,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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,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));
|
||||||
@@ -378,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));
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class AppSettings
|
|||||||
public double WindowTop { get; set; } = double.NaN;
|
public double WindowTop { get; set; } = double.NaN;
|
||||||
public bool WindowMaximized { get; set; }
|
public bool WindowMaximized { get; set; }
|
||||||
public bool UseInch { get; set; }
|
public bool UseInch { get; set; }
|
||||||
|
public string? Language { get; set; }
|
||||||
public List<string> RecentFiles { get; set; } = new();
|
public List<string> RecentFiles { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>快速工具列顯示的按鈕 key 清單;null = 用預設(QuickBarViewModel.Registry)</summary>
|
/// <summary>快速工具列顯示的按鈕 key 清單;null = 用預設(QuickBarViewModel.Registry)</summary>
|
||||||
@@ -36,7 +37,7 @@ public static class SettingsService
|
|||||||
if (File.Exists(SettingsPath))
|
if (File.Exists(SettingsPath))
|
||||||
return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(SettingsPath)) ?? new AppSettings();
|
return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(SettingsPath)) ?? new AppSettings();
|
||||||
}
|
}
|
||||||
catch { /* 壞檔回預設 */ }
|
catch { }
|
||||||
return new AppSettings();
|
return new AppSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +49,6 @@ public static class SettingsService
|
|||||||
File.WriteAllText(SettingsPath,
|
File.WriteAllText(SettingsPath,
|
||||||
JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }));
|
JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
}
|
}
|
||||||
catch { /* 儲存失敗靜默 */ }
|
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,12 +127,13 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +146,11 @@ public class StepImportService
|
|||||||
|
|
||||||
private void ImportStlFile(string path, ImportedNode root)
|
private void ImportStlFile(string path, ImportedNode root)
|
||||||
{
|
{
|
||||||
Progress?.Invoke("解析 STL(直接讀三角形)…");
|
ReportProgress(() => Get("Runtime.ParsingStl"));
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
List<StlMeshReader.MeshGroup> groups = StlMeshReader.Read(path);
|
List<StlMeshReader.MeshGroup> groups = StlMeshReader.Read(path);
|
||||||
if (groups.Count == 0)
|
if (groups.Count == 0)
|
||||||
throw new InvalidDataException("STL 檔內沒有三角形");
|
throw new InvalidDataException(Get("Runtime.StlNoTriangles"));
|
||||||
|
|
||||||
foreach (StlMeshReader.MeshGroup g in groups)
|
foreach (StlMeshReader.MeshGroup g in groups)
|
||||||
{
|
{
|
||||||
@@ -149,6 +163,8 @@ public class StepImportService
|
|||||||
var leaf = new ImportedNode
|
var leaf = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = g.Name,
|
Name = g.Name,
|
||||||
|
NameKey = g.NameKey,
|
||||||
|
NameNumber = g.NameNumber,
|
||||||
HasBrep = false,
|
HasBrep = false,
|
||||||
SolidCount = 1,
|
SolidCount = 1,
|
||||||
FaceCount = tris, // STL 無 B-rep 面 → 以三角形數代表
|
FaceCount = tris, // STL 無 B-rep 面 → 以三角形數代表
|
||||||
@@ -158,7 +174,9 @@ public class StepImportService
|
|||||||
leaf.Faces.Add(new ImportedFace(null, mesh));
|
leaf.Faces.Add(new ImportedFace(null, mesh));
|
||||||
root.Children.Add(leaf);
|
root.Children.Add(leaf);
|
||||||
}
|
}
|
||||||
Progress?.Invoke($"STL 解析 {sw.ElapsedMilliseconds:N0} ms({root.Children.Count} 個網格)");
|
long elapsed = sw.ElapsedMilliseconds;
|
||||||
|
int meshCount = root.Children.Count;
|
||||||
|
ReportProgress(() => Format("Runtime.StlParsed", elapsed, meshCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
||||||
@@ -167,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));
|
||||||
}
|
}
|
||||||
@@ -219,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(() =>
|
||||||
{
|
{
|
||||||
@@ -243,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),
|
||||||
@@ -260,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;
|
||||||
@@ -277,7 +302,12 @@ 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)
|
||||||
{
|
{
|
||||||
only.Name = node.Name;
|
if (node.NameKey is null || only.NameKey is not null || string.IsNullOrWhiteSpace(only.Name))
|
||||||
|
{
|
||||||
|
only.Name = node.Name;
|
||||||
|
only.NameKey = node.NameKey;
|
||||||
|
only.NameNumber = node.NameNumber;
|
||||||
|
}
|
||||||
return only;
|
return only;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,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);
|
||||||
@@ -312,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(() =>
|
||||||
@@ -381,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,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);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public static class StlExportService
|
|||||||
for (int i = 0; i + 2 < idx.Count; i += 3)
|
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]];
|
Point3D a = pos[idx[i]], b = pos[idx[i + 1]], c = pos[idx[i + 2]];
|
||||||
if (!TryNormal(a, b, c, out Vector3D n)) continue; // 退化三角形
|
if (!TryNormal(a, b, c, out Vector3D n)) continue;
|
||||||
bw.Write((float)n.X); bw.Write((float)n.Y); bw.Write((float)n.Z);
|
bw.Write((float)n.X); bw.Write((float)n.Y); bw.Write((float)n.Z);
|
||||||
WriteVertex(bw, a, scale);
|
WriteVertex(bw, a, scale);
|
||||||
WriteVertex(bw, b, scale);
|
WriteVertex(bw, b, scale);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ namespace STPViewer.Services;
|
|||||||
public static class StlMeshReader
|
public static class StlMeshReader
|
||||||
{
|
{
|
||||||
/// <summary>一個 solid 區塊的網格(頂點未焊接:每三角形 3 個獨立頂點,與 STL 原生一致)</summary>
|
/// <summary>一個 solid 區塊的網格(頂點未焊接:每三角形 3 個獨立頂點,與 STL 原生一致)</summary>
|
||||||
public record MeshGroup(string Name, Point3DCollection Positions, Int32Collection Indices);
|
public record MeshGroup(string Name, Point3DCollection Positions, Int32Collection Indices,
|
||||||
|
string? NameKey = null, int NameNumber = 0);
|
||||||
|
|
||||||
public static List<MeshGroup> Read(string path)
|
public static List<MeshGroup> Read(string path)
|
||||||
{
|
{
|
||||||
@@ -71,7 +72,7 @@ public static class StlMeshReader
|
|||||||
}
|
}
|
||||||
off += 2; // attribute byte count
|
off += 2; // attribute byte count
|
||||||
}
|
}
|
||||||
return new List<MeshGroup> { new("網格", positions, indices) };
|
return new List<MeshGroup> { new(LocalizationService.Get("Runtime.MeshName"), positions, indices, "Runtime.MeshName") };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -88,7 +89,8 @@ public static class StlMeshReader
|
|||||||
void FlushGroup()
|
void FlushGroup()
|
||||||
{
|
{
|
||||||
if (pos is { Count: > 0 } && idx is not null)
|
if (pos is { Count: > 0 } && idx is not null)
|
||||||
groups.Add(new MeshGroup(groups.Count == 0 ? "網格" : $"網格 {groups.Count + 1}", pos, idx));
|
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;
|
pos = null; idx = null; vi = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:STPViewer.ViewModels"
|
xmlns:vm="clr-namespace:STPViewer.ViewModels"
|
||||||
Title="匯出 STL — 參數設定"
|
xmlns:loc="clr-namespace:STPViewer.Localization"
|
||||||
Width="440" SizeToContent="Height"
|
Title="{loc:Loc Ui.StlExport.Title}"
|
||||||
|
Width="520" SizeToContent="Height"
|
||||||
WindowStartupLocation="CenterOwner"
|
WindowStartupLocation="CenterOwner"
|
||||||
ResizeMode="NoResize" ShowInTaskbar="False"
|
ResizeMode="NoResize" ShowInTaskbar="False"
|
||||||
d:DataContext="{d:DesignInstance Type=vm:StlExportViewModel}"
|
d:DataContext="{d:DesignInstance Type=vm:StlExportViewModel}"
|
||||||
@@ -38,38 +39,38 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Grid.Resources>
|
</Grid.Resources>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="匯出範圍" />
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{loc:Loc Ui.StlExport.Scope}" />
|
||||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedIndex="{Binding ScopeIndex}"
|
<ComboBox Grid.Row="0" Grid.Column="1" SelectedIndex="{Binding ScopeIndex}"
|
||||||
ToolTip="STL 無裝配結構;合併 = 所有可見零件進同一檔">
|
ToolTip="{loc:Loc Ui.StlExport.ScopeTooltip}">
|
||||||
<ComboBoxItem Content="全部可見檔案 → 合併成單一 STL" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.ScopeMerge}" />
|
||||||
<ComboBoxItem Content="每個可見檔案 → 各一個 STL" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.ScopePerFile}" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="格式" />
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{loc:Loc Ui.StlExport.Format}" />
|
||||||
<ComboBox Grid.Row="1" Grid.Column="1" SelectedIndex="{Binding FormatIndex}">
|
<ComboBox Grid.Row="1" Grid.Column="1" SelectedIndex="{Binding FormatIndex}">
|
||||||
<ComboBoxItem Content="Binary(建議,檔案小)" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.FormatBinary}" />
|
||||||
<ComboBoxItem Content="ASCII(文字可讀,檔案約 4 倍大)" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.FormatAscii}" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="單位縮放" />
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{loc:Loc Ui.StlExport.UnitScale}" />
|
||||||
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="{Binding UnitIndex}"
|
<ComboBox Grid.Row="2" Grid.Column="1" SelectedIndex="{Binding UnitIndex}"
|
||||||
ToolTip="STL 檔本身無單位;內部幾何為 mm,選 inch 會把座標 ÷25.4">
|
ToolTip="{loc:Loc Ui.StlExport.UnitTooltip}">
|
||||||
<ComboBoxItem Content="mm(1:1,預設)" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.UnitMillimeter}" />
|
||||||
<ComboBoxItem Content="inch(座標 ÷ 25.4)" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.UnitInch}" />
|
||||||
<ComboBoxItem Content="自訂縮放係數…" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.UnitCustom}" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CustomScaleText, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CustomScaleText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Margin="0,0,0,10" Padding="4,2"
|
Margin="0,0,0,10" Padding="4,2"
|
||||||
Visibility="{Binding IsCustomScale, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding IsCustomScale, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="座標乘上此係數(例:0.001 = mm→m)" />
|
ToolTip="{loc:Loc Ui.StlExport.CustomScaleTooltip}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="網格精度" />
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="{loc:Loc Ui.StlExport.MeshQuality}" />
|
||||||
<ComboBox Grid.Row="4" Grid.Column="1" SelectedIndex="{Binding QualityIndex}"
|
<ComboBox Grid.Row="4" Grid.Column="1" SelectedIndex="{Binding QualityIndex}"
|
||||||
IsEnabled="{Binding HasBrep}"
|
IsEnabled="{Binding HasBrep}"
|
||||||
ToolTip="精細會由 B-rep 重新三角化(僅 STEP 來源;STL/DXF 維持現有網格),大組件需要時間">
|
ToolTip="{loc:Loc Ui.StlExport.QualityTooltip}">
|
||||||
<ComboBoxItem Content="目前顯示網格(快,與畫面一致)" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.QualityCurrent}" />
|
||||||
<ComboBoxItem Content="精細 — 重新三角化,曲面更平滑(慢、檔案大)" />
|
<ComboBoxItem Content="{loc:Loc Ui.StlExport.QualityFine}" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<Border Grid.Row="5" Grid.ColumnSpan="2" Background="#F0F4F8" CornerRadius="4"
|
<Border Grid.Row="5" Grid.ColumnSpan="2" Background="#F0F4F8" CornerRadius="4"
|
||||||
@@ -78,8 +79,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Row="6" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Grid.Row="6" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button Content="取消" Width="80" Margin="0,0,8,0" IsCancel="True" />
|
<Button Content="{loc:Loc Ui.StlExport.Cancel}" Width="80" Margin="0,0,8,0" IsCancel="True" />
|
||||||
<Button Content="匯出…" Width="96" IsDefault="True"
|
<Button Content="{loc:Loc Ui.StlExport.Export}" Width="96" IsDefault="True"
|
||||||
IsEnabled="{Binding CanConfirm}" Click="Confirm_Click" />
|
IsEnabled="{Binding CanConfirm}" Click="Confirm_Click" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
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;
|
||||||
|
|
||||||
@@ -23,26 +24,28 @@ 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>
|
/// <summary>
|
||||||
@@ -60,14 +63,14 @@ public partial class MainViewModel
|
|||||||
.ToList();
|
.ToList();
|
||||||
if (geos.Count == 0)
|
if (geos.Count == 0)
|
||||||
{
|
{
|
||||||
StatusText = "沒有可匯出的 B-rep 幾何(STEP 匯出僅含 Solid/Shell;請勾選要匯出的 STEP 檔案)";
|
SetStatus(() => Get("Runtime.StepExportEmpty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dlg = new SaveFileDialog
|
var dlg = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Title = "匯出目前場景為 STEP(零件在目前對齊位置)",
|
Title = Get("Runtime.StepExportTitle"),
|
||||||
Filter = "STEP 檔案 (*.stp)|*.stp|STEP 檔案 (*.step)|*.step",
|
Filter = Get("Runtime.StepExportFilter"),
|
||||||
FileName = $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stp",
|
FileName = $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stp",
|
||||||
};
|
};
|
||||||
if (dlg.ShowDialog() != true) return;
|
if (dlg.ShowDialog() != true) return;
|
||||||
@@ -79,11 +82,14 @@ public partial class MainViewModel
|
|||||||
CADability.Model model = project.GetActiveModel();
|
CADability.Model model = project.GetActiveModel();
|
||||||
foreach (CADability.GeoObject.IGeoObject g in geos) model.Add(g);
|
foreach (CADability.GeoObject.IGeoObject g in geos) model.Add(g);
|
||||||
new CADability.ExportStep().WriteToFile(dlg.FileName, project);
|
new CADability.ExportStep().WriteToFile(dlg.FileName, project);
|
||||||
StatusText = $"已匯出 STEP({geos.Count} 個實體,目前位置)→ {Path.GetFileName(dlg.FileName)}";
|
int count = geos.Count;
|
||||||
|
string fileName = Path.GetFileName(dlg.FileName);
|
||||||
|
SetStatus(() => Format("Runtime.StepExported", count, fileName));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusText = $"STEP 匯出失敗:{ex.Message}";
|
string error = ex.Message;
|
||||||
|
SetStatus(() => Format("Runtime.StepExportFailed", error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +108,7 @@ public partial class MainViewModel
|
|||||||
.ToList();
|
.ToList();
|
||||||
if (groups.Count == 0)
|
if (groups.Count == 0)
|
||||||
{
|
{
|
||||||
StatusText = "沒有可匯出的網格(請勾選至少一個含實體網格的檔案)";
|
SetStatus(() => Get("Runtime.StlExportEmpty"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +123,8 @@ public partial class MainViewModel
|
|||||||
|
|
||||||
var dlg = new SaveFileDialog
|
var dlg = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Title = vm.PerFile ? "匯出 STL(每個檔案各一個,以此為基底檔名)" : "匯出 STL(合併單一檔)",
|
Title = Get(vm.PerFile ? "Runtime.StlExportPerFileTitle" : "Runtime.StlExportMergedTitle"),
|
||||||
Filter = "STL 檔案 (*.stl)|*.stl",
|
Filter = Get("Runtime.StlExportFilter"),
|
||||||
FileName = vm.PerFile
|
FileName = vm.PerFile
|
||||||
? $"stl_{DateTime.Now:yyyyMMdd_HHmmss}.stl"
|
? $"stl_{DateTime.Now:yyyyMMdd_HHmmss}.stl"
|
||||||
: $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stl",
|
: $"assembly_{DateTime.Now:yyyyMMdd_HHmmss}.stl",
|
||||||
@@ -134,9 +140,8 @@ public partial class MainViewModel
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
StatusText = vm.Quality == StlMeshQuality.Current
|
SetStatus(() => Get(vm.Quality == StlMeshQuality.Current
|
||||||
? "匯出 STL 中…"
|
? "Runtime.StlExporting" : "Runtime.StlRetriangulating"));
|
||||||
: "重新三角化 + 匯出 STL 中…(大組件需要時間)";
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
double factor = StlExportService.PrecisionFactor(vm.Quality);
|
double factor = StlExportService.PrecisionFactor(vm.Quality);
|
||||||
@@ -164,13 +169,16 @@ public partial class MainViewModel
|
|||||||
});
|
});
|
||||||
|
|
||||||
long total = written.Sum(w => (long)w.Triangles);
|
long total = written.Sum(w => (long)w.Triangles);
|
||||||
StatusText = written.Count == 1
|
int fileCount = written.Count;
|
||||||
? $"已匯出 STL({total:N0} 三角形)→ {Path.GetFileName(written[0].File)}"
|
string destination = fileCount == 1 ? Path.GetFileName(written[0].File) : Path.GetDirectoryName(dlg.FileName)!;
|
||||||
: $"已匯出 {written.Count} 個 STL(共 {total:N0} 三角形)→ {Path.GetDirectoryName(dlg.FileName)}";
|
SetStatus(() => fileCount == 1
|
||||||
|
? Format("Runtime.StlExportedOne", total, destination)
|
||||||
|
: Format("Runtime.StlExportedMany", fileCount, total, destination));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StatusText = $"STL 匯出失敗:{ex.Message}";
|
string error = ex.Message;
|
||||||
|
SetStatus(() => Format("Runtime.StlExportFailed", error));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -221,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;
|
||||||
@@ -236,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;
|
||||||
|
|
||||||
@@ -90,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;
|
||||||
}
|
}
|
||||||
@@ -123,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()
|
||||||
@@ -211,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;
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ public partial class MainViewModel
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +47,8 @@ public partial class MainViewModel
|
|||||||
{
|
{
|
||||||
pairs++;
|
pairs++;
|
||||||
string nameA = visibleRoots[i].Name, nameB = visibleRoots[j].Name;
|
string nameA = visibleRoots[i].Name, nameB = visibleRoots[j].Name;
|
||||||
StatusText = $"干涉檢查中(第 {pairs} 組):{nameA} ⟷ {nameB} …";
|
int currentPair = pairs;
|
||||||
|
SetStatus(() => Format("Runtime.InterferenceChecking", currentPair, nameA, nameB));
|
||||||
var a = meshes[visibleRoots[i]];
|
var a = meshes[visibleRoots[i]];
|
||||||
var b = meshes[visibleRoots[j]];
|
var b = meshes[visibleRoots[j]];
|
||||||
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
InterferenceResult result = await Task.Run(() => InterferenceService.Check(a, b));
|
||||||
@@ -53,11 +56,15 @@ public partial class MainViewModel
|
|||||||
if (result.Intersects) intersecting++;
|
if (result.Intersects) intersecting++;
|
||||||
}
|
}
|
||||||
if (pairs > 1)
|
if (pairs > 1)
|
||||||
StatusText = $"干涉檢查完成:{pairs} 組配對、{intersecting} 組相交";
|
{
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@@ -78,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); }
|
||||||
@@ -88,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
|
||||||
@@ -107,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 });
|
||||||
@@ -122,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ public partial class MainViewModel
|
|||||||
if (value == CustomAxisIndex)
|
if (value == CustomAxisIndex)
|
||||||
{
|
{
|
||||||
if (_customNormal is null)
|
if (_customNormal is null)
|
||||||
StatusText = "3點剖面:請在模型上點 3 個點定義剖切平面(Esc 取消)";
|
SetStatus(() => Get("Runtime.SectionPickPrompt"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -98,13 +99,14 @@ public partial class MainViewModel
|
|||||||
}
|
}
|
||||||
if (hit is null)
|
if (hit is null)
|
||||||
{
|
{
|
||||||
StatusText = $"3點剖面:未命中模型表面(已選 {_sectionPicks.Count}/3 點,Esc 取消)";
|
int selected = _sectionPicks.Count;
|
||||||
|
SetStatus(() => Format("Runtime.SectionNoHit", selected));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (_sectionPicks.Count == 2 &&
|
if (_sectionPicks.Count == 2 &&
|
||||||
Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], hit.Value - _sectionPicks[0]).LengthSquared < 1e-12)
|
Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], hit.Value - _sectionPicks[0]).LengthSquared < 1e-12)
|
||||||
{
|
{
|
||||||
StatusText = "3點剖面:三點共線,請改選不共線的第 3 點";
|
SetStatus(() => Get("Runtime.SectionCollinear"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +118,8 @@ public partial class MainViewModel
|
|||||||
|
|
||||||
if (_sectionPicks.Count < 3)
|
if (_sectionPicks.Count < 3)
|
||||||
{
|
{
|
||||||
StatusText = $"3點剖面:已選 {_sectionPicks.Count}/3 點";
|
int selected = _sectionPicks.Count;
|
||||||
|
SetStatus(() => Format("Runtime.SectionPicked", selected));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,12 +127,12 @@ public partial class MainViewModel
|
|||||||
ClearSectionPicks();
|
ClearSectionPicks();
|
||||||
if (n.LengthSquared < 1e-12)
|
if (n.LengthSquared < 1e-12)
|
||||||
{
|
{
|
||||||
StatusText = "3點剖面:三點退化(共線),請重新點選";
|
SetStatus(() => Get("Runtime.SectionDegenerate"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
n.Normalize();
|
n.Normalize();
|
||||||
_customNormal = n;
|
_customNormal = n;
|
||||||
StatusText = $"3點剖面:平面已定義(法向 {n.X:F2}, {n.Y:F2}, {n.Z:F2})— 用位置滑桿掃剖面";
|
SetStatus(() => Format("Runtime.SectionDefined", n.X, n.Y, n.Z));
|
||||||
ApplySection();
|
ApplySection();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -142,7 +145,7 @@ public partial class MainViewModel
|
|||||||
ClearSectionPicks();
|
ClearSectionPicks();
|
||||||
if (SectionAxisIndex == CustomAxisIndex && _customNormal is null)
|
if (SectionAxisIndex == CustomAxisIndex && _customNormal is null)
|
||||||
SectionAxisIndex = 0; // 回 X 軸
|
SectionAxisIndex = 0; // 回 X 軸
|
||||||
if (!quiet) StatusText = "已取消 3 點剖面定義";
|
if (!quiet) SetStatus(() => Get("Runtime.SectionCancelled"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +180,7 @@ public partial class MainViewModel
|
|||||||
_sectionPlaneVisual = null;
|
_sectionPlaneVisual = null;
|
||||||
}
|
}
|
||||||
ApplyRenderMode(); // 剖面關閉 → 瀏覽模式可切回合併網格
|
ApplyRenderMode(); // 剖面關閉 → 瀏覽模式可切回合併網格
|
||||||
StatusText = "剖面已關閉";
|
SetStatus(() => Get("Runtime.SectionClosed"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,8 +262,12 @@ public partial class MainViewModel
|
|||||||
|
|
||||||
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
|
||||||
UpdateSectionPlaneVisual(planePoint, axis, size);
|
UpdateSectionPlaneVisual(planePoint, axis, size);
|
||||||
string axisName = SectionAxisIndex == CustomAxisIndex ? "自訂平面" : $"{"XYZ"[SectionAxisIndex]} 軸";
|
int axisIndex = SectionAxisIndex;
|
||||||
StatusText = $"剖面:{axisName} {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : "");
|
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
|
||||||
@@ -279,7 +286,7 @@ 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),
|
||||||
2 => (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), // 自訂平面
|
_ => (AnyPerpendicular(axis), size.Length * 0.8, size.Length * 0.8),
|
||||||
};
|
};
|
||||||
lenDir.Normalize();
|
lenDir.Normalize();
|
||||||
_sectionPlaneVisual = new RectangleVisual3D
|
_sectionPlaneVisual = new RectangleVisual3D
|
||||||
|
|||||||
@@ -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,7 +58,24 @@ 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))]
|
||||||
@@ -103,6 +121,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
public void SaveSettingsInto(AppSettings s)
|
public void SaveSettingsInto(AppSettings s)
|
||||||
{
|
{
|
||||||
s.UseInch = UseInch;
|
s.UseInch = UseInch;
|
||||||
|
s.Language = SelectedLanguage;
|
||||||
s.RecentFiles = RecentFiles.ToList();
|
s.RecentFiles = RecentFiles.ToList();
|
||||||
s.QuickBarKeys = QuickBar.ToKeys();
|
s.QuickBarKeys = QuickBar.ToKeys();
|
||||||
}
|
}
|
||||||
@@ -176,9 +195,8 @@ 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)
|
||||||
@@ -194,7 +212,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
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
|
||||||
@@ -202,11 +220,11 @@ 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));
|
||||||
@@ -217,8 +235,9 @@ public partial class MainViewModel : ObservableObject
|
|||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,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) : ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,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,
|
||||||
@@ -485,7 +507,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
cam.LookDirection = dir * dist; // 目標點 = Position + LookDirection = 場景中心
|
cam.LookDirection = dir * dist; // 目標點 = Position + LookDirection = 場景中心
|
||||||
cam.UpDirection = up;
|
cam.UpDirection = up;
|
||||||
_viewport.ZoomExtents(400);
|
_viewport.ZoomExtents(400);
|
||||||
StatusText = $"視圖:{name}";
|
SetStatus(() => Format("Runtime.View", Get($"Runtime.View{name}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>正交投影切換(平行投影,量尺寸不受透視變形;Helix 保持視角換相機型別)</summary>
|
/// <summary>正交投影切換(平行投影,量尺寸不受透視變形;Helix 保持視角換相機型別)</summary>
|
||||||
@@ -496,7 +518,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (_viewport is null) return;
|
if (_viewport is null) return;
|
||||||
_viewport.Orthographic = value;
|
_viewport.Orthographic = value;
|
||||||
StatusText = value ? "正交投影(平行投影,適合尺寸確認)" : "透視投影";
|
SetStatus(() => Get(value ? "Runtime.Orthographic" : "Runtime.Perspective"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -526,7 +548,8 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 顯示控制 / 體積(樹右鍵選單)────────────────────────────
|
// ─── 顯示控制 / 體積(樹右鍵選單)────────────────────────────
|
||||||
@@ -537,7 +560,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, false);
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, false);
|
||||||
SetVisibleRecursive(node, true);
|
SetVisibleRecursive(node, true);
|
||||||
StatusText = $"只顯示「{node.Name}」(樹右鍵 → 全部顯示 復原)";
|
SetStatus(() => Format("Runtime.Isolated", node.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -545,14 +568,14 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
foreach (ModelNodeViewModel l in Roots.SelectMany(r => r.Leaves()))
|
foreach (ModelNodeViewModel l in Roots.SelectMany(r => r.Leaves()))
|
||||||
l.IsVisible = !l.IsVisible;
|
l.IsVisible = !l.IsVisible;
|
||||||
StatusText = "已反轉顯示";
|
SetStatus(() => Get("Runtime.VisibilityInverted"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ShowAll()
|
private void ShowAll()
|
||||||
{
|
{
|
||||||
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, true);
|
foreach (ModelNodeViewModel r in Roots) SetVisibleRecursive(r, true);
|
||||||
StatusText = "已全部顯示";
|
SetStatus(() => Get("Runtime.AllVisible"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>逐節點顯式遞迴設定可見性。不能只設 root(setter 同值不觸發 cascade,
|
/// <summary>逐節點顯式遞迴設定可見性。不能只設 root(setter 同值不觸發 cascade,
|
||||||
@@ -576,14 +599,14 @@ public partial class MainViewModel : ObservableObject
|
|||||||
.ToList();
|
.ToList();
|
||||||
if (meshes.Count == 0)
|
if (meshes.Count == 0)
|
||||||
{
|
{
|
||||||
StatusText = $"「{node.Name}」沒有可計算的面網格(線架構無體積)";
|
SetStatus(() => Format("Runtime.VolumeNoMesh", node.Name));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(double vol, Point3D centroid, bool reliable) = MeasurementService.MeshVolume(meshes);
|
(double vol, Point3D centroid, bool reliable) = MeasurementService.MeshVolume(meshes);
|
||||||
if (!reliable)
|
if (!reliable)
|
||||||
{
|
{
|
||||||
StatusText = $"「{node.Name}」非封閉網格(開放殼),無法可靠計算體積";
|
SetStatus(() => Format("Runtime.VolumeOpenMesh", node.Name));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,9 +619,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
Kind = MeasureMode.Volume,
|
Kind = MeasureMode.Volume,
|
||||||
TitleFor = u => $"{label} {name} V ≈ {Units.V(vol, u)}",
|
TitleFor = u => $"{label} {name} V ≈ {Units.V(vol, u)}",
|
||||||
DetailFor = u => $"體積 ≈ {Units.V(vol, u)}(網格近似)\n質心 {Units.P(centroid, u)}\n" +
|
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"),
|
||||||
$"外形(AABB){Units.C(b.SizeX, u)} × {Units.C(b.SizeY, u)} × {Units.C(b.SizeZ, u)} " +
|
|
||||||
(u == UnitSystem.Millimeter ? "mm" : "in"),
|
|
||||||
};
|
};
|
||||||
m.Overlays.Add(new SphereVisual3D { Center = centroid, Radius = markerR, Fill = Brushes.OrangeRed });
|
m.Overlays.Add(new SphereVisual3D { Center = centroid, Radius = markerR, Fill = Brushes.OrangeRed });
|
||||||
var volLabel = new BillboardTextVisual3D
|
var volLabel = new BillboardTextVisual3D
|
||||||
@@ -621,28 +642,29 @@ 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>
|
/// <summary>Esc(MainWindow 轉發):先取消進行中的多段量測(保留模式),再按退回瀏覽模式;再退操作器</summary>
|
||||||
@@ -654,7 +676,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
if (hadPending)
|
if (hadPending)
|
||||||
{
|
{
|
||||||
CancelPending();
|
CancelPending();
|
||||||
StatusText = "已取消目前量測起點(再按 Esc 退出量測模式)";
|
SetStatus(() => Get("Runtime.MeasurementStartCancelled"));
|
||||||
}
|
}
|
||||||
else if (CurrentMode != MeasureMode.None)
|
else if (CurrentMode != MeasureMode.None)
|
||||||
{
|
{
|
||||||
@@ -718,7 +740,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
if (fi is null)
|
if (fi is null)
|
||||||
{
|
{
|
||||||
StatusText = "未命中模型表面";
|
SetStatus(() => Get("Runtime.NoSurfaceHit"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,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
|
||||||
{
|
{
|
||||||
@@ -791,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;
|
||||||
}
|
}
|
||||||
@@ -803,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;
|
||||||
}
|
}
|
||||||
@@ -814,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
|
||||||
{
|
{
|
||||||
@@ -831,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
|
||||||
{
|
{
|
||||||
@@ -850,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
|
||||||
{
|
{
|
||||||
@@ -867,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;
|
||||||
}
|
}
|
||||||
@@ -876,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,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;
|
||||||
}
|
}
|
||||||
@@ -961,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]
|
||||||
@@ -981,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()
|
||||||
@@ -1024,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 面吃滿多核)
|
||||||
@@ -1110,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;
|
||||||
@@ -1126,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; }
|
||||||
@@ -83,11 +92,20 @@ 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) +
|
||||||
(Bounds.IsEmpty ? "" : $"\n外形 {SizeText}");
|
(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 可見性)──
|
// ── 樹搜尋/過濾(只影響樹面板顯示,不影響 3D 可見性)──
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -111,8 +129,8 @@ public partial class ModelNodeViewModel : ObservableObject
|
|||||||
|
|
||||||
// ── 視覺物件 ──
|
// ── 視覺物件 ──
|
||||||
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; }
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using STPViewer.Services;
|
||||||
|
|
||||||
namespace STPViewer.ViewModels;
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
@@ -10,7 +11,8 @@ namespace STPViewer.ViewModels;
|
|||||||
public partial class QuickBarItemViewModel : ObservableObject
|
public partial class QuickBarItemViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
public string Key { get; }
|
public string Key { get; }
|
||||||
public string Label { get; }
|
public string Label => LocalizationService.Get(_labelKey);
|
||||||
|
private readonly string _labelKey;
|
||||||
public bool Default { get; }
|
public bool Default { get; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -19,10 +21,12 @@ public partial class QuickBarItemViewModel : ObservableObject
|
|||||||
public QuickBarItemViewModel(string key, string label, bool @default)
|
public QuickBarItemViewModel(string key, string label, bool @default)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
Label = label;
|
_labelKey = label;
|
||||||
Default = @default;
|
Default = @default;
|
||||||
isChecked = @default;
|
isChecked = @default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RefreshText() => OnPropertyChanged(nameof(Label));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -35,36 +39,37 @@ public partial class QuickBarViewModel : ObservableObject
|
|||||||
/// <summary>註冊表 = 快速列順序 + 選單顯示名 + 是否預設顯示(一般 user 常用)</summary>
|
/// <summary>註冊表 = 快速列順序 + 選單顯示名 + 是否預設顯示(一般 user 常用)</summary>
|
||||||
private static readonly (string Key, string Label, bool Default)[] Registry =
|
private static readonly (string Key, string Label, bool Default)[] Registry =
|
||||||
{
|
{
|
||||||
("Import", "📂 匯入", true),
|
("Import", "QuickBar.Import", true),
|
||||||
("Recent", "▾ 最近檔案", true),
|
("Recent", "QuickBar.Recent", true),
|
||||||
("ZoomAll", "🔍 全覽", true),
|
("ZoomAll", "QuickBar.ZoomAll", true),
|
||||||
("ViewIso", "視圖:等角", true),
|
("ViewIso", "QuickBar.ViewIso", true),
|
||||||
("ViewFront", "視圖:前", false),
|
("ViewFront", "QuickBar.ViewFront", false),
|
||||||
("ViewTop", "視圖:上", false),
|
("ViewTop", "QuickBar.ViewTop", false),
|
||||||
("ViewRight", "視圖:右", false),
|
("ViewRight", "QuickBar.ViewRight", false),
|
||||||
("Ortho", "正交投影", true),
|
("Ortho", "QuickBar.Ortho", true),
|
||||||
("Point", "📍 點", true),
|
("Point", "QuickBar.Point", true),
|
||||||
("Distance", "📏 距離", true),
|
("Distance", "QuickBar.Distance", true),
|
||||||
("Edge", "📐 邊", false),
|
("Edge", "QuickBar.Edge", false),
|
||||||
("Face", "⬛ 面", false),
|
("Face", "QuickBar.Face", false),
|
||||||
("Circle", "⭕ 圓", true),
|
("Circle", "QuickBar.Circle", true),
|
||||||
("Angle", "∠ 角度", true),
|
("Angle", "QuickBar.Angle", true),
|
||||||
("FaceDistance", "⇔ 面距", false),
|
("FaceDistance", "QuickBar.FaceDistance", false),
|
||||||
("Align", "🎯 兩點對齊", false),
|
("Align", "QuickBar.Align", false),
|
||||||
("Align3", "🎯 三點對齊", false),
|
("Align3", "QuickBar.Align3", false),
|
||||||
("RotateX", "旋轉 ↻X", false),
|
("RotateX", "QuickBar.RotateX", false),
|
||||||
("RotateY", "旋轉 ↻Y", false),
|
("RotateY", "QuickBar.RotateY", false),
|
||||||
("RotateZ", "旋轉 ↻Z", false),
|
("RotateZ", "QuickBar.RotateZ", false),
|
||||||
("Drag", "🖐 拖曳", false),
|
("Drag", "QuickBar.Drag", false),
|
||||||
("Gizmo", "⊹ 操作器", false),
|
("Gizmo", "QuickBar.Gizmo", false),
|
||||||
("Interference", "🧩 干涉", true),
|
("Interference", "QuickBar.Interference", true),
|
||||||
("Section", "✂ 剖面", true),
|
("Section", "QuickBar.Section", true),
|
||||||
("Clear", "🧹 清除量測", true),
|
("Clear", "QuickBar.Clear", true),
|
||||||
("Unit", "mm ⇄ in", true),
|
("Unit", "QuickBar.Unit", true),
|
||||||
("ExportCsv", "💾 匯出 CSV", false),
|
("ExportCsv", "QuickBar.ExportCsv", false),
|
||||||
("Screenshot", "📷 截圖", false),
|
("Screenshot", "QuickBar.Screenshot", false),
|
||||||
("ExportStep", "📤 匯出 STEP", false),
|
("ExportStep", "QuickBar.ExportStep", false),
|
||||||
("ExportStl", "📐 匯出 STL", false),
|
("ExportStl", "QuickBar.ExportStl", false),
|
||||||
|
("Language", "QuickBar.Language", true),
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Dictionary<string, QuickBarItemViewModel> _byKey;
|
private readonly Dictionary<string, QuickBarItemViewModel> _byKey;
|
||||||
@@ -94,6 +99,12 @@ public partial class QuickBarViewModel : ObservableObject
|
|||||||
public List<string> ToKeys() =>
|
public List<string> ToKeys() =>
|
||||||
Items.Where(i => i.IsChecked).Select(i => i.Key).ToList();
|
Items.Where(i => i.IsChecked).Select(i => i.Key).ToList();
|
||||||
|
|
||||||
|
public void RefreshText()
|
||||||
|
{
|
||||||
|
foreach (QuickBarItemViewModel item in Items)
|
||||||
|
item.RefreshText();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ResetDefaults()
|
private void ResetDefaults()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ public partial class StlExportViewModel : ObservableObject
|
|||||||
long est = StlExportService.EstimateBytes(TriangleCount, Ascii);
|
long est = StlExportService.EstimateBytes(TriangleCount, Ascii);
|
||||||
string size = StlExportService.FormatBytes(est);
|
string size = StlExportService.FormatBytes(est);
|
||||||
string basis = QualityIndex == 0
|
string basis = QualityIndex == 0
|
||||||
? $"預估 {size}"
|
? LocalizationService.Format("Ui.StlExport.EstimatedSize", size)
|
||||||
: $"以目前網格預估 {size}(實際依重新三角化結果)";
|
: LocalizationService.Format("Ui.StlExport.EstimatedSizeFine", size);
|
||||||
return $"{FileCount} 個可見檔案 · {TriangleCount:N0} 三角形 · {basis}";
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,26 +20,26 @@ public static class ExportTest
|
|||||||
Console.WriteLine($"=== ExportTest: {IOPath.GetFileName(sourcePath)} → {IOPath.GetFileName(outPath)} ===");
|
Console.WriteLine($"=== ExportTest: {IOPath.GetFileName(sourcePath)} → {IOPath.GetFileName(outPath)} ===");
|
||||||
ImportedFileData data = new StepImportService().Import(sourcePath);
|
ImportedFileData data = new StepImportService().Import(sourcePath);
|
||||||
var geos = Collect(data.Root).Where(g => g is Solid or Shell).ToList();
|
var geos = Collect(data.Root).Where(g => g is Solid or Shell).ToList();
|
||||||
Console.WriteLine($" 來源 Solid/Shell 物件 = {geos.Count}(Solids = {data.Root.SolidCount})");
|
Console.WriteLine($" Source Solid/Shell objects = {geos.Count} (solids = {data.Root.SolidCount})");
|
||||||
|
|
||||||
Project project = Project.CreateSimpleProject();
|
Project project = Project.CreateSimpleProject();
|
||||||
Model model = project.GetActiveModel();
|
Model model = project.GetActiveModel();
|
||||||
foreach (IGeoObject g in geos) model.Add(g);
|
foreach (IGeoObject g in geos) model.Add(g);
|
||||||
new ExportStep().WriteToFile(outPath, project);
|
new ExportStep().WriteToFile(outPath, project);
|
||||||
var fi = new FileInfo(outPath);
|
var fi = new FileInfo(outPath);
|
||||||
Console.WriteLine($" 已寫出({fi.Length:N0} bytes)");
|
Console.WriteLine($" Written ({fi.Length:N0} bytes)");
|
||||||
if (fi.Length < 100)
|
if (fi.Length < 100)
|
||||||
{
|
{
|
||||||
Console.WriteLine("ExportTest: FAIL(輸出檔近乎空白)");
|
Console.WriteLine("ExportTest: FAIL (output is nearly empty)");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportedFileData back = new StepImportService().Import(outPath);
|
ImportedFileData back = new StepImportService().Import(outPath);
|
||||||
Console.WriteLine($" 回讀 Solids = {back.Root.SolidCount}, Faces = {back.Root.FaceCount}");
|
Console.WriteLine($" Reimported solids = {back.Root.SolidCount}, faces = {back.Root.FaceCount}");
|
||||||
bool ok = back.Root.SolidCount == data.Root.SolidCount && back.Root.FaceCount > 0;
|
bool ok = back.Root.SolidCount == data.Root.SolidCount && back.Root.FaceCount > 0;
|
||||||
Console.WriteLine(ok
|
Console.WriteLine(ok
|
||||||
? "ExportTest: PASS(實體數一致)"
|
? "ExportTest: PASS (solid counts match)"
|
||||||
: $"ExportTest: FAIL(來源 Solids {data.Root.SolidCount} ↔ 回讀 {back.Root.SolidCount})");
|
: $"ExportTest: FAIL (source solids {data.Root.SolidCount} vs reimported {back.Root.SolidCount})");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,6 @@ internal static class MakeStl
|
|||||||
WriteTri(bw, x0, y0, Z(x, y), x1, y1, Z(x + 1, y + 1), x0, y1, Z(x, y + 1));
|
WriteTri(bw, x0, y0, Z(x, y), x1, y1, Z(x + 1, y + 1), x0, y1, Z(x, y + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"寫出 {tris:N0} 三角形 → {path} ({new FileInfo(path).Length:N0} bytes)");
|
Console.WriteLine($"Wrote {tris:N0} triangles to {path} ({new FileInfo(path).Length:N0} bytes)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// 無 UI smoke test:驗證 CAD 檔解析 → 三角化 → 裝配樹 整條匯入管線
|
// 無 UI smoke test:驗證 CAD 檔解析 → 三角化 → 裝配樹 整條匯入管線
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +38,9 @@ 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]);
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ if (args[0] == "--export-test")
|
|||||||
if (args[0] == "--stl-export-test")
|
if (args[0] == "--stl-export-test")
|
||||||
return SmokeTest.StlExportTest.Run(args.Length > 1 ? args[1] : null) == 0 ? 0 : 2;
|
return SmokeTest.StlExportTest.Run(args.Length > 1 ? args[1] : null) == 0 ? 0 : 2;
|
||||||
|
|
||||||
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
|
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)
|
||||||
@@ -64,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)
|
||||||
@@ -81,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ public static class StlExportTest
|
|||||||
// 1) binary:三角形數 + 縮放後座標範圍
|
// 1) binary:三角形數 + 縮放後座標範圍
|
||||||
{
|
{
|
||||||
string f = IOPath.Combine(dir, "cube_bin.stl");
|
string f = IOPath.Combine(dir, "cube_bin.stl");
|
||||||
double scale = 1.0 / 25.4; // mm → inch
|
double scale = 1.0 / 25.4; // mm to inches.
|
||||||
int written = StlExportService.Write(f, new[] { cube }, ascii: false, scale);
|
int written = StlExportService.Write(f, new[] { cube }, ascii: false, scale);
|
||||||
(uint count, float min, float max) = ReadBinary(f);
|
(uint count, float min, float max) = ReadBinary(f);
|
||||||
bool ok = written == 12 && count == 12
|
bool ok = written == 12 && count == 12
|
||||||
&& Math.Abs(min - 0f) < 1e-5 && Math.Abs(max - 10f / 25.4f) < 1e-4;
|
&& Math.Abs(min - 0f) < 1e-5 && Math.Abs(max - 10f / 25.4f) < 1e-4;
|
||||||
Report(ref failed, ok, $"binary 寫出/回讀 12 三角形、inch 縮放範圍 [{min:F4}, {max:F4}]");
|
Report(ref failed, ok, $"binary round trip: 12 triangles, inch range [{min:F4}, {max:F4}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) ASCII:facet 數 + 結構
|
// 2) ASCII:facet 數 + 結構
|
||||||
@@ -46,7 +46,7 @@ public static class StlExportTest
|
|||||||
bool ok = written == 12 && facets == 12
|
bool ok = written == 12 && facets == 12
|
||||||
&& text.StartsWith("solid ", StringComparison.Ordinal)
|
&& text.StartsWith("solid ", StringComparison.Ordinal)
|
||||||
&& text.Contains("endsolid");
|
&& text.Contains("endsolid");
|
||||||
Report(ref failed, ok, $"ASCII 結構完整、facet = {facets}");
|
Report(ref failed, ok, $"ASCII structure valid, facets = {facets}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) 退化三角形濾除:加一個零面積三角形 → 仍是 12
|
// 3) 退化三角形濾除:加一個零面積三角形 → 仍是 12
|
||||||
@@ -64,7 +64,7 @@ public static class StlExportTest
|
|||||||
int written = StlExportService.Write(f, new[] { cube, degenerate }, ascii: false, scale: 1.0);
|
int written = StlExportService.Write(f, new[] { cube, degenerate }, ascii: false, scale: 1.0);
|
||||||
(uint count, _, _) = ReadBinary(f);
|
(uint count, _, _) = ReadBinary(f);
|
||||||
bool ok = written == 12 && count == 12;
|
bool ok = written == 12 && count == 12;
|
||||||
Report(ref failed, ok, $"退化三角形濾除(寫出 {written})");
|
Report(ref failed, ok, $"degenerate triangle filtered ({written} written)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4)(可選)真實 STEP:B-rep 精細重新三角化 → 寫 binary STL 驗證
|
// 4)(可選)真實 STEP:B-rep 精細重新三角化 → 寫 binary STL 驗證
|
||||||
@@ -74,7 +74,7 @@ public static class StlExportTest
|
|||||||
var data = new StepImportService().Import(stepPath);
|
var data = new StepImportService().Import(stepPath);
|
||||||
var geos = Collect(data.Root).ToList();
|
var geos = Collect(data.Root).ToList();
|
||||||
int importTris = data.Root.TriangleCount;
|
int importTris = data.Root.TriangleCount;
|
||||||
Console.WriteLine($" 來源 {IOPath.GetFileName(stepPath)}:{geos.Count} 個 B-rep 物件、匯入網格 {importTris:N0} 三角形");
|
Console.WriteLine($" Source {IOPath.GetFileName(stepPath)}: {geos.Count} B-rep objects, {importTris:N0} imported triangles");
|
||||||
|
|
||||||
var meshes = StlExportService.Tessellate(
|
var meshes = StlExportService.Tessellate(
|
||||||
geos, StlExportService.PrecisionFactor(StlMeshQuality.Fine));
|
geos, StlExportService.PrecisionFactor(StlMeshQuality.Fine));
|
||||||
@@ -82,13 +82,13 @@ public static class StlExportTest
|
|||||||
int written = StlExportService.Write(f, meshes, ascii: false, scale: 1.0);
|
int written = StlExportService.Write(f, meshes, ascii: false, scale: 1.0);
|
||||||
(uint count, _, _) = ReadBinary(f);
|
(uint count, _, _) = ReadBinary(f);
|
||||||
Report(ref failed, meshes.Count > 0 && written > 0 && count == written,
|
Report(ref failed, meshes.Count > 0 && written > 0 && count == written,
|
||||||
$"精細重新三角化 {meshes.Count} 面 → {written:N0} 三角形");
|
$"fine retessellation: {meshes.Count} faces, {written:N0} triangles");
|
||||||
// CADability 三角形數對精度非單調(0.4× 反而比 1× 少),Fine=0.15× 實測明顯更細
|
// CADability 三角形數對精度非單調(0.4× 反而比 1× 少),Fine=0.15× 實測明顯更細
|
||||||
Report(ref failed, written > importTris,
|
Report(ref failed, written > importTris,
|
||||||
$"精細({written:N0})> 匯入預設({importTris:N0})");
|
$"fine ({written:N0}) > imported default ({importTris:N0})");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(failed == 0 ? "StlExportTest: PASS" : $"StlExportTest: FAIL({failed} 項)");
|
Console.WriteLine(failed == 0 ? "StlExportTest: PASS" : $"StlExportTest: FAIL ({failed} checks)");
|
||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ public static class StlExportTest
|
|||||||
using var br = new BinaryReader(File.OpenRead(path));
|
using var br = new BinaryReader(File.OpenRead(path));
|
||||||
byte[] header = br.ReadBytes(80);
|
byte[] header = br.ReadBytes(80);
|
||||||
if (Encoding.ASCII.GetString(header, 0, 5) == "solid")
|
if (Encoding.ASCII.GetString(header, 0, 5) == "solid")
|
||||||
throw new InvalidDataException("binary STL header 不可以 solid 開頭");
|
throw new InvalidDataException("Binary STL header must not begin with solid");
|
||||||
uint count = br.ReadUInt32();
|
uint count = br.ReadUInt32();
|
||||||
float min = float.MaxValue, max = float.MinValue;
|
float min = float.MaxValue, max = float.MinValue;
|
||||||
for (uint t = 0; t < count; t++)
|
for (uint t = 0; t < count; t++)
|
||||||
|
|||||||
Reference in New Issue
Block a user