Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89ed1270a8 | ||
|
|
060ca4ecd0 | ||
|
|
45b27d51cb | ||
|
|
3d124782d5 | ||
|
|
e4485667e2 |
@@ -108,6 +108,7 @@ STPViewer/
|
|||||||
│ └── UnitSystem.cs # mm/inch + Units 格式化
|
│ └── UnitSystem.cs # mm/inch + Units 格式化
|
||||||
│
|
│
|
||||||
├── StlExportDialog.xaml / .cs # STL 匯出參數對話框(範圍/格式/單位/精度 + 預估,確認再匯出)
|
├── StlExportDialog.xaml / .cs # STL 匯出參數對話框(範圍/格式/單位/精度 + 預估,確認再匯出)
|
||||||
|
├── AboutDialog.xaml / .cs # 關於視窗(程式/開發者/技術棧 + 使用者版 changelog)
|
||||||
│
|
│
|
||||||
├── Services/
|
├── Services/
|
||||||
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
│ ├── StepImportService.cs # STEP/STL/DXF 讀檔 + 三角化 + 裝配樹
|
||||||
@@ -463,6 +464,31 @@ WPF retained-mode 每幀重走 visual tree → frame rate 崩。瀏覽模式因
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Development Phases — 第八輪(UI 重整 v0.7.0)
|
||||||
|
|
||||||
|
### Phase 16 — 選單列 + 可自訂快速列(工作量:M)
|
||||||
|
|
||||||
|
**問題:** 功能累積後單一工具列 30+ 顆按鈕擠在一排,視窗不夠寬就溢出到「▾」,難找難按。
|
||||||
|
|
||||||
|
- [x] **選單列**(Menu):全部功能分類下拉 — 檔案(匯入/MRU/四種匯出)、視圖(全覽/標準視圖/正交/單位)、
|
||||||
|
量測(7 種模式 checkable + 快速鍵提示 + 清除)、變換(對齊/三點/拖曳/操作器/旋轉XYZ)、
|
||||||
|
工具(干涉/剖面)、快速列(自訂按鈕勾選 + 恢復預設)
|
||||||
|
- [x] **快速工具列**(`QuickBarViewModel`):只顯示使用者勾選的常用按鈕;
|
||||||
|
註冊表定義全部按鈕(key/選單名/預設值),XAML 以索引子綁定
|
||||||
|
`Visibility="{Binding QuickBar[Key].IsChecked, Converter=BoolToVis}"`(按鈕本體與綁定保持靜態)
|
||||||
|
- [x] 預設常用:匯入、最近、全覽、等角、正交、點、距離、圓、角度、干涉、剖面、清除、mm⇄in;
|
||||||
|
其餘(邊/面/面距/對齊/三點/旋轉/拖曳/操作器/四種匯出/前上右視圖)預設收進選單
|
||||||
|
- [x] 勾選清單隨 settings.json(`QuickBarKeys`)保存;null/新按鈕用註冊表預設值
|
||||||
|
- [x] **剖面參數列**獨立成第三列,只在 `SectionEnabled` 時顯示(含「✕ 關閉」);
|
||||||
|
平常整條隱藏,不再常駐佔位
|
||||||
|
- [x] **關於視窗**(`AboutDialog`,選單「說明 → 關於 STPViewer」)— 程式資訊/開發者/技術棧 +
|
||||||
|
給一般使用者看的版本紀錄(樣式參考 ETTerms AboutView);發新版在 Changelog 陣列頂端加一筆
|
||||||
|
|
||||||
|
**驗收:** 選單所有功能可用(checkable 與工具列 toggle 同步);快速列勾選即時增減、重開保留;
|
||||||
|
既有快速鍵 P/D/E/F/C/A/M + Esc 不受影響
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Future Extensions(下一輪)
|
## Future Extensions(下一輪)
|
||||||
|
|
||||||
- **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證),
|
- **大檔重開快取** — 匯入成功後把三角網格 + 裝配樹序列化成 sidecar 快取檔(以來源檔 hash 驗證),
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
(量孔對孔 pitch 靠這個)。不要把圓心吸附拿掉或改成永遠優先頂點
|
(量孔對孔 pitch 靠這個)。不要把圓心吸附拿掉或改成永遠優先頂點
|
||||||
- 鍵盤快速鍵在 `MainWindow.Window_PreviewKeyDown`:**焦點在 TextBoxBase/ComboBox 時直接 return**
|
- 鍵盤快速鍵在 `MainWindow.Window_PreviewKeyDown`:**焦點在 TextBoxBase/ComboBox 時直接 return**
|
||||||
(樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理
|
(樹搜尋框、剖面數值框要能打字),新增輸入控件不用再各自處理
|
||||||
|
- UI 三層(v0.7.0):**選單列**(全部功能,分類下拉)+ **快速列**(`QuickBarViewModel` 註冊表,
|
||||||
|
使用者勾選常用按鈕,XAML 索引子綁定 `QuickBar[Key].IsChecked` 控 Visibility,按鈕本體維持靜態綁定)+
|
||||||
|
**剖面參數列**(只在 SectionEnabled 顯示)。新增功能要三處都接:選單分類、快速列註冊表
|
||||||
|
(含預設值)+ 對應 XAML 按鈕;勾選清單存 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)`;
|
||||||
@@ -93,6 +97,9 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
放開事件靠 overlay 的 `AddHandler(MouseLeftButtonUp, handledEventsToo:true)`(manipulator 會標 handled),`_gizmoBakePending` 防同次重複烘焙
|
放開事件靠 overlay 的 `AddHandler(MouseLeftButtonUp, handledEventsToo:true)`(manipulator 會標 handled),`_gizmoBakePending` 防同次重複烘焙
|
||||||
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
|
- 發新版:升 csproj `<Version>` 之外,記得在 `AboutDialog.xaml.cs` 的 `Changelog` 陣列**頂端**加一筆
|
||||||
|
(口吻寫給一般使用者:「你會感覺到什麼」,不是技術 changelog)。
|
||||||
|
⚠️ WPF **頂層 MenuItem 直接掛 Click 不會觸發**(實測滑鼠/鍵盤都沒反應),要開視窗的入口一律放子選單(如 說明 → 關於)
|
||||||
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
||||||
|
|
||||||
## 注意事項 / 已知限制
|
## 注意事項 / 已知限制
|
||||||
@@ -112,13 +119,21 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
`_edgesSuspended` 為真時 `RefreshRootEdges` 不把線掛回。轉動/縮放/平移時不付邊線重建成本。
|
`_edgesSuspended` 為真時 `RefreshRootEdges` 不把線掛回。轉動/縮放/平移時不付邊線重建成本。
|
||||||
**只訂 `Camera.Changed` 不夠**:`Attach` 在建構式呼叫,相機實例若被 Helix 換掉訂閱會孤兒化 → 暫停永不觸發;故加訂控制項層級事件保底(重複觸發 `OnCameraMoved` 無害,有 guard)
|
**只訂 `Camera.Changed` 不夠**:`Attach` 在建構式呼叫,相機實例若被 Helix 換掉訂閱會孤兒化 → 暫停永不觸發;故加訂控制項層級事件保底(重複觸發 `OnCameraMoved` 無害,有 guard)
|
||||||
- CADability `ImportStep` 對少數 AP242 檔案支援不完整;匯入失敗要 catch 顯示訊息,不可閃退
|
- CADability `ImportStep` 對少數 AP242 檔案支援不完整;匯入失敗要 catch 顯示訊息,不可閃退
|
||||||
|
- ⚡ **STL 直接解析(`StlMeshReader`,v0.7.1),不經過 CADability `ImportSTL`**。CADability 會為 STL 每個三角形
|
||||||
|
建一個 B-rep Face(含 Edge/Vertex 拓樸)→ 百萬三角形要數十分鐘且吃光記憶體(實測 100k 三角形:CADability ~41 s
|
||||||
|
vs 直接解析 ~11 ms;75 MB/150 萬三角形整條匯入管線 ~0.77 s)。binary/ASCII 皆自解;binary 判定用
|
||||||
|
**檔案大小 == 84 + 50×count**(header 常也以 "solid" 開頭,不能只看字串)。頂點不焊接(STL 原生就無共用頂點)。
|
||||||
|
**後果**:STL leaf **沒有 `SourceGeos`(CADability B-rep)** → (1) 不進 STEP 匯出(STL 本就非 B-rep 幾何,符合原設計意圖;
|
||||||
|
v0.7.0 以前會把 STL shell 當 Shell 混進 STEP,現移除);(2) 剛體變換照常 — `TransformRoot` 的 SourceGeos 迴圈空轉、
|
||||||
|
網格層照樣變換;(3) 量測走網格近似不受影響。**不要為了「STL 也要進 STEP」而改回 CADability ImportSTL**(會退回龜速)
|
||||||
- IGES 無 reader;STL 無 B-rep(FaceInfo.BrepFace == null 的分支要保留)
|
- IGES 無 reader;STL 無 B-rep(FaceInfo.BrepFace == null 的分支要保留)
|
||||||
- 不寫入原始檔(唯讀工具);WPF 限 Windows,不要嘗試移植 vbox/Linux
|
- 不寫入原始檔(唯讀工具);WPF 限 Windows,不要嘗試移植 vbox/Linux
|
||||||
- 干涉檢查需剛好 2 個可見檔案(樹面板勾選);共面貼合(無穿透)不算干涉、gap≈0 視為配合(match)
|
- 干涉檢查需剛好 2 個可見檔案(樹面板勾選);共面貼合(無穿透)不算干涉、gap≈0 視為配合(match)
|
||||||
- SmokeTest 工具:`--tree`(裝配樹)、`--clip-test`(剖切數學)、`--interference-test`(干涉相交/分離/貼合)、
|
- SmokeTest 工具:`--tree`(裝配樹)、`--clip-test`(剖切數學)、`--interference-test`(干涉相交/分離/貼合)、
|
||||||
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)、
|
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)、
|
||||||
`--export-test <in.stp> <out.stp>`(STEP 匯出往返:寫出→回讀比對實體數)、
|
`--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 匯入效能用)
|
||||||
- **絕不要用 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/
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# v0.7.1 — Large STL files open in a blink
|
||||||
|
|
||||||
|
## ✨ New features
|
||||||
|
|
||||||
|
**Large STL files open in a blink**
|
||||||
|
* Opening big STL meshes (tens of MB, millions of triangles) is now near-instant instead of a multi-minute wait. A 75 MB / 1.5-million-triangle STL that used to take ages now shows in **under a second**.
|
||||||
|
* STL is now parsed directly instead of going through the CAD kernel's importer, which used to build a full B-rep face per triangle (100k triangles: **~41 s → ~11 ms**). Both binary and ASCII STL are detected and read automatically.
|
||||||
|
|
||||||
|
**Customizable menu bar & quick toolbar**
|
||||||
|
* A full menu bar groups every feature into categorized dropdowns, and a quick toolbar lets you tick just the buttons you use most — your selection is remembered between sessions.
|
||||||
|
|
||||||
|
**About dialog with a changelog**
|
||||||
|
* Help → About now shows a per-version, plain-language changelog so you can see what each release brought.
|
||||||
|
|
||||||
|
## 🐛 Bug fixes
|
||||||
|
|
||||||
|
**STL geometry no longer leaks into STEP export**
|
||||||
|
* STL is pure mesh (no B-rep), so STL parts are now correctly excluded from STEP export — earlier versions wrongly mixed STL shells into the exported STEP. Rigid transforms and measurement on STL parts are unaffected.
|
||||||
|
|
||||||
|
## 📦 Downloads
|
||||||
|
|
||||||
|
| Build | Needs .NET Runtime? | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| **STPViewer_v0.7.1** (Standard) | ✅ Requires .NET 8 Desktop Runtime | Smaller download |
|
||||||
|
| **STPViewer_v0.7.1_portable** (Portable) | ❌ Runtime included | Unzip and run anywhere, no admin |
|
||||||
|
|
||||||
|
Run `STPViewer v0.7.1.exe`.
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
* [Architecture](https://github.com/ETWen/STPViewer/blob/main/ARCHITECTURE.md)
|
||||||
|
* [README](https://github.com/ETWen/STPViewer/blob/main/README.md)
|
||||||
|
|
||||||
|
**Full changelog:** [v0.6.0...v0.7.1](https://github.com/ETWen/STPViewer/compare/v0.6.0...v0.7.1)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<Window x:Class="STPViewer.AboutDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="關於 STPViewer"
|
||||||
|
Width="860" Height="600"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="CanResize" MinWidth="700" MinHeight="420"
|
||||||
|
ShowInTaskbar="False">
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#F0F4F8" />
|
||||||
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="Padding" Value="14,10" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,10" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="CardTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,6" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="DimText" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#667788" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="14">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="300" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- ─── 左:程式 / 開發者 / 技術棧 ─── -->
|
||||||
|
<StackPanel Grid.Column="0" Grid.Row="0" Margin="0,0,12,0">
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel HorizontalAlignment="Center">
|
||||||
|
<TextBlock Text="STPViewer" FontSize="22" FontWeight="Bold"
|
||||||
|
Foreground="#1565C0" HorizontalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="VersionText" Text="v0.0.0" Style="{StaticResource DimText}"
|
||||||
|
HorizontalAlignment="Center" Margin="0,2,0,8" />
|
||||||
|
<TextBlock TextAlignment="Center" FontSize="12" LineHeight="18">
|
||||||
|
STP / STEP 3D 檢視器<LineBreak />
|
||||||
|
免開重量級 CAD,快速開檔、量測、<LineBreak />
|
||||||
|
對齊插合、干涉檢查、剖面、匯出
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="👤 開發者" Style="{StaticResource CardTitle}" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="60" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition /><RowDefinition /><RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name" Style="{StaticResource DimText}" />
|
||||||
|
<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="1" Text="eric441151893@gmail.com" FontSize="12" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="GitHub" Style="{StaticResource DimText}" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="github.com/ETWen" FontSize="12" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="🔧 技術棧" Style="{StaticResource CardTitle}" />
|
||||||
|
<TextBlock Style="{StaticResource DimText}" LineHeight="18">
|
||||||
|
.NET 8 · WPF · C#<LineBreak />
|
||||||
|
CADability(STEP / B-rep 幾何核心)<LineBreak />
|
||||||
|
HelixToolkit(3D 顯示與相機)
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ─── 右:Change log ─── -->
|
||||||
|
<Border Grid.Column="1" Grid.Row="0" Background="#FAFBFC" CornerRadius="6" Padding="4">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="📋 版本紀錄" FontWeight="Bold" FontSize="14"
|
||||||
|
Margin="12,8,0,4" />
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="12,0,12,8">
|
||||||
|
<ItemsControl x:Name="ChangelogList">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="0,8,0,4">
|
||||||
|
<TextBlock FontWeight="Bold" Foreground="#1565C0" FontSize="13">
|
||||||
|
<Run Text="●" /> <Run Text="{Binding Header, Mode=OneWay}" />
|
||||||
|
</TextBlock>
|
||||||
|
<ItemsControl ItemsSource="{Binding Changes}" Margin="16,3,0,0">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" TextWrapping="Wrap" FontSize="12"
|
||||||
|
LineHeight="17" Margin="0,1,0,1" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button Grid.Column="1" Grid.Row="1" Content="關閉" Width="90" Margin="0,10,0,0"
|
||||||
|
HorizontalAlignment="Right" IsCancel="True" IsDefault="True" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace STPViewer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 關於視窗:左 = 程式資訊 / 開發者 / 技術棧,右 = 版本紀錄(寫給一般使用者看,非技術 changelog)。
|
||||||
|
/// 樣式參考 ETTerms 的 AboutView。發新版時在 Changelog 陣列最上面加一筆。
|
||||||
|
/// </summary>
|
||||||
|
public partial class AboutDialog : Window
|
||||||
|
{
|
||||||
|
public AboutDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Version v = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
VersionText.Text = $"v{v.Major}.{v.Minor}.{v.Build}";
|
||||||
|
ChangelogList.ItemsSource = Changelog.Select(e => new
|
||||||
|
{
|
||||||
|
Header = $"v{e.Version} · {e.Title} ({e.Date:yyyy-MM-dd})",
|
||||||
|
e.Changes,
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChangelogEntry(string Version, DateOnly Date, string Title, string[] Changes);
|
||||||
|
|
||||||
|
// ── 版本紀錄(新版加最上面;口吻:一般使用者看得懂,講「你會感覺到什麼」而非實作細節)──
|
||||||
|
private static readonly ChangelogEntry[] Changelog =
|
||||||
|
{
|
||||||
|
new("0.7.1", new DateOnly(2026, 7, 12), "大型 STL 秒開",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 開啟大型 STL 網格檔(數十 MB、上百萬三角形)現在幾乎瞬間完成,不用再等好幾分鐘。",
|
||||||
|
"• 例如一個 75 MB、150 萬個三角形的 STL,以前要等到天荒地老,現在不到 1 秒就顯示出來。",
|
||||||
|
}),
|
||||||
|
new("0.7.0", new DateOnly(2026, 7, 4), "介面大整理 — 選單列+可自訂快速列",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 上方新增選單列,全部功能依「檔案/視圖/量測/變換/工具」分類,不用再在一長排按鈕裡找。",
|
||||||
|
"• 原本的工具列變成「快速列」:只放常用按鈕。想放哪些自己決定 — 到「快速列 → 自訂按鈕」勾選,下次開啟會記住。",
|
||||||
|
"• 剖面的軸向/位置控制列改成只在開啟剖面時出現,平常不佔空間。",
|
||||||
|
"• 新增「關於」視窗(就是這個畫面),可以看版本紀錄。",
|
||||||
|
}),
|
||||||
|
new("0.6.0", new DateOnly(2026, 7, 4), "匯出 STL",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 新增匯出 STL:把畫面上的零件(含對齊、旋轉後的位置)存成 STL 網格檔,可拿去 3D 列印或給其他軟體用。",
|
||||||
|
"• 匯出前會先跳出設定視窗:全部合併成一個檔或每個檔案各存一個、Binary 或文字格式、mm 或 inch、網格精細度 — 確認後才匯出。",
|
||||||
|
"• 設定視窗會即時顯示三角形數量與預估檔案大小。",
|
||||||
|
}),
|
||||||
|
new("0.5.0", new DateOnly(2026, 7, 3), "量測、視圖、樹狀面板大升級",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 量距離時點到圓孔邊緣會自動吸到圓心 — 量兩孔中心距(pitch)超方便。",
|
||||||
|
"• 量測快速鍵:P 點、D 距離、E 邊、F 面、C 圓、A 角度、M 面距;Esc 取消。",
|
||||||
|
"• 樹狀面板可搜尋零件名稱;右鍵可「只顯示此節點」隔離觀察,也可以量體積與質心。",
|
||||||
|
"• 新增標準視圖(等角/前/上/右)與正交投影,量尺寸不受透視變形。",
|
||||||
|
"• 剖面新增「3點」模式:在模型上點 3 個點,就能沿任意斜面剖開。",
|
||||||
|
"• 新增匯出 STEP:把對齊好的零件位置存成新 STEP 檔,交接給正式 CAD。",
|
||||||
|
"• 會記住視窗大小、單位設定與最近開啟的 10 個檔案。",
|
||||||
|
}),
|
||||||
|
new("0.4.0", new DateOnly(2026, 7, 2), "更快、更穩",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 大檔案拉剖面滑桿不再卡住畫面(改到背景計算)。",
|
||||||
|
"• 拖曳/操作器放開零件後的定位計算大幅加速。",
|
||||||
|
"• 程式遇到錯誤會提示訊息並繼續運作,不再整個閃退。",
|
||||||
|
}),
|
||||||
|
new("0.3.2", new DateOnly(2026, 6, 18), "大組件量測不再卡",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 修正大型組件(數萬個面)進入量測模式後,轉動視角嚴重卡頓的問題 — 現在量測跟瀏覽一樣順。",
|
||||||
|
}),
|
||||||
|
new("0.3.1", new DateOnly(2026, 6, 13), "操作器永遠可見",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 三軸操作器改成永遠浮在畫面最上層,不會被零件擋住,任何角度都抓得到。",
|
||||||
|
}),
|
||||||
|
new("0.3.0", new DateOnly(2026, 6, 13), "三軸操作器",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 新增操作器:選取檔案後出現 XYZ 三色箭頭與旋轉環,箭頭沿軸精確移動、旋轉環繞軸轉任意角度。",
|
||||||
|
}),
|
||||||
|
new("0.2.0", new DateOnly(2026, 6, 13), "拖曳模式",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• 新增拖曳模式:左鍵按住零件直接拖到想要的位置,放開就定位。",
|
||||||
|
"• 0.2.1 修正拖曳過的零件再點擊會當掉的問題。",
|
||||||
|
}),
|
||||||
|
new("0.1.0", new DateOnly(2026, 6, 13), "首次釋出",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"• STEP/STL/DXF 匯入與裝配樹、點/距離/邊/面/圓/角度/面距量測、兩點與三點對齊、干涉檢查、剖面、mm ⇄ inch、CSV 與截圖匯出。",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace STPViewer.Converters;
|
||||||
|
|
||||||
|
/// <summary>完整路徑 → 檔名(選單「最近檔案」顯示用;tooltip 仍給完整路徑)</summary>
|
||||||
|
public class FileNameConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
value is string s ? System.IO.Path.GetFileName(s) : "";
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
+182
-40
@@ -12,111 +12,253 @@
|
|||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<conv:EnumToBoolConverter x:Key="ModeConv" />
|
<conv:EnumToBoolConverter x:Key="ModeConv" />
|
||||||
|
<conv:FileNameConverter x:Key="FileNameConv" />
|
||||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||||
<Style x:Key="ModeButton" TargetType="ToggleButton">
|
<Style x:Key="ModeButton" TargetType="ToggleButton">
|
||||||
<Setter Property="Padding" Value="8,4" />
|
<Setter Property="Padding" Value="8,4" />
|
||||||
<Setter Property="Margin" Value="2,0" />
|
<Setter Property="Margin" Value="2,0" />
|
||||||
<Setter Property="FontSize" Value="13" />
|
<Setter Property="FontSize" Value="13" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style x:Key="QuickButton" TargetType="Button">
|
||||||
|
<Setter Property="Padding" Value="8,4" />
|
||||||
|
<Setter Property="Margin" Value="2,0" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<!-- ─── 工具列 ─── -->
|
<!-- ─── 選單列(全部功能,分類下拉)─── -->
|
||||||
|
<Menu DockPanel.Dock="Top">
|
||||||
|
<MenuItem Header="檔案(_F)">
|
||||||
|
<MenuItem Header="📂 匯入…" Command="{Binding ImportCommand}"
|
||||||
|
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)" />
|
||||||
|
<MenuItem Header="最近檔案" ItemsSource="{Binding RecentFiles}">
|
||||||
|
<MenuItem.ItemContainerStyle>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Header" Value="{Binding Converter={StaticResource FileNameConv}}" />
|
||||||
|
<Setter Property="ToolTip" Value="{Binding}" />
|
||||||
|
<Setter Property="Command"
|
||||||
|
Value="{Binding DataContext.ImportRecentCommand, RelativeSource={RelativeSource AncestorType=Menu}}" />
|
||||||
|
<Setter Property="CommandParameter" Value="{Binding}" />
|
||||||
|
</Style>
|
||||||
|
</MenuItem.ItemContainerStyle>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="💾 匯出量測 CSV…" Command="{Binding ExportCsvCommand}" />
|
||||||
|
<MenuItem Header="📷 視圖截圖 PNG…" Command="{Binding SaveScreenshotCommand}"
|
||||||
|
ToolTip="3D 視圖截圖存 PNG(2x 解析度)" />
|
||||||
|
<MenuItem Header="📤 匯出 STEP…" Command="{Binding ExportStepFileCommand}"
|
||||||
|
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔;不動原始檔" />
|
||||||
|
<MenuItem Header="📐 匯出 STL…" Command="{Binding ExportStlFileCommand}"
|
||||||
|
ToolTip="先設定範圍/格式/單位/精度,確認再匯出" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="視圖(_V)">
|
||||||
|
<MenuItem Header="🔍 縮放至全部" Command="{Binding ZoomAllCommand}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="等角視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" />
|
||||||
|
<MenuItem Header="前視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
||||||
|
ToolTip="-Y 方向看,Z 向上" />
|
||||||
|
<MenuItem Header="上視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
||||||
|
ToolTip="+Z 往下看" />
|
||||||
|
<MenuItem Header="右視圖" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
||||||
|
ToolTip="+X 方向看,Z 向上" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="正交投影" IsCheckable="True" IsChecked="{Binding OrthographicView}"
|
||||||
|
ToolTip="平行投影,量尺寸不受透視變形;取消勾選切回透視" />
|
||||||
|
<MenuItem Header="單位顯示 inch(mm ⇄ in)" IsCheckable="True" IsChecked="{Binding UseInch}"
|
||||||
|
ToolTip="既有量測即時換算;內部一律存 mm" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="量測(_M)">
|
||||||
|
<MenuItem Header="📍 點座標" IsCheckable="True" InputGestureText="P"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Point}"
|
||||||
|
ToolTip="量測點座標(吸附頂點/圓心)" />
|
||||||
|
<MenuItem Header="📏 兩點距離" IsCheckable="True" InputGestureText="D"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Distance}"
|
||||||
|
ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)" />
|
||||||
|
<MenuItem Header="📐 邊長" IsCheckable="True" InputGestureText="E"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Edge}"
|
||||||
|
ToolTip="邊長(直線/曲線/圓弧)" />
|
||||||
|
<MenuItem Header="⬛ 面" IsCheckable="True" InputGestureText="F"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Face}"
|
||||||
|
ToolTip="面積 + 曲面類型/法向量" />
|
||||||
|
<MenuItem Header="⭕ 圓" IsCheckable="True" InputGestureText="C"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Circle}"
|
||||||
|
ToolTip="圓心/半徑/直徑/周長" />
|
||||||
|
<MenuItem Header="∠ 角度" IsCheckable="True" InputGestureText="A"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Angle}"
|
||||||
|
ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)" />
|
||||||
|
<MenuItem Header="⇔ 面距" IsCheckable="True" InputGestureText="M"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=FaceDistance}"
|
||||||
|
ToolTip="面到面最短距離(網格近似)" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="🧹 清除全部量測" Command="{Binding ClearMeasurementsCommand}"
|
||||||
|
InputGestureText="Esc" ToolTip="Esc = 取消進行中量測/退出模式" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="變換(_T)">
|
||||||
|
<MenuItem Header="🎯 兩點對齊(平移)" IsCheckable="True"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align}"
|
||||||
|
ToolTip="先點要移動零件上的點,再點目標點" />
|
||||||
|
<MenuItem Header="🎯 三點對齊(旋轉+平移)" IsCheckable="True"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Align3}"
|
||||||
|
ToolTip="先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)" />
|
||||||
|
<MenuItem Header="🖐 拖曳模式" IsCheckable="True"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}"
|
||||||
|
ToolTip="左鍵按住零件沿螢幕平面拖動,放開定位" />
|
||||||
|
<MenuItem Header="⊹ 三軸操作器" IsCheckable="True" IsChecked="{Binding GizmoEnabled}"
|
||||||
|
ToolTip="對樹面板選取的檔案顯示 XYZ 箭頭與旋轉環,每次放開即定位" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="↻ 繞 X 軸 +90°" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
||||||
|
ToolTip="對樹面板選取的檔案,繞其中心旋轉(先在樹面板點選檔案)" />
|
||||||
|
<MenuItem Header="↻ 繞 Y 軸 +90°" Command="{Binding RotateRootCommand}" CommandParameter="Y" />
|
||||||
|
<MenuItem Header="↻ 繞 Z 軸 +90°" Command="{Binding RotateRootCommand}" CommandParameter="Z" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="工具(_L)">
|
||||||
|
<MenuItem Header="🧩 干涉檢查" Command="{Binding CheckInterferenceCommand}"
|
||||||
|
ToolTip="2 個以上可見檔案兩兩檢查是否相交;無相交時回報最小間隙" />
|
||||||
|
<MenuItem Header="✂ 剖面檢視" IsCheckable="True" IsChecked="{Binding SectionEnabled}"
|
||||||
|
ToolTip="開啟後下方出現剖面參數列(軸向/位置/反向)" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="快速列(_Q)">
|
||||||
|
<MenuItem Header="自訂按鈕" ItemsSource="{Binding QuickBar.Items}"
|
||||||
|
ToolTip="勾選要顯示在快速列的常用按鈕(隨設定保存)">
|
||||||
|
<MenuItem.ItemContainerStyle>
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Header" Value="{Binding Label}" />
|
||||||
|
<Setter Property="IsCheckable" Value="True" />
|
||||||
|
<Setter Property="IsChecked" Value="{Binding IsChecked}" />
|
||||||
|
<Setter Property="StaysOpenOnClick" Value="True" />
|
||||||
|
</Style>
|
||||||
|
</MenuItem.ItemContainerStyle>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="恢復預設按鈕" Command="{Binding QuickBar.ResetDefaultsCommand}" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="說明(_H)">
|
||||||
|
<MenuItem Header="ℹ 關於 STPViewer…" Click="About_Click"
|
||||||
|
ToolTip="程式資訊與版本紀錄" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<!-- ─── 快速工具列(可自訂:選單「快速列 → 自訂按鈕」勾選)─── -->
|
||||||
<ToolBarTray DockPanel.Dock="Top">
|
<ToolBarTray DockPanel.Dock="Top">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Button Command="{Binding ImportCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ImportCommand}"
|
||||||
|
Visibility="{Binding QuickBar[Import].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
ToolTip="匯入 STP/STEP/STL/DXF(可複選,也可直接拖放到視窗)">📂 匯入</Button>
|
||||||
<Button Click="Recent_Click" Padding="4,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Click="Recent_Click" Padding="4,4"
|
||||||
|
Visibility="{Binding QuickBar[Recent].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="最近開啟的檔案">▾</Button>
|
ToolTip="最近開啟的檔案">▾</Button>
|
||||||
<Button Command="{Binding ZoomAllCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ZoomAllCommand}"
|
||||||
|
Visibility="{Binding QuickBar[ZoomAll].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
ToolTip="縮放至全部模型">🔍 全覽</Button>
|
||||||
<Separator />
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Iso"
|
||||||
<TextBlock Text="視圖:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
Visibility="{Binding QuickBar[ViewIso].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Iso" Padding="6,4" FontSize="13"
|
|
||||||
ToolTip="等角視圖">等角</Button>
|
ToolTip="等角視圖">等角</Button>
|
||||||
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Front" Padding="6,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Front"
|
||||||
|
Visibility="{Binding QuickBar[ViewFront].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="前視圖(-Y 方向看,Z 向上)">前</Button>
|
ToolTip="前視圖(-Y 方向看,Z 向上)">前</Button>
|
||||||
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Top" Padding="6,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Top"
|
||||||
|
Visibility="{Binding QuickBar[ViewTop].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="上視圖(+Z 往下看)">上</Button>
|
ToolTip="上視圖(+Z 往下看)">上</Button>
|
||||||
<Button Command="{Binding SetStandardViewCommand}" CommandParameter="Right" Padding="6,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SetStandardViewCommand}" CommandParameter="Right"
|
||||||
|
Visibility="{Binding QuickBar[ViewRight].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="右視圖(+X 方向看,Z 向上)">右</Button>
|
ToolTip="右視圖(+X 方向看,Z 向上)">右</Button>
|
||||||
<ToggleButton IsChecked="{Binding OrthographicView}" Padding="6,4" FontSize="13" Margin="2,0"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding OrthographicView}"
|
||||||
|
Visibility="{Binding QuickBar[Ortho].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="正交投影(平行投影,量尺寸不受透視變形;再按切回透視)">正交</ToggleButton>
|
ToolTip="正交投影(平行投影,量尺寸不受透視變形;再按切回透視)">正交</ToggleButton>
|
||||||
<Separator />
|
|
||||||
<TextBlock Text="量測:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點/圓心)— 快速鍵 P"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="量測點座標(吸附頂點/圓心)— 快速鍵 P"
|
||||||
|
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}">📍 點</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)— 快速鍵 D"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩點距離 + ΔXYZ(點到圓邊會吸圓心 → 可量孔對孔 pitch)— 快速鍵 D"
|
||||||
|
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}">📏 距離</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)— 快速鍵 E"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="邊長(直線/曲線/圓弧)— 快速鍵 E"
|
||||||
|
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}">📐 邊</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量 — 快速鍵 F"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面積 + 曲面類型/法向量 — 快速鍵 F"
|
||||||
|
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}">⬛ 面</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長 — 快速鍵 C"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="圓心/半徑/直徑/周長 — 快速鍵 C"
|
||||||
|
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}">⭕ 圓</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)— 快速鍵 A"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="兩面/兩邊夾角(點兩個面或靠近直線邊)— 快速鍵 A"
|
||||||
|
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}">∠ 角度</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)— 快速鍵 M"
|
<ToggleButton Style="{StaticResource ModeButton}" ToolTip="面到面最短距離(網格近似)— 快速鍵 M"
|
||||||
|
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}">⇔ 面距</ToggleButton>
|
||||||
<Separator />
|
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="兩點對齊(平移零件):先點要移動零件上的點,再點目標點"
|
ToolTip="兩點對齊(平移零件):先點要移動零件上的點,再點目標點"
|
||||||
|
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}">🎯 對齊</ToggleButton>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="三點對齊(旋轉+平移):先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)"
|
ToolTip="三點對齊(旋轉+平移):先在要移動的檔案點 3 個特徵點,再到目標檔案點 3 個對應點(順序對應)"
|
||||||
|
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}">🎯 三點</ToggleButton>
|
||||||
<Separator />
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="X"
|
||||||
<TextBlock Text="旋轉:" VerticalAlignment="Center" Margin="4,0,2,0"
|
Visibility="{Binding QuickBar[RotateX].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="對樹面板選取的檔案,繞其中心旋轉 +90°" />
|
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="X" Padding="6,4" FontSize="13"
|
|
||||||
ToolTip="選取的檔案繞 X 軸 +90°(先在樹面板點選檔案)">↻X</Button>
|
ToolTip="選取的檔案繞 X 軸 +90°(先在樹面板點選檔案)">↻X</Button>
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="Y" Padding="6,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Y"
|
||||||
|
Visibility="{Binding QuickBar[RotateY].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="選取的檔案繞 Y 軸 +90°">↻Y</Button>
|
ToolTip="選取的檔案繞 Y 軸 +90°">↻Y</Button>
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="Z" Padding="6,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding RotateRootCommand}" CommandParameter="Z"
|
||||||
|
Visibility="{Binding QuickBar[RotateZ].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="選取的檔案繞 Z 軸 +90°">↻Z</Button>
|
ToolTip="選取的檔案繞 Z 軸 +90°">↻Z</Button>
|
||||||
<ToggleButton Style="{StaticResource ModeButton}"
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
ToolTip="拖曳模式:左鍵按住零件沿螢幕平面拖動,放開定位(深度方向先轉視角再拖)"
|
ToolTip="拖曳模式:左鍵按住零件沿螢幕平面拖動,放開定位(深度方向先轉視角再拖)"
|
||||||
|
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}">🖐 拖曳</ToggleButton>
|
||||||
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding GizmoEnabled}"
|
||||||
|
Visibility="{Binding QuickBar[Gizmo].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
||||||
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding CheckInterferenceCommand}"
|
||||||
|
Visibility="{Binding QuickBar[Interference].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="干涉檢查:2 個以上可見檔案兩兩檢查是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
ToolTip="干涉檢查:2 個以上可見檔案兩兩檢查是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
||||||
<Separator />
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding SectionEnabled}"
|
||||||
<Button Command="{Binding ClearMeasurementsCommand}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[Section].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="開啟/關閉剖面檢視(開啟後下方出現參數列)">✂ 剖面</ToggleButton>
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ClearMeasurementsCommand}"
|
||||||
|
Visibility="{Binding QuickBar[Clear].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="清除全部量測與標記(Esc = 取消進行中量測/退出模式)">🧹 清除</Button>
|
ToolTip="清除全部量測與標記(Esc = 取消進行中量測/退出模式)">🧹 清除</Button>
|
||||||
<Separator />
|
<ToggleButton Style="{StaticResource ModeButton}" IsChecked="{Binding UseInch}"
|
||||||
<ToggleButton IsChecked="{Binding UseInch}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[Unit].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
ToolTip="切換顯示單位 mm ⇄ inch(既有量測即時換算)">mm ⇄ in</ToggleButton>
|
||||||
<Separator />
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportCsvCommand}"
|
||||||
<Button Command="{Binding ExportCsvCommand}" Padding="8,4" FontSize="13"
|
Visibility="{Binding QuickBar[ExportCsv].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
ToolTip="量測結果匯出 CSV">💾 CSV</Button>
|
||||||
<Button Command="{Binding SaveScreenshotCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding SaveScreenshotCommand}"
|
||||||
|
Visibility="{Binding QuickBar[Screenshot].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
ToolTip="3D 視圖截圖存 PNG(2x 解析度)">📷 截圖</Button>
|
||||||
<Button Command="{Binding ExportStepFileCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStepFileCommand}"
|
||||||
|
Visibility="{Binding QuickBar[ExportStep].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔 — 對齊插合結果可交接 CAD;不動原始檔">📤 STEP</Button>
|
ToolTip="把可見檔案(目前對齊位置)匯出成新 STEP 檔 — 對齊插合結果可交接 CAD;不動原始檔">📤 STEP</Button>
|
||||||
<Button Command="{Binding ExportStlFileCommand}" Padding="8,4" FontSize="13"
|
<Button Style="{StaticResource QuickButton}" Command="{Binding ExportStlFileCommand}"
|
||||||
|
Visibility="{Binding QuickBar[ExportStl].IsChecked, Converter={StaticResource BoolToVis}}"
|
||||||
ToolTip="把可見檔案(目前位置)匯出成 STL 網格檔 — 先設定範圍/格式/單位/精度,確認再匯出">📐 STL</Button>
|
ToolTip="把可見檔案(目前位置)匯出成 STL 網格檔 — 先設定範圍/格式/單位/精度,確認再匯出">📐 STL</Button>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
|
</ToolBarTray>
|
||||||
|
|
||||||
|
<!-- ─── 剖面參數列(只在剖面開啟時出現)─── -->
|
||||||
|
<ToolBarTray DockPanel.Dock="Top"
|
||||||
|
Visibility="{Binding SectionEnabled, Converter={StaticResource BoolToVis}}">
|
||||||
<ToolBar>
|
<ToolBar>
|
||||||
<ToggleButton IsChecked="{Binding SectionEnabled}" Padding="8,4" FontSize="13"
|
<TextBlock Text="✂ 剖面:" VerticalAlignment="Center" Margin="4,0,2,0" />
|
||||||
ToolTip="開啟/關閉剖面檢視">✂ 剖面</ToggleButton>
|
|
||||||
<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="剖切軸向(3點 = 在模型上點 3 點定義任意平面)">
|
||||||
IsEnabled="{Binding SectionEnabled}">
|
|
||||||
<ComboBoxItem>X</ComboBoxItem>
|
<ComboBoxItem>X</ComboBoxItem>
|
||||||
<ComboBoxItem>Y</ComboBoxItem>
|
<ComboBoxItem>Y</ComboBoxItem>
|
||||||
<ComboBoxItem>Z</ComboBoxItem>
|
<ComboBoxItem>Z</ComboBoxItem>
|
||||||
<ComboBoxItem>3點</ComboBoxItem>
|
<ComboBoxItem>3點</ComboBoxItem>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Slider Value="{Binding SectionPosition}" Minimum="0" Maximum="100" Width="180"
|
<Slider Value="{Binding SectionPosition}" Minimum="0" Maximum="100" Width="180"
|
||||||
VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿法向 %)"
|
VerticalAlignment="Center" Margin="4,0" ToolTip="剖面位置(沿法向 %)" />
|
||||||
IsEnabled="{Binding SectionEnabled}" />
|
|
||||||
<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="剖面位置 %(0–100,可直接輸入,離開欄位生效)" />
|
||||||
IsEnabled="{Binding SectionEnabled}" />
|
|
||||||
<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="反向" VerticalAlignment="Center"
|
||||||
Margin="4,0" ToolTip="切換保留側" IsEnabled="{Binding SectionEnabled}" />
|
Margin="4,0" ToolTip="切換保留側" />
|
||||||
|
<Button Style="{StaticResource QuickButton}" Command="{Binding CloseSectionCommand}"
|
||||||
|
ToolTip="關閉剖面檢視">✕ 關閉</Button>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
</ToolBarTray>
|
</ToolBarTray>
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ public partial class MainWindow : Window
|
|||||||
menu.IsOpen = true;
|
menu.IsOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void About_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
new AboutDialog { Owner = this }.ShowDialog();
|
||||||
|
|
||||||
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) =>
|
private void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) =>
|
||||||
_vm.SelectedNode = e.NewValue as ModelNodeViewModel;
|
_vm.SelectedNode = e.NewValue as ModelNodeViewModel;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Version>0.6.0</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
<Version>0.7.1</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ public class AppSettings
|
|||||||
public bool WindowMaximized { get; set; }
|
public bool WindowMaximized { get; set; }
|
||||||
public bool UseInch { get; set; }
|
public bool UseInch { get; set; }
|
||||||
public List<string> RecentFiles { get; set; } = new();
|
public List<string> RecentFiles { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>快速工具列顯示的按鈕 key 清單;null = 用預設(QuickBarViewModel.Registry)</summary>
|
||||||
|
public List<string>? QuickBarKeys { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>設定讀寫:壞檔/缺檔一律回預設值,儲存失敗靜默(設定不值得讓程式閃退)</summary>
|
/// <summary>設定讀寫:壞檔/缺檔一律回預設值,儲存失敗靜默(設定不值得讓程式閃退)</summary>
|
||||||
|
|||||||
@@ -123,46 +123,42 @@ public class StepImportService
|
|||||||
BuildChildren(root, EnumerateGeo(list));
|
BuildChildren(root, EnumerateGeo(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── STL(純網格,無 B-rep;整個 shell 合併成單一 mesh 避免模型數爆炸)──
|
// ─── STL(純網格,無 B-rep)──
|
||||||
|
// 直接解析三角形(StlMeshReader),**不經過 CADability**。CADability 的 ImportSTL 會為每個
|
||||||
|
// 三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),百萬三角形需數十分鐘且吃光記憶體
|
||||||
|
// (實測 100k 三角形:CADability ~41 s,直接解析 ~11 ms)。STL 本就無 B-rep,讀三角形即可。
|
||||||
|
// ⚠️ 因此 STL leaf 沒有 SourceGeos(CADability B-rep)→ 不進 STEP 匯出(本就非 STEP B-rep 幾何);
|
||||||
|
// 剛體變換仍走網格層(TransformRoot 的 SourceGeos 迴圈空轉、mesh 照樣變換),量測走網格近似不受影響。
|
||||||
|
|
||||||
private void ImportStlFile(string path, ImportedNode root)
|
private void ImportStlFile(string path, ImportedNode root)
|
||||||
{
|
{
|
||||||
Shell[] shells = new ImportSTL().Read(path)
|
Progress?.Invoke("解析 STL(直接讀三角形)…");
|
||||||
?? throw new InvalidDataException("無法解析 STL 檔");
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
int i = 1;
|
List<StlMeshReader.MeshGroup> groups = StlMeshReader.Read(path);
|
||||||
foreach (Shell sh in shells)
|
if (groups.Count == 0)
|
||||||
{
|
throw new InvalidDataException("STL 檔內沒有三角形");
|
||||||
var positions = new Point3DCollection();
|
|
||||||
var indices = new Int32Collection();
|
|
||||||
double precision = PrecisionFor(SafeBounds(() => sh.GetBoundingCube()));
|
|
||||||
foreach (Face f in sh.Faces)
|
|
||||||
{
|
|
||||||
GeoPoint[] pts; int[] ind;
|
|
||||||
try { f.GetTriangulation(precision, out pts, out _, out ind, out _); }
|
|
||||||
catch { continue; }
|
|
||||||
if (pts is null || ind is null) continue;
|
|
||||||
int offset = positions.Count;
|
|
||||||
foreach (GeoPoint p in pts) positions.Add(new Point3D(p.x, p.y, p.z));
|
|
||||||
foreach (int k in ind) indices.Add(offset + k);
|
|
||||||
}
|
|
||||||
if (positions.Count == 0) continue;
|
|
||||||
|
|
||||||
var mesh = new MeshGeometry3D { Positions = positions, TriangleIndices = indices };
|
foreach (StlMeshReader.MeshGroup g in groups)
|
||||||
|
{
|
||||||
|
if (g.Positions.Count == 0) continue;
|
||||||
|
g.Positions.Freeze();
|
||||||
|
g.Indices.Freeze();
|
||||||
|
var mesh = new MeshGeometry3D { Positions = g.Positions, TriangleIndices = g.Indices };
|
||||||
mesh.Freeze();
|
mesh.Freeze();
|
||||||
|
int tris = g.Indices.Count / 3;
|
||||||
var leaf = new ImportedNode
|
var leaf = new ImportedNode
|
||||||
{
|
{
|
||||||
Name = shells.Length == 1 ? "網格" : $"網格 {i}",
|
Name = g.Name,
|
||||||
HasBrep = false,
|
HasBrep = false,
|
||||||
SolidCount = 1,
|
SolidCount = 1,
|
||||||
FaceCount = sh.Faces.Length,
|
FaceCount = tris, // STL 無 B-rep 面 → 以三角形數代表
|
||||||
TriangleCount = indices.Count / 3,
|
TriangleCount = tris,
|
||||||
Bounds = BoundsOfMesh(mesh),
|
Bounds = BoundsOfMesh(mesh),
|
||||||
};
|
};
|
||||||
leaf.SourceGeos.Add(sh);
|
|
||||||
leaf.Faces.Add(new ImportedFace(null, mesh));
|
leaf.Faces.Add(new ImportedFace(null, mesh));
|
||||||
root.Children.Add(leaf);
|
root.Children.Add(leaf);
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
Progress?.Invoke($"STL 解析 {sw.ElapsedMilliseconds:N0} ms({root.Children.Count} 個網格)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Media3D;
|
||||||
|
|
||||||
|
namespace STPViewer.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直接解析 STL 三角網格(binary / ASCII)→ WPF mesh,**不經過 CADability**。
|
||||||
|
/// CADability 的 ImportSTL 會為每個三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),
|
||||||
|
/// 大檔(百萬三角形)需數十分鐘且吃光記憶體;STL 本就無 B-rep,直接讀三角形即可。
|
||||||
|
/// 實測 100k 三角形:CADability 約 41,000 ms,直接解析約 11 ms。
|
||||||
|
/// </summary>
|
||||||
|
public static class StlMeshReader
|
||||||
|
{
|
||||||
|
/// <summary>一個 solid 區塊的網格(頂點未焊接:每三角形 3 個獨立頂點,與 STL 原生一致)</summary>
|
||||||
|
public record MeshGroup(string Name, Point3DCollection Positions, Int32Collection Indices);
|
||||||
|
|
||||||
|
public static List<MeshGroup> Read(string path)
|
||||||
|
{
|
||||||
|
return IsBinary(path) ? ReadBinary(path) : ReadAscii(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// binary STL 判定:檔案大小必須剛好等於 84 + 50×count(count = offset 80 的 uint32)。
|
||||||
|
/// 這是最可靠的判別法 — binary 的 80-byte header 常也以 "solid" 開頭,不能只看開頭字串。
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsBinary(string path)
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(path);
|
||||||
|
if (fi.Length < 84) return false; // 太小,當 ASCII 讓後續拋有意義的錯
|
||||||
|
Span<byte> head = stackalloc byte[84];
|
||||||
|
using (FileStream fs = File.OpenRead(path))
|
||||||
|
{
|
||||||
|
int read = 0;
|
||||||
|
while (read < 84)
|
||||||
|
{
|
||||||
|
int n = fs.Read(head[read..]);
|
||||||
|
if (n <= 0) break;
|
||||||
|
read += n;
|
||||||
|
}
|
||||||
|
if (read < 84) return false;
|
||||||
|
}
|
||||||
|
uint count = BitConverter.ToUInt32(head[80..]);
|
||||||
|
return fi.Length == 84L + 50L * count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MeshGroup> ReadBinary(string path)
|
||||||
|
{
|
||||||
|
byte[] bytes = File.ReadAllBytes(path);
|
||||||
|
uint count = BitConverter.ToUInt32(bytes, 80);
|
||||||
|
var positions = new Point3DCollection((int)Math.Min(count * 3, int.MaxValue));
|
||||||
|
var indices = new Int32Collection((int)Math.Min(count * 3, int.MaxValue));
|
||||||
|
|
||||||
|
var span = bytes.AsSpan();
|
||||||
|
int off = 84, vi = 0;
|
||||||
|
for (uint t = 0; t < count; t++)
|
||||||
|
{
|
||||||
|
if (off + 50 > bytes.Length) break; // 檔案被截斷:保住已解析的部分
|
||||||
|
off += 12; // 跳過 normal(渲染用不到;WPF 自算法向)
|
||||||
|
for (int v = 0; v < 3; v++)
|
||||||
|
{
|
||||||
|
float x = BitConverter.ToSingle(span[off..]);
|
||||||
|
float y = BitConverter.ToSingle(span[(off + 4)..]);
|
||||||
|
float z = BitConverter.ToSingle(span[(off + 8)..]);
|
||||||
|
positions.Add(new Point3D(x, y, z));
|
||||||
|
indices.Add(vi++);
|
||||||
|
off += 12;
|
||||||
|
}
|
||||||
|
off += 2; // attribute byte count
|
||||||
|
}
|
||||||
|
return new List<MeshGroup> { new("網格", positions, indices) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ASCII STL:逐行掃 "vertex x y z",每 3 個湊成一個三角形;"solid <name>" 起新群組。
|
||||||
|
/// ASCII 大檔罕見(同樣三角數約為 binary 的 5 倍體積),故以正確性為主。
|
||||||
|
/// </summary>
|
||||||
|
private static List<MeshGroup> ReadAscii(string path)
|
||||||
|
{
|
||||||
|
var groups = new List<MeshGroup>();
|
||||||
|
Point3DCollection? pos = null;
|
||||||
|
Int32Collection? idx = null;
|
||||||
|
int vi = 0;
|
||||||
|
|
||||||
|
void FlushGroup()
|
||||||
|
{
|
||||||
|
if (pos is { Count: > 0 } && idx is not null)
|
||||||
|
groups.Add(new MeshGroup(groups.Count == 0 ? "網格" : $"網格 {groups.Count + 1}", pos, idx));
|
||||||
|
pos = null; idx = null; vi = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char[] sep = { ' ', '\t' };
|
||||||
|
foreach (string raw in File.ReadLines(path))
|
||||||
|
{
|
||||||
|
string line = raw.Trim();
|
||||||
|
if (line.Length == 0) continue;
|
||||||
|
|
||||||
|
if (line.StartsWith("solid", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
FlushGroup();
|
||||||
|
pos = new Point3DCollection();
|
||||||
|
idx = new Int32Collection();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("endsolid", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
FlushGroup();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!line.StartsWith("vertex", StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
|
pos ??= new Point3DCollection();
|
||||||
|
idx ??= new Int32Collection();
|
||||||
|
string[] tok = line.Split(sep, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (tok.Length < 4) continue;
|
||||||
|
if (double.TryParse(tok[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double x) &&
|
||||||
|
double.TryParse(tok[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double y) &&
|
||||||
|
double.TryParse(tok[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double z))
|
||||||
|
{
|
||||||
|
pos.Add(new Point3D(x, y, z));
|
||||||
|
idx.Add(vi++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlushGroup();
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Media.Media3D;
|
using System.Windows.Media.Media3D;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using HelixToolkit.Wpf;
|
using HelixToolkit.Wpf;
|
||||||
using STPViewer.Models;
|
using STPViewer.Models;
|
||||||
using STPViewer.Services;
|
using STPViewer.Services;
|
||||||
@@ -38,6 +39,10 @@ public partial class MainViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool sectionFlip;
|
private bool sectionFlip;
|
||||||
|
|
||||||
|
/// <summary>剖面參數列的「✕ 關閉」按鈕(參數列只在剖面開啟時顯示)</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void CloseSection() => SectionEnabled = false;
|
||||||
|
|
||||||
// ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)──
|
// ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)──
|
||||||
private const int CustomAxisIndex = 3;
|
private const int CustomAxisIndex = 3;
|
||||||
private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點)
|
private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點)
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ public partial class MainViewModel : ObservableObject
|
|||||||
public ObservableCollection<ModelNodeViewModel> Roots { get; } = new();
|
public ObservableCollection<ModelNodeViewModel> Roots { get; } = new();
|
||||||
public ObservableCollection<MeasurementResult> Measurements { get; } = new();
|
public ObservableCollection<MeasurementResult> Measurements { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>快速工具列自訂(選單列有全部功能,快速列只放勾選的常用按鈕)</summary>
|
||||||
|
public QuickBarViewModel QuickBar { get; } = new();
|
||||||
|
|
||||||
// ─── 使用者設定(視窗由 MainWindow 處理;VM 管單位 + MRU)─────
|
// ─── 使用者設定(視窗由 MainWindow 處理;VM 管單位 + MRU)─────
|
||||||
|
|
||||||
public void LoadSettings(AppSettings s)
|
public void LoadSettings(AppSettings s)
|
||||||
@@ -94,12 +97,14 @@ public partial class MainViewModel : ObservableObject
|
|||||||
RecentFiles.Clear();
|
RecentFiles.Clear();
|
||||||
foreach (string f in s.RecentFiles.Where(File.Exists).Take(SettingsService.MaxRecentFiles))
|
foreach (string f in s.RecentFiles.Where(File.Exists).Take(SettingsService.MaxRecentFiles))
|
||||||
RecentFiles.Add(f);
|
RecentFiles.Add(f);
|
||||||
|
QuickBar.Load(s.QuickBarKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettingsInto(AppSettings s)
|
public void SaveSettingsInto(AppSettings s)
|
||||||
{
|
{
|
||||||
s.UseInch = UseInch;
|
s.UseInch = UseInch;
|
||||||
s.RecentFiles = RecentFiles.ToList();
|
s.RecentFiles = RecentFiles.ToList();
|
||||||
|
s.QuickBarKeys = QuickBar.ToKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TouchRecent(string path)
|
private void TouchRecent(string path)
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace STPViewer.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>快速列單一按鈕的顯示開關(選單「快速列 → 自訂按鈕」的 checkbox 項)</summary>
|
||||||
|
public partial class QuickBarItemViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public bool Default { get; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isChecked;
|
||||||
|
|
||||||
|
public QuickBarItemViewModel(string key, string label, bool @default)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Label = label;
|
||||||
|
Default = @default;
|
||||||
|
isChecked = @default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速工具列自訂:所有功能都在選單列,快速列只放使用者勾選的常用按鈕。
|
||||||
|
/// XAML 以索引子綁定:Visibility="{Binding QuickBar[Point].IsChecked, Converter=…}"。
|
||||||
|
/// 勾選狀態隨 settings.json(QuickBarKeys)保存;null = 用預設。
|
||||||
|
/// </summary>
|
||||||
|
public partial class QuickBarViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
/// <summary>註冊表 = 快速列順序 + 選單顯示名 + 是否預設顯示(一般 user 常用)</summary>
|
||||||
|
private static readonly (string Key, string Label, bool Default)[] Registry =
|
||||||
|
{
|
||||||
|
("Import", "📂 匯入", true),
|
||||||
|
("Recent", "▾ 最近檔案", true),
|
||||||
|
("ZoomAll", "🔍 全覽", true),
|
||||||
|
("ViewIso", "視圖:等角", true),
|
||||||
|
("ViewFront", "視圖:前", false),
|
||||||
|
("ViewTop", "視圖:上", false),
|
||||||
|
("ViewRight", "視圖:右", false),
|
||||||
|
("Ortho", "正交投影", true),
|
||||||
|
("Point", "📍 點", true),
|
||||||
|
("Distance", "📏 距離", true),
|
||||||
|
("Edge", "📐 邊", false),
|
||||||
|
("Face", "⬛ 面", false),
|
||||||
|
("Circle", "⭕ 圓", true),
|
||||||
|
("Angle", "∠ 角度", true),
|
||||||
|
("FaceDistance", "⇔ 面距", false),
|
||||||
|
("Align", "🎯 兩點對齊", false),
|
||||||
|
("Align3", "🎯 三點對齊", false),
|
||||||
|
("RotateX", "旋轉 ↻X", false),
|
||||||
|
("RotateY", "旋轉 ↻Y", false),
|
||||||
|
("RotateZ", "旋轉 ↻Z", false),
|
||||||
|
("Drag", "🖐 拖曳", false),
|
||||||
|
("Gizmo", "⊹ 操作器", false),
|
||||||
|
("Interference", "🧩 干涉", true),
|
||||||
|
("Section", "✂ 剖面", true),
|
||||||
|
("Clear", "🧹 清除量測", true),
|
||||||
|
("Unit", "mm ⇄ in", true),
|
||||||
|
("ExportCsv", "💾 匯出 CSV", false),
|
||||||
|
("Screenshot", "📷 截圖", false),
|
||||||
|
("ExportStep", "📤 匯出 STEP", false),
|
||||||
|
("ExportStl", "📐 匯出 STL", false),
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly Dictionary<string, QuickBarItemViewModel> _byKey;
|
||||||
|
|
||||||
|
/// <summary>依快速列順序的全部項目(選單自訂清單用)</summary>
|
||||||
|
public IReadOnlyList<QuickBarItemViewModel> Items { get; }
|
||||||
|
|
||||||
|
public QuickBarViewModel()
|
||||||
|
{
|
||||||
|
Items = Registry.Select(r => new QuickBarItemViewModel(r.Key, r.Label, r.Default)).ToList();
|
||||||
|
_byKey = Items.ToDictionary(i => i.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>XAML 索引子綁定入口:QuickBar[Point].IsChecked</summary>
|
||||||
|
public QuickBarItemViewModel this[string key] => _byKey[key];
|
||||||
|
|
||||||
|
/// <summary>套用保存的勾選清單;null = 維持預設(含未知 key 容錯:新版本新增按鈕用預設值)</summary>
|
||||||
|
public void Load(List<string>? visibleKeys)
|
||||||
|
{
|
||||||
|
if (visibleKeys is null) return;
|
||||||
|
var set = new HashSet<string>(visibleKeys, StringComparer.Ordinal);
|
||||||
|
foreach (QuickBarItemViewModel item in Items)
|
||||||
|
item.IsChecked = set.Contains(item.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>目前勾選清單(存 settings.json)</summary>
|
||||||
|
public List<string> ToKeys() =>
|
||||||
|
Items.Where(i => i.IsChecked).Select(i => i.Key).ToList();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ResetDefaults()
|
||||||
|
{
|
||||||
|
foreach (QuickBarItemViewModel item in Items)
|
||||||
|
item.IsChecked = item.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SmokeTest;
|
||||||
|
|
||||||
|
/// <summary>產生測試用 binary STL:一片 gridN×gridN 的網格,每格 2 個三角形(波浪起伏,避免全共面)。</summary>
|
||||||
|
internal static class MakeStl
|
||||||
|
{
|
||||||
|
public static void Run(string path, int triCount)
|
||||||
|
{
|
||||||
|
// 每格 2 三角,格數 = triCount/2;邊長 grid = ceil(sqrt(cells))
|
||||||
|
int cells = System.Math.Max(1, triCount / 2);
|
||||||
|
int grid = (int)System.Math.Ceiling(System.Math.Sqrt(cells));
|
||||||
|
int tris = grid * grid * 2;
|
||||||
|
|
||||||
|
using var fs = File.Create(path);
|
||||||
|
using var bw = new BinaryWriter(fs);
|
||||||
|
bw.Write(new byte[80]); // header
|
||||||
|
bw.Write((uint)tris); // triangle count
|
||||||
|
|
||||||
|
static void WriteTri(BinaryWriter w, float ax, float ay, float az,
|
||||||
|
float bx, float by, float bz, float cx, float cy, float cz)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 3; i++) w.Write(0f); // normal (0,0,0)
|
||||||
|
w.Write(ax); w.Write(ay); w.Write(az);
|
||||||
|
w.Write(bx); w.Write(by); w.Write(bz);
|
||||||
|
w.Write(cx); w.Write(cy); w.Write(cz);
|
||||||
|
w.Write((ushort)0); // attribute byte count
|
||||||
|
}
|
||||||
|
|
||||||
|
float Z(int x, int y) => (float)(System.Math.Sin(x * 0.1) * System.Math.Cos(y * 0.1) * 5.0);
|
||||||
|
for (int y = 0; y < grid; y++)
|
||||||
|
for (int x = 0; x < grid; x++)
|
||||||
|
{
|
||||||
|
float x0 = x, x1 = x + 1, y0 = y, y1 = y + 1;
|
||||||
|
WriteTri(bw, x0, y0, Z(x, y), x1, y0, Z(x + 1, y), x1, y1, Z(x + 1, y + 1));
|
||||||
|
WriteTri(bw, x0, y0, Z(x, y), x1, y1, Z(x + 1, y + 1), x0, y1, Z(x, y + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"寫出 {tris:N0} 三角形 → {path} ({new FileInfo(path).Length:N0} bytes)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,12 @@ if (args[0] == "--make-dxf")
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args[0] == "--make-stl")
|
||||||
|
{
|
||||||
|
SmokeTest.MakeStl.Run(args[1], args.Length > 2 ? int.Parse(args[2]) : 1_500_000);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (args[0] == "--clip-test")
|
if (args[0] == "--clip-test")
|
||||||
return SmokeTest.ClipTest.Run() == 0 ? 0 : 2;
|
return SmokeTest.ClipTest.Run() == 0 ? 0 : 2;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user