Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaa72eac1e | ||
|
|
ae79222bf4 | ||
|
|
9072842bba | ||
|
|
dd15a83986 |
@@ -13,3 +13,7 @@ secret/*
|
|||||||
|
|
||||||
# AI 協作素材不入 git
|
# AI 協作素材不入 git
|
||||||
For_AI/
|
For_AI/
|
||||||
|
|
||||||
|
# 客戶 CAD 料號檔不入 git(避免外流);測試模型放 For_AI/
|
||||||
|
*.stp
|
||||||
|
*.step
|
||||||
|
|||||||
+32
-4
@@ -89,10 +89,9 @@ STPViewer/
|
|||||||
│ ├── run-STPViewer.ps1.example
|
│ ├── run-STPViewer.ps1.example
|
||||||
│ └── run-STPViewer.sh.example
|
│ └── run-STPViewer.sh.example
|
||||||
│
|
│
|
||||||
├── For_AI/ # 🚫 gitignored — AI 協作素材
|
├── For_AI/ # 🚫 gitignored — AI 協作素材 + 測試模型 *.stp(客戶料號不入 git)
|
||||||
│
|
│
|
||||||
├── STPViewer.sln
|
├── STPViewer.sln
|
||||||
├── *.stp # 根目錄現有測試模型(Amphenol connector)
|
|
||||||
│
|
│
|
||||||
└── src/
|
└── src/
|
||||||
└── STPViewer/
|
└── STPViewer/
|
||||||
@@ -102,7 +101,7 @@ STPViewer/
|
|||||||
│
|
│
|
||||||
├── Models/
|
├── Models/
|
||||||
│ ├── FaceInfo.cs # GeometryModel3D ↔ B-rep Face 對照(STL 為 null)
|
│ ├── FaceInfo.cs # GeometryModel3D ↔ B-rep Face 對照(STL 為 null)
|
||||||
│ ├── MeasureMode.cs # enum: None/Point/Distance/Edge/Face/Circle/Angle/FaceDistance/Align/Align3/Interference
|
│ ├── MeasureMode.cs # enum: None/Point/Distance/Edge/Face/Circle/Angle/FaceDistance/Align/Align3/Drag/Interference
|
||||||
│ ├── MeasurementResult.cs # 量測結果(雙單位 lambda、3D 標籤同步)
|
│ ├── MeasurementResult.cs # 量測結果(雙單位 lambda、3D 標籤同步)
|
||||||
│ └── UnitSystem.cs # mm/inch + Units 格式化
|
│ └── UnitSystem.cs # mm/inch + Units 格式化
|
||||||
│
|
│
|
||||||
@@ -147,7 +146,7 @@ class FaceInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum MeasureMode { None, Point, Distance, Edge, Face, Circle,
|
enum MeasureMode { None, Point, Distance, Edge, Face, Circle,
|
||||||
Angle, FaceDistance, Align, Interference }
|
Angle, FaceDistance, Align, Align3, Drag, Interference }
|
||||||
|
|
||||||
class MeasurementResult
|
class MeasurementResult
|
||||||
{
|
{
|
||||||
@@ -178,6 +177,8 @@ class MeasurementResult
|
|||||||
| 對齊 | 模式「對齊」+ 點「要移動零件」上一點、再點目標點 | 純平移該檔案使點1貼到點2(B-rep 用 `ModOp` 整體位移,量測清空) |
|
| 對齊 | 模式「對齊」+ 點「要移動零件」上一點、再點目標點 | 純平移該檔案使點1貼到點2(B-rep 用 `ModOp` 整體位移,量測清空) |
|
||||||
| 三點對齊 | 模式「三點」+ 來源檔 3 點、目標檔 3 對應點 | 旋轉+平移剛體變換(點1精確貼合、1→2 方向對齊、三點平面對齊;`RigidAlign`) |
|
| 三點對齊 | 模式「三點」+ 來源檔 3 點、目標檔 3 對應點 | 旋轉+平移剛體變換(點1精確貼合、1→2 方向對齊、三點平面對齊;`RigidAlign`) |
|
||||||
| 旋轉 | 樹面板選檔案 + 工具列 ↻X/↻Y/↻Z | 繞檔案中心 +90°(連按累加;方向不合時先轉正再對齊) |
|
| 旋轉 | 樹面板選檔案 + 工具列 ↻X/↻Y/↻Z | 繞檔案中心 +90°(連按累加;方向不合時先轉正再對齊) |
|
||||||
|
| 拖曳 | 模式「🖐 拖曳」(手形游標)+ 左鍵按住零件拖 | 沿螢幕平面移動該檔案,放開烘進 B-rep(拖曳中僅暫時 Transform,不卡) |
|
||||||
|
| 操作器 | 「⊹ 操作器」+ 樹面板選檔案 | XYZ 箭頭沿軸移動、旋轉環繞軸轉任意角度(與視角無關);每次放開烘進 B-rep |
|
||||||
| 干涉 | 工具列「🧩 干涉」(需剛好 2 個可見檔案) | 相交→紅色交線+相交三角形對數;無相交→最小間隙 gap(≈0 即配合 match) |
|
| 干涉 | 工具列「🧩 干涉」(需剛好 2 個可見檔案) | 相交→紅色交線+相交三角形對數;無相交→最小間隙 gap(≈0 即配合 match) |
|
||||||
| 剖面 | 工具列「✂ 剖面」+ 軸向/位置/反向 | CPU 裁切渲染網格(原始幾何保留,量測仍精確) |
|
| 剖面 | 工具列「✂ 剖面」+ 軸向/位置/反向 | CPU 裁切渲染網格(原始幾何保留,量測仍精確) |
|
||||||
| 單位 | 工具列 mm ⇄ in | 既有量測(清單+3D 標籤)即時換算 |
|
| 單位 | 工具列 mm ⇄ in | 既有量測(清單+3D 標籤)即時換算 |
|
||||||
@@ -314,6 +315,33 @@ NuGet 相依(自動還原):`CADability`、`HelixToolkit.Wpf`、`CommunityT
|
|||||||
|
|
||||||
**驗收:** AlignTest 8 項全過;公母連接器可旋轉擺正後三點對齊插合,再用干涉檢查驗證配合
|
**驗收:** AlignTest 8 項全過;公母連接器可旋轉擺正後三點對齊插合,再用干涉檢查驗證配合
|
||||||
|
|
||||||
|
### Phase 9 — 拖曳模式(工作量:M)
|
||||||
|
- [x] 🖐 拖曳模式(`MeasureMode.Drag`)— 手形游標,左鍵按住零件沿螢幕平面拖動(Helix `UnProject` 投影到過錨點、面向相機的平面)
|
||||||
|
- [x] 拖曳中僅掛暫時 `TranslateTransform3D`(GPU 端,大組件不卡),放開一次性 `TranslateRoot` 烘進 B-rep(量測精度不受影響)
|
||||||
|
- [x] `_mergedMap`(合併網格 → leaf 反查)支援瀏覽渲染下的 hit-test;拖曳中邊線暫停
|
||||||
|
- [x] 滑鼠捕捉(拖出視窗持續)、意外中斷以當前位置定格
|
||||||
|
|
||||||
|
**驗收:** 大組件手形拖曳流暢;放開後干涉/量測結果與拖曳位置一致;右鍵視角操作不受影響
|
||||||
|
|
||||||
|
> v0.2.1 修正:拖曳放開時清除暫時位移誤用 `Transform = null`,導致下次 `FindHits` 命中該檔案時
|
||||||
|
> HelixToolkit `GetTransform` 對 null transform `Add` 而 crash。改用 `Transform3D.Identity`。
|
||||||
|
|
||||||
|
### Phase 10 — Gizmo 三軸操作器(工作量:M)
|
||||||
|
- [x] 「⊹ 操作器」toggle — 對樹面板選取的檔案顯示 Helix `TranslateManipulator` ×3(XYZ 紅綠藍箭頭)+ `RotateManipulator` ×3(旋轉環),尺寸隨檔案 Bounds 自適應
|
||||||
|
- [x] 操作器綁定代理 `ModelVisual3D`,拖動中代理 Transform 即時套到目標所有 BodyVisual(暫時、GPU 端);邊線暫停
|
||||||
|
- [x] 放開滑鼠(`handledEventsToo` 捕捉)→ `Dispatcher.BeginInvoke` 延後 `TransformRoot` 烘進 B-rep、操作器歸零並移到新中心
|
||||||
|
- [x] 換樹選取自動換目標;目標被移除自動收掉;暫時 Transform 清除用 `Identity`(非 null)
|
||||||
|
|
||||||
|
**驗收:** 拖 X 箭頭零件僅沿世界 X 移動(與視角無關);拖環任意角度旋轉;放開後量測/干涉與顯示位置一致
|
||||||
|
|
||||||
|
### Phase 11 — Gizmo always-on-top 疊圖層(工作量:M)
|
||||||
|
- [x] 操作器移到獨立透明 `Viewport3D`(`gizmoOverlay`)疊在主視窗上 → 不在主場景,**永不被實體遮擋、永遠可抓**
|
||||||
|
- [x] `_overlayCamera` 在主相機 `Changed` 時同步(Position/方向/FOV/near-far);同尺寸 → 投影一致、操作器精準貼合
|
||||||
|
- [x] raw Viewport3D 空白處不吃滑鼠 → 穿透回主視窗(orbit/縮放/量測不受影響);`IsHitTestVisible` 綁 `GizmoEnabled`
|
||||||
|
- [x] 放開事件改掛 overlay(`handledEventsToo`);`_gizmoBakePending` 防同次重複烘焙
|
||||||
|
|
||||||
|
**驗收:** 操作器從任何視角都浮在零件上可見可抓;開啟操作器時右鍵 orbit / 量測仍正常(穿透)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Future Extensions(下一輪)
|
## Future Extensions(下一輪)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
CAD 3D 檢視器(Windows 桌面 WPF, .NET 8):STEP/STL/DXF 匯入、STEP 裝配樹、
|
CAD 3D 檢視器(Windows 桌面 WPF, .NET 8):STEP/STL/DXF 匯入、STEP 裝配樹、
|
||||||
點/距離/邊/面/圓/角度/面距量測、兩點對齊(平移)、三點對齊(旋轉+平移)、軸向旋轉 90°、
|
點/距離/邊/面/圓/角度/面距量測、兩點對齊(平移)、三點對齊(旋轉+平移)、軸向旋轉 90°、
|
||||||
干涉檢查、剖面、mm⇄inch、CSV/截圖匯出。
|
拖曳模式(手形游標直接拖零件)、干涉檢查、剖面、mm⇄inch、CSV/截圖匯出。
|
||||||
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
詳細設計見 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
||||||
|
|
||||||
## 技術棧
|
## 技術棧
|
||||||
@@ -21,7 +21,8 @@ dotnet run --project src/STPViewer
|
|||||||
dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
||||||
```
|
```
|
||||||
|
|
||||||
測試模型:根目錄 `Amphenol RA PHD GPD20-50075_RevB_DC (for Cable).stp`
|
測試模型放 `For_AI/`(gitignored,**客戶料號不入 git**):`For_AI/test.stp`(Amphenol RA PHD 座端,小檔)與
|
||||||
|
`For_AI/Amphenol PHD to PHD Cable 10201248 1.stp`(39MB 大組件,效能測試用)。`*.stp`/`*.step` 已全域 gitignore
|
||||||
|
|
||||||
## 開發慣例
|
## 開發慣例
|
||||||
|
|
||||||
@@ -39,6 +40,21 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
|||||||
(勿逐面位移,會重複位移共用邊),網格/合併網格/邊線/邊界同步重算;變換後量測已失效要 `ClearMeasurements()`。
|
(勿逐面位移,會重複位移共用邊),網格/合併網格/邊線/邊界同步重算;變換後量測已失效要 `ClearMeasurements()`。
|
||||||
**op 與 m 必須是同一個變換** — 數學在 `Services/RigidAlign.cs`:WPF `Matrix3D` 是「列向量」約定、CADability `ModOp` 是「行向量」約定,
|
**op 與 m 必須是同一個變換** — 數學在 `Services/RigidAlign.cs`:WPF `Matrix3D` 是「列向量」約定、CADability `ModOp` 是「行向量」約定,
|
||||||
`ToModOp` 負責轉置轉換,改動務必跑 `SmokeTest --align-test` 驗證兩種表示一致,否則 B-rep 與顯示網格會悄悄分家
|
`ToModOp` 負責轉置轉換,改動務必跑 `SmokeTest --align-test` 驗證兩種表示一致,否則 B-rep 與顯示網格會悄悄分家
|
||||||
|
- 拖曳模式(`MeasureMode.Drag`):拖曳中只掛**暫時 `TranslateTransform3D`**(GPU 免費),放開才一次性 `TranslateRoot` 烘進 B-rep —
|
||||||
|
**不要改成拖曳中逐幀 TransformRoot**(大檔每幀重建網格會卡死)。2D→3D 用 Helix `UnProject`(過錨點、法向=相機 LookDirection 的平面)。
|
||||||
|
合併網格的 hit-test 走 `_mergedMap`(合併 Model → leaf);拖曳中邊線用 `_edgesSuspended` 暫停
|
||||||
|
- ⚠️ **`Visual3D.Transform` 永遠不要設成 `null`,清除要用 `Transform3D.Identity`**。HelixToolkit `Viewport3DHelper.GetTransform`
|
||||||
|
對 `child.Transform` 沒做 null 檢查(`GeneralTransform3DGroup.Children.Add(null)` → 拋「無法新增空值到集合中」),
|
||||||
|
之後任何 `FindHits` 都會 crash。v0.2.1 修的就是拖曳放開時把 BodyVisual.Transform 設 null(拖過一次後再點擊就炸)。
|
||||||
|
Gizmo 的暫時 Transform 清除同理一律用 Identity
|
||||||
|
- Gizmo 操作器:Helix `TranslateManipulator`/`RotateManipulator` ×6 `Bind` 到代理 `ModelVisual3D`,
|
||||||
|
代理 Transform 變更(`DependencyPropertyDescriptor.AddValueChanged`)即時套到目標 BodyVisual(暫時);
|
||||||
|
**放開滑鼠才 `TransformRoot` 烘焙** — MouseUp 被 manipulator 標 handled,MainWindow 用 `AddHandler(..., handledEventsToo: true)` 才收得到。
|
||||||
|
烘焙用 `Dispatcher.BeginInvoke` 延後到 manipulator 自身事件處理完,避免 reentrancy;`_gizmoBaking` 旗標防 Transform 歸零的回呼重入
|
||||||
|
- Gizmo always-on-top(v0.3.1):操作器放在獨立透明 `Viewport3D`(`gizmoOverlay`)疊在主視窗上,**不在主場景所以永不被實體遮擋**。
|
||||||
|
`_overlayCamera` 在主 `Camera.Changed` 時同步主相機(Position/方向/FOV/near-far);raw Viewport3D 空白處不吃滑鼠 → 穿透回主視窗(orbit/量測正常);
|
||||||
|
`IsHitTestVisible` 綁 `GizmoEnabled`。Manipulator 是 `UIElement3D`、用 `GetViewport3D()` 抓所在層相機,故在疊圖層用同步相機運作。
|
||||||
|
放開事件靠 overlay 的 `AddHandler(MouseLeftButtonUp, handledEventsToo:true)`(manipulator 會標 handled),`_gizmoBakePending` 防同次重複烘焙
|
||||||
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 干涉/面距/對齊等運算在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
- 匯入在背景執行緒;`Freeze()` 幾何後才跨執行緒
|
||||||
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
- Commit 格式:Conventional Commits(`feat:` / `fix:` / `docs:` …)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ STP/STEP 3D 檢視器(Windows 桌面程式,C# .NET 8 WPF)— 多檔匯入
|
|||||||
| ⤚ 對齊 | 點「要移動零件」一點 + 目標點 → 純平移該檔案使兩點貼合(B-rep 整體位移) |
|
| ⤚ 對齊 | 點「要移動零件」一點 + 目標點 → 純平移該檔案使兩點貼合(B-rep 整體位移) |
|
||||||
| 🎯 三點 | 來源檔 3 特徵點 + 目標檔 3 對應點 → 旋轉+平移一次貼合(方向不同也能對) |
|
| 🎯 三點 | 來源檔 3 特徵點 + 目標檔 3 對應點 → 旋轉+平移一次貼合(方向不同也能對) |
|
||||||
- **旋轉**:樹面板選檔案 + 工具列 ↻X/↻Y/↻Z 繞中心 +90°(擺正方向用,連按累加)
|
- **旋轉**:樹面板選檔案 + 工具列 ↻X/↻Y/↻Z 繞中心 +90°(擺正方向用,連按累加)
|
||||||
|
- **拖曳** 🖐:手形游標模式,左鍵按住零件沿螢幕平面拖動、放開定位(粗定位用;精確貼合用對齊);右鍵轉視角不受影響
|
||||||
|
- **操作器** ⊹:樹面板選檔案 → 顯示 XYZ 三色箭頭 + 旋轉環(Fusion 360 風格);拖箭頭沿該軸移動(與視角無關,適合沿插合軸推進)、拖環繞軸轉任意角度,放開即定位。操作器永遠浮在最上層、不被實體遮擋
|
||||||
- **干涉檢查** 🧩:勾選剛好 2 個可見檔案 → 相交時顯示紅色干涉交線 + 相交三角形對數;無相交時回報最小間隙 gap(gap≈0 即為配合 match,共面貼合不算干涉)
|
- **干涉檢查** 🧩:勾選剛好 2 個可見檔案 → 相交時顯示紅色干涉交線 + 相交三角形對數;無相交時回報最小間隙 gap(gap≈0 即為配合 match,共面貼合不算干涉)
|
||||||
- **剖面**:✂ 開關 + X/Y/Z 軸 + 位置滑桿 + 反向;CPU 網格裁切,原始幾何保留(量測不受影響)
|
- **剖面**:✂ 開關 + X/Y/Z 軸 + 位置滑桿 + 反向;CPU 網格裁切,原始幾何保留(量測不受影響)
|
||||||
- **單位**:mm ⇄ inch 一鍵切換,既有量測(清單與 3D 標籤)即時換算
|
- **單位**:mm ⇄ inch 一鍵切換,既有量測(清單與 3D 標籤)即時換算
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
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 檢視器"
|
Title="STPViewer V0.3.1 — STP/STEP 3D 檢視器"
|
||||||
Height="820" Width="1320"
|
Height="820" Width="1320"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver">
|
AllowDrop="True" Drop="Window_Drop" DragOver="Window_DragOver">
|
||||||
@@ -59,6 +59,11 @@
|
|||||||
ToolTip="選取的檔案繞 Y 軸 +90°">↻Y</Button>
|
ToolTip="選取的檔案繞 Y 軸 +90°">↻Y</Button>
|
||||||
<Button Command="{Binding RotateRootCommand}" CommandParameter="Z" Padding="6,4" FontSize="13"
|
<Button Command="{Binding RotateRootCommand}" CommandParameter="Z" Padding="6,4" FontSize="13"
|
||||||
ToolTip="選取的檔案繞 Z 軸 +90°">↻Z</Button>
|
ToolTip="選取的檔案繞 Z 軸 +90°">↻Z</Button>
|
||||||
|
<ToggleButton Style="{StaticResource ModeButton}"
|
||||||
|
ToolTip="拖曳模式:左鍵按住零件沿螢幕平面拖動,放開定位(深度方向先轉視角再拖)"
|
||||||
|
IsChecked="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}">🖐 拖曳</ToggleButton>
|
||||||
|
<ToggleButton IsChecked="{Binding GizmoEnabled}" Padding="8,4" FontSize="13"
|
||||||
|
ToolTip="三軸操作器:對樹面板選取的檔案顯示 XYZ 箭頭(沿軸移動)與旋轉環(繞軸轉任意角度),每次放開即定位">⊹ 操作器</ToggleButton>
|
||||||
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
<Button Command="{Binding CheckInterferenceCommand}" Padding="8,4" FontSize="13"
|
||||||
ToolTip="干涉檢查:檢查 2 個可見檔案是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
ToolTip="干涉檢查:檢查 2 個可見檔案是否相交(match 判定);無相交時回報最小間隙">🧩 干涉</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -178,7 +183,20 @@
|
|||||||
ShowViewCube="True" ShowCoordinateSystem="True"
|
ShowViewCube="True" ShowCoordinateSystem="True"
|
||||||
ShowFrameRate="False" ZoomExtentsWhenLoaded="True"
|
ShowFrameRate="False" ZoomExtentsWhenLoaded="True"
|
||||||
PanGesture="MiddleClick" PanGesture2="Shift+LeftClick"
|
PanGesture="MiddleClick" PanGesture2="Shift+LeftClick"
|
||||||
MouseLeftButtonDown="Viewport_MouseLeftButtonDown">
|
MouseLeftButtonDown="Viewport_MouseLeftButtonDown"
|
||||||
|
MouseMove="Viewport_MouseMove"
|
||||||
|
MouseLeftButtonUp="Viewport_MouseLeftButtonUp"
|
||||||
|
LostMouseCapture="Viewport_LostMouseCapture">
|
||||||
|
<h:HelixViewport3D.Style>
|
||||||
|
<Style TargetType="h:HelixViewport3D">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Value="True"
|
||||||
|
Binding="{Binding CurrentMode, Converter={StaticResource ModeConv}, ConverterParameter=Drag}">
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</h:HelixViewport3D.Style>
|
||||||
<h:HelixViewport3D.Background>
|
<h:HelixViewport3D.Background>
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||||
<GradientStop Color="#FFEAF2F8" Offset="0" />
|
<GradientStop Color="#FFEAF2F8" Offset="0" />
|
||||||
@@ -188,6 +206,11 @@
|
|||||||
<h:DefaultLights />
|
<h:DefaultLights />
|
||||||
</h:HelixViewport3D>
|
</h:HelixViewport3D>
|
||||||
|
|
||||||
|
<!-- 操作器疊圖層:透明 Viewport3D 疊在主視窗上、永遠最上層。
|
||||||
|
只在操作器開啟時 IsHitTestVisible(否則空白處滑鼠才會穿透回主視窗) -->
|
||||||
|
<Viewport3D x:Name="gizmoOverlay" Grid.Column="1"
|
||||||
|
IsHitTestVisible="{Binding GizmoEnabled}" />
|
||||||
|
|
||||||
<!-- ─── 量測結果面板 ─── -->
|
<!-- ─── 量測結果面板 ─── -->
|
||||||
<GroupBox Grid.Column="2" Header="量測結果" Margin="4">
|
<GroupBox Grid.Column="2" Header="量測結果" Margin="4">
|
||||||
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
<ListBox ItemsSource="{Binding Measurements}" HorizontalContentAlignment="Stretch"
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ public partial class MainWindow : Window
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = _vm;
|
DataContext = _vm;
|
||||||
_vm.Attach(viewport);
|
_vm.Attach(viewport);
|
||||||
|
_vm.AttachOverlay(gizmoOverlay);
|
||||||
Loaded += MainWindow_Loaded;
|
Loaded += MainWindow_Loaded;
|
||||||
|
|
||||||
|
// Gizmo 操作器放在疊圖層;manipulator 會把 MouseUp 標 handled,需 handledEventsToo 才收得到
|
||||||
|
gizmoOverlay.AddHandler(MouseLeftButtonUpEvent,
|
||||||
|
new MouseButtonEventHandler((_, _) => _vm.OnGizmoMouseUp()), handledEventsToo: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …</summary>
|
/// <summary>支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …</summary>
|
||||||
@@ -32,11 +37,36 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void Viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
private void Viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
// Shift+左鍵 = 平移(PanGesture2),不觸發量測
|
// Shift+左鍵 = 平移(PanGesture2),不觸發量測/拖曳
|
||||||
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) return;
|
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) return;
|
||||||
|
|
||||||
|
if (_vm.CurrentMode == STPViewer.Models.MeasureMode.Drag)
|
||||||
|
{
|
||||||
|
if (_vm.OnDragStart(e.GetPosition(viewport.Viewport)))
|
||||||
|
{
|
||||||
|
viewport.CaptureMouse(); // 拖出視窗外也持續收到 Move/Up
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
_vm.OnViewportClick(e.GetPosition(viewport.Viewport));
|
_vm.OnViewportClick(e.GetPosition(viewport.Viewport));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Viewport_MouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton == MouseButtonState.Pressed)
|
||||||
|
_vm.OnDragMove(e.GetPosition(viewport.Viewport));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Viewport_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (_vm.OnDragEnd())
|
||||||
|
viewport.ReleaseMouseCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Viewport_LostMouseCapture(object sender, MouseEventArgs e) =>
|
||||||
|
_vm.OnDragEnd(); // 捕捉意外中斷(Alt+Tab 等)→ 以目前位置定格,不留懸空 Transform
|
||||||
|
|
||||||
private void Window_DragOver(object sender, DragEventArgs e)
|
private void Window_DragOver(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
e.Effects = HasSupportedFiles(e) ? DragDropEffects.Copy : DragDropEffects.None;
|
e.Effects = HasSupportedFiles(e) ? DragDropEffects.Copy : DragDropEffects.None;
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ public enum MeasureMode
|
|||||||
FaceDistance, // 面到面最短距離(網格近似)
|
FaceDistance, // 面到面最短距離(網格近似)
|
||||||
Align, // 兩點對齊:平移零件使點 1 貼到點 2
|
Align, // 兩點對齊:平移零件使點 1 貼到點 2
|
||||||
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
|
||||||
|
Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
|
||||||
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ public partial class MainViewModel : ObservableObject
|
|||||||
/// <summary>渲染物件 → B-rep Face 反查表(HitTest 用;overlay 不在表內所以自然被忽略)</summary>
|
/// <summary>渲染物件 → B-rep Face 反查表(HitTest 用;overlay 不在表內所以自然被忽略)</summary>
|
||||||
private readonly Dictionary<Model3D, FaceInfo> _faceMap = new();
|
private readonly Dictionary<Model3D, FaceInfo> _faceMap = new();
|
||||||
|
|
||||||
|
/// <summary>合併網格 → leaf 反查表(拖曳模式在瀏覽渲染下 hit-test 用)</summary>
|
||||||
|
private readonly Dictionary<Model3D, ModelNodeViewModel> _mergedMap = new();
|
||||||
|
|
||||||
|
// 拖曳模式:暫時 Transform 跟著滑鼠,放開才一次性烘進 B-rep(TranslateRoot)
|
||||||
|
private ModelNodeViewModel? _dragRoot;
|
||||||
|
private Point3D _dragAnchor; // 命中點(世界座標),拖曳平面的錨點
|
||||||
|
private Vector3D _dragApplied; // 目前累計位移
|
||||||
|
private TranslateTransform3D? _dragTransform;
|
||||||
|
|
||||||
private HelixViewport3D? _viewport;
|
private HelixViewport3D? _viewport;
|
||||||
private int _paletteIndex;
|
private int _paletteIndex;
|
||||||
private readonly Dictionary<MeasureMode, int> _counters = new();
|
private readonly Dictionary<MeasureMode, int> _counters = new();
|
||||||
@@ -103,8 +112,33 @@ public partial class MainViewModel : ObservableObject
|
|||||||
viewport.Camera.Changed += (_, _) => OnCameraMoved();
|
viewport.Camera.Changed += (_, _) => OnCameraMoved();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>注入操作器疊圖層(透明 Viewport3D,疊在主視窗上、永遠最上層)</summary>
|
||||||
|
public void AttachOverlay(System.Windows.Controls.Viewport3D overlay)
|
||||||
|
{
|
||||||
|
_overlayViewport = overlay;
|
||||||
|
_overlayCamera = new PerspectiveCamera();
|
||||||
|
overlay.Camera = _overlayCamera;
|
||||||
|
// 操作器材質需打光(manipulator 用 DiffuseMaterial)
|
||||||
|
overlay.Children.Add(new ModelVisual3D { Content = new AmbientLight(Color.FromRgb(0x80, 0x80, 0x80)) });
|
||||||
|
overlay.Children.Add(new ModelVisual3D { Content = new DirectionalLight(Colors.White, new Vector3D(-1, -1, -3)) });
|
||||||
|
SyncOverlayCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>疊圖層相機跟隨主相機(每次主相機變更時呼叫,讓操作器疊在正確螢幕位置)</summary>
|
||||||
|
private void SyncOverlayCamera()
|
||||||
|
{
|
||||||
|
if (_overlayCamera is null || _viewport?.Camera is not ProjectionCamera src) return;
|
||||||
|
_overlayCamera.Position = src.Position;
|
||||||
|
_overlayCamera.LookDirection = src.LookDirection;
|
||||||
|
_overlayCamera.UpDirection = src.UpDirection;
|
||||||
|
_overlayCamera.NearPlaneDistance = src.NearPlaneDistance;
|
||||||
|
_overlayCamera.FarPlaneDistance = src.FarPlaneDistance;
|
||||||
|
if (src is PerspectiveCamera p) _overlayCamera.FieldOfView = p.FieldOfView;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCameraMoved()
|
private void OnCameraMoved()
|
||||||
{
|
{
|
||||||
|
SyncOverlayCamera(); // 操作器疊圖層跟著主相機
|
||||||
if (!_edgesSuspended) { _edgesSuspended = true; SetEdgesActive(false); }
|
if (!_edgesSuspended) { _edgesSuspended = true; SetEdgesActive(false); }
|
||||||
_interactionTimer.Stop();
|
_interactionTimer.Stop();
|
||||||
_interactionTimer.Start();
|
_interactionTimer.Start();
|
||||||
@@ -257,11 +291,13 @@ public partial class MainViewModel : ObservableObject
|
|||||||
mergedModel.BackMaterial = backMat;
|
mergedModel.BackMaterial = backMat;
|
||||||
vm.MergedContent = new Model3DGroup();
|
vm.MergedContent = new Model3DGroup();
|
||||||
vm.MergedContent.Children.Add(mergedModel);
|
vm.MergedContent.Children.Add(mergedModel);
|
||||||
|
_mergedMap[mergedModel] = vm;
|
||||||
|
|
||||||
// 預設瀏覽模式 → 顯示合併網格(量測模式時由 ApplyRenderMode 換回逐面)
|
// 預設瀏覽模式 → 顯示合併網格(量測模式時由 ApplyRenderMode 換回逐面)
|
||||||
vm.BodyVisual = new ModelVisual3D
|
vm.BodyVisual = new ModelVisual3D
|
||||||
{
|
{
|
||||||
Content = (CurrentMode == MeasureMode.None && !SectionEnabled) ? vm.MergedContent : group,
|
Content = (CurrentMode is MeasureMode.None or MeasureMode.Drag && !SectionEnabled)
|
||||||
|
? vm.MergedContent : group,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +413,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void ApplyRenderMode()
|
private void ApplyRenderMode()
|
||||||
{
|
{
|
||||||
bool browse = CurrentMode == MeasureMode.None && !SectionEnabled;
|
bool browse = CurrentMode is MeasureMode.None or MeasureMode.Drag && !SectionEnabled;
|
||||||
foreach (ModelNodeViewModel leaf in Roots.SelectMany(r => r.Leaves()))
|
foreach (ModelNodeViewModel leaf in Roots.SelectMany(r => r.Leaves()))
|
||||||
{
|
{
|
||||||
if (leaf.BodyVisual is null) continue;
|
if (leaf.BodyVisual is null) continue;
|
||||||
@@ -404,6 +440,8 @@ public partial class MainViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (_viewport is null || !root.IsRoot) return;
|
if (_viewport is null || !root.IsRoot) return;
|
||||||
|
|
||||||
|
if (ReferenceEquals(root, _gizmoTarget)) GizmoEnabled = false; // 目標被移除 → 收掉操作器
|
||||||
|
|
||||||
// 量測 overlay 可能標在此檔模型上 → 一併清除(簡化:全清)
|
// 量測 overlay 可能標在此檔模型上 → 一併清除(簡化:全清)
|
||||||
if (Measurements.Count > 0) ClearMeasurements();
|
if (Measurements.Count > 0) ClearMeasurements();
|
||||||
|
|
||||||
@@ -414,6 +452,9 @@ public partial class MainViewModel : ObservableObject
|
|||||||
if (leaf.FacesContent is not null) // 逐面群組才在 _faceMap(與目前顯示哪種內容無關)
|
if (leaf.FacesContent is not null) // 逐面群組才在 _faceMap(與目前顯示哪種內容無關)
|
||||||
foreach (Model3D m in leaf.FacesContent.Children)
|
foreach (Model3D m in leaf.FacesContent.Children)
|
||||||
_faceMap.Remove(m);
|
_faceMap.Remove(m);
|
||||||
|
if (leaf.MergedContent is not null)
|
||||||
|
foreach (Model3D m in leaf.MergedContent.Children)
|
||||||
|
_mergedMap.Remove(m);
|
||||||
}
|
}
|
||||||
if (root.EdgeVisual is not null)
|
if (root.EdgeVisual is not null)
|
||||||
_viewport.Children.Remove(root.EdgeVisual);
|
_viewport.Children.Remove(root.EdgeVisual);
|
||||||
@@ -439,6 +480,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
MeasureMode.FaceDistance => "面距量測:點選第 1 個面",
|
MeasureMode.FaceDistance => "面距量測:點選第 1 個面",
|
||||||
MeasureMode.Align => "兩點對齊:先點「要移動零件」上的點,再點目標點(純平移,量測會清空)",
|
MeasureMode.Align => "兩點對齊:先點「要移動零件」上的點,再點目標點(純平移,量測會清空)",
|
||||||
MeasureMode.Align3 => "三點對齊:先在「要移動的檔案」點 3 個特徵點,再到目標檔案點 3 個對應點(旋轉+平移)",
|
MeasureMode.Align3 => "三點對齊:先在「要移動的檔案」點 3 個特徵點,再到目標檔案點 3 個對應點(旋轉+平移)",
|
||||||
|
MeasureMode.Drag => "拖曳模式:左鍵按住零件拖動(沿螢幕平面),放開定位;深度方向先轉視角再拖。右鍵仍可轉視角",
|
||||||
_ => StatusText,
|
_ => StatusText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -453,7 +495,7 @@ public partial class MainViewModel : ObservableObject
|
|||||||
/// <summary>MainWindow 滑鼠左鍵點擊轉發進來</summary>
|
/// <summary>MainWindow 滑鼠左鍵點擊轉發進來</summary>
|
||||||
public void OnViewportClick(Point position)
|
public void OnViewportClick(Point position)
|
||||||
{
|
{
|
||||||
if (_viewport is null || IsBusy || CurrentMode == MeasureMode.None) return;
|
if (_viewport is null || IsBusy || CurrentMode is MeasureMode.None or MeasureMode.Drag) return;
|
||||||
|
|
||||||
var hits = _viewport.Viewport.FindHits(position);
|
var hits = _viewport.Viewport.FindHits(position);
|
||||||
FaceInfo? fi = null;
|
FaceInfo? fi = null;
|
||||||
@@ -863,6 +905,239 @@ public partial class MainViewModel : ObservableObject
|
|||||||
|
|
||||||
// 三點對齊的剛體變換數學在 Services/RigidAlign.cs(SmokeTest --align-test 驗證)
|
// 三點對齊的剛體變換數學在 Services/RigidAlign.cs(SmokeTest --align-test 驗證)
|
||||||
|
|
||||||
|
// ─── 拖曳模式(左鍵按住零件沿螢幕平面拖;放開才烘進 B-rep)──
|
||||||
|
|
||||||
|
/// <summary>左鍵按下:命中零件則開始拖曳(回傳 true 表示要捕捉滑鼠)</summary>
|
||||||
|
public bool OnDragStart(Point position)
|
||||||
|
{
|
||||||
|
if (_viewport is null || IsBusy || CurrentMode != MeasureMode.Drag || _dragRoot is not null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 兩種渲染內容都可能在場:合併網格查 _mergedMap、逐面(剖面時)查 _faceMap
|
||||||
|
ModelNodeViewModel? leaf = null;
|
||||||
|
Point3D anchor = default;
|
||||||
|
foreach (var h in _viewport.Viewport.FindHits(position))
|
||||||
|
{
|
||||||
|
if (h.Model is null) continue;
|
||||||
|
if (_faceMap.TryGetValue(h.Model, out FaceInfo? fi)) { leaf = fi.Owner; anchor = h.Position; break; }
|
||||||
|
if (_mergedMap.TryGetValue(h.Model, out ModelNodeViewModel? ml)) { leaf = ml; anchor = h.Position; break; }
|
||||||
|
}
|
||||||
|
if (leaf is null) return false;
|
||||||
|
ModelNodeViewModel? root = FindRootOf(leaf) ?? RootContaining(leaf);
|
||||||
|
if (root is null) return false;
|
||||||
|
|
||||||
|
_dragRoot = root;
|
||||||
|
_dragAnchor = anchor;
|
||||||
|
_dragApplied = default;
|
||||||
|
_dragTransform = new TranslateTransform3D();
|
||||||
|
foreach (ModelNodeViewModel l in root.Leaves())
|
||||||
|
if (l.BodyVisual is not null)
|
||||||
|
l.BodyVisual.Transform = _dragTransform;
|
||||||
|
|
||||||
|
// 拖曳中隱藏邊線(LinesVisual3D 隨 Transform 變更逐幀重建會卡)
|
||||||
|
_edgesSuspended = true;
|
||||||
|
SetEdgesActive(false);
|
||||||
|
StatusText = $"拖曳「{root.Name}」中…(放開定位)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>滑鼠移動:把 2D 位移投影到「過錨點、面向相機」的平面上 → 暫時 Transform</summary>
|
||||||
|
public void OnDragMove(Point position)
|
||||||
|
{
|
||||||
|
if (_viewport?.Camera is not System.Windows.Media.Media3D.ProjectionCamera cam ||
|
||||||
|
_dragRoot is null || _dragTransform is null) return;
|
||||||
|
|
||||||
|
Point3D? p = _viewport.Viewport.UnProject(position, _dragAnchor, cam.LookDirection);
|
||||||
|
if (p is null) return;
|
||||||
|
_dragApplied = p.Value - _dragAnchor;
|
||||||
|
_dragTransform.OffsetX = _dragApplied.X;
|
||||||
|
_dragTransform.OffsetY = _dragApplied.Y;
|
||||||
|
_dragTransform.OffsetZ = _dragApplied.Z;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>放開:移除暫時 Transform,一次性烘進 B-rep(量測精度不受拖曳影響)</summary>
|
||||||
|
public bool OnDragEnd()
|
||||||
|
{
|
||||||
|
if (_dragRoot is null) return false;
|
||||||
|
ModelNodeViewModel root = _dragRoot;
|
||||||
|
Vector3D delta = _dragApplied;
|
||||||
|
|
||||||
|
// 清除暫時位移:必須用 Identity,不可用 null —— HelixToolkit GetTransform 對 child.Transform
|
||||||
|
// 不做 null 檢查(Children.Add(null) 會拋「無法新增空值到集合中」),下次 FindHits 即 crash
|
||||||
|
foreach (ModelNodeViewModel l in root.Leaves())
|
||||||
|
if (l.BodyVisual is not null)
|
||||||
|
l.BodyVisual.Transform = Transform3D.Identity;
|
||||||
|
_dragRoot = null;
|
||||||
|
_dragTransform = null;
|
||||||
|
_dragApplied = default;
|
||||||
|
_edgesSuspended = false;
|
||||||
|
SetEdgesActive(true); // 恢復所有檔案的邊線(被拖檔案的點隨後由 TranslateRoot 更新,期間不會渲染)
|
||||||
|
|
||||||
|
if (delta.LengthSquared > 1e-12)
|
||||||
|
{
|
||||||
|
TranslateRoot(root, delta); // 烘進 B-rep + 網格 + 邊線 + 邊界(量測清空)
|
||||||
|
StatusText = $"已拖曳「{root.Name}」 Δ({delta.X:F2}, {delta.Y:F2}, {delta.Z:F2}) mm(可用 🧩干涉 驗證)";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Gizmo 三軸操作器(XYZ 箭頭 + 旋轉環;每次放開滑鼠烘進 B-rep)──
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool gizmoEnabled;
|
||||||
|
|
||||||
|
private readonly List<HelixToolkit.Wpf.Manipulator> _gizmoParts = new();
|
||||||
|
private ModelVisual3D? _gizmoProxy; // 操作器綁定的代理 visual(其 Transform = 使用者拖出的變換)
|
||||||
|
private ModelNodeViewModel? _gizmoTarget;
|
||||||
|
private bool _gizmoDragActive; // 拖動中(邊線已暫停)
|
||||||
|
private bool _gizmoBaking; // 烘焙中,忽略 Transform 變更回呼
|
||||||
|
private bool _gizmoBakePending; // 已排程烘焙,防同一次放開重複觸發
|
||||||
|
|
||||||
|
// 操作器疊圖層:另一個透明 Viewport3D 疊在主視窗上、相機同步、只放操作器 →
|
||||||
|
// 操作器不在主場景,永不被實體遮擋(always-on-top);空白處滑鼠穿透回主視窗
|
||||||
|
private System.Windows.Controls.Viewport3D? _overlayViewport;
|
||||||
|
private PerspectiveCamera? _overlayCamera;
|
||||||
|
|
||||||
|
partial void OnGizmoEnabledChanged(bool value) => UpdateGizmo();
|
||||||
|
|
||||||
|
partial void OnSelectedNodeChanged(ModelNodeViewModel? value)
|
||||||
|
{
|
||||||
|
if (GizmoEnabled) UpdateGizmo(); // 換選取 → 操作器跟著換目標
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGizmo()
|
||||||
|
{
|
||||||
|
RemoveGizmo();
|
||||||
|
if (!GizmoEnabled || _viewport is null || _overlayViewport is null) return;
|
||||||
|
|
||||||
|
ModelNodeViewModel? root = SelectedNode is not null ? RootContaining(SelectedNode)
|
||||||
|
: Roots.Count == 1 ? Roots[0] : null;
|
||||||
|
if (root is null || root.Bounds.IsEmpty)
|
||||||
|
{
|
||||||
|
StatusText = "操作器:請先在樹面板點選要操作的檔案";
|
||||||
|
GizmoEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_gizmoTarget = root;
|
||||||
|
SyncOverlayCamera(); // 操作器出現前先對齊相機
|
||||||
|
|
||||||
|
Rect3D b = root.Bounds;
|
||||||
|
var center = new Point3D(b.X + b.SizeX / 2, b.Y + b.SizeY / 2, b.Z + b.SizeZ / 2);
|
||||||
|
double diag = new Vector3D(b.SizeX, b.SizeY, b.SizeZ).Length;
|
||||||
|
|
||||||
|
_gizmoProxy = new ModelVisual3D();
|
||||||
|
_overlayViewport.Children.Add(_gizmoProxy);
|
||||||
|
System.ComponentModel.DependencyPropertyDescriptor
|
||||||
|
.FromProperty(Visual3D.TransformProperty, typeof(Visual3D))
|
||||||
|
.AddValueChanged(_gizmoProxy, GizmoTransformChanged);
|
||||||
|
|
||||||
|
void AddPart(HelixToolkit.Wpf.Manipulator man)
|
||||||
|
{
|
||||||
|
man.Position = center;
|
||||||
|
man.Bind(_gizmoProxy);
|
||||||
|
_gizmoParts.Add(man);
|
||||||
|
_overlayViewport!.Children.Add(man); // 放疊圖層 → 永不被實體遮擋
|
||||||
|
}
|
||||||
|
// 平移箭頭(X 紅 / Y 綠 / Z 藍 — 業界慣例)
|
||||||
|
AddPart(new TranslateManipulator { Direction = new Vector3D(1, 0, 0), Color = Colors.Red, Length = diag * 0.22, Diameter = diag * 0.016 });
|
||||||
|
AddPart(new TranslateManipulator { Direction = new Vector3D(0, 1, 0), Color = Colors.Green, Length = diag * 0.22, Diameter = diag * 0.016 });
|
||||||
|
AddPart(new TranslateManipulator { Direction = new Vector3D(0, 0, 1), Color = Colors.Blue, Length = diag * 0.22, Diameter = diag * 0.016 });
|
||||||
|
// 旋轉環
|
||||||
|
AddPart(new RotateManipulator { Axis = new Vector3D(1, 0, 0), Color = Colors.Red, 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 });
|
||||||
|
|
||||||
|
StatusText = $"操作器:拖箭頭沿軸移動「{root.Name}」、拖環繞軸旋轉;放開即定位";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveGizmo()
|
||||||
|
{
|
||||||
|
if (_gizmoProxy is not null)
|
||||||
|
{
|
||||||
|
System.ComponentModel.DependencyPropertyDescriptor
|
||||||
|
.FromProperty(Visual3D.TransformProperty, typeof(Visual3D))
|
||||||
|
.RemoveValueChanged(_gizmoProxy, GizmoTransformChanged);
|
||||||
|
_overlayViewport?.Children.Remove(_gizmoProxy);
|
||||||
|
}
|
||||||
|
foreach (HelixToolkit.Wpf.Manipulator man in _gizmoParts)
|
||||||
|
{
|
||||||
|
man.UnBind();
|
||||||
|
_overlayViewport?.Children.Remove(man);
|
||||||
|
}
|
||||||
|
_gizmoParts.Clear();
|
||||||
|
_gizmoProxy = null;
|
||||||
|
|
||||||
|
if (_gizmoTarget is not null) // 清掉殘留的暫時 Transform(用 Identity,不可 null → 否則 FindHits crash)
|
||||||
|
foreach (ModelNodeViewModel l in _gizmoTarget.Leaves())
|
||||||
|
if (l.BodyVisual is not null)
|
||||||
|
l.BodyVisual.Transform = Transform3D.Identity;
|
||||||
|
_gizmoTarget = null;
|
||||||
|
if (_gizmoDragActive)
|
||||||
|
{
|
||||||
|
_gizmoDragActive = false;
|
||||||
|
_edgesSuspended = false;
|
||||||
|
SetEdgesActive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>操作器拖動中:把代理的 Transform 套到目標檔案的所有 BodyVisual(暫時、GPU 端)</summary>
|
||||||
|
private void GizmoTransformChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_gizmoBaking || _gizmoTarget is null || _gizmoProxy is null) return;
|
||||||
|
Transform3D t = _gizmoProxy.Transform;
|
||||||
|
if (!_gizmoDragActive && t is not null && !t.Value.IsIdentity)
|
||||||
|
{
|
||||||
|
_gizmoDragActive = true;
|
||||||
|
_edgesSuspended = true;
|
||||||
|
SetEdgesActive(false); // 邊線逐幀重建會卡,拖動中暫停
|
||||||
|
}
|
||||||
|
foreach (ModelNodeViewModel l in _gizmoTarget.Leaves())
|
||||||
|
if (l.BodyVisual is not null)
|
||||||
|
l.BodyVisual.Transform = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>滑鼠放開(MainWindow 轉發,handledEventsToo):把累積變換烘進 B-rep 並重置操作器</summary>
|
||||||
|
public void OnGizmoMouseUp()
|
||||||
|
{
|
||||||
|
if (_gizmoTarget is null || _gizmoProxy is null || _gizmoBakePending) return;
|
||||||
|
Matrix3D m = _gizmoProxy.Transform?.Value ?? Matrix3D.Identity;
|
||||||
|
if (m.IsIdentity) return; // 只是點一下、沒拖操作器 → 不烘焙
|
||||||
|
|
||||||
|
// 延後到 manipulator 自身的 mouse-up 處理(釋放捕捉等)完成後再烘焙,避免在其事件中改動視覺樹造成 reentrancy
|
||||||
|
_gizmoBakePending = true;
|
||||||
|
_viewport?.Dispatcher.BeginInvoke(new Action(() => BakeGizmo(m)),
|
||||||
|
System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BakeGizmo(Matrix3D m)
|
||||||
|
{
|
||||||
|
_gizmoBakePending = false;
|
||||||
|
if (_gizmoTarget is null) return;
|
||||||
|
ModelNodeViewModel root = _gizmoTarget;
|
||||||
|
|
||||||
|
_gizmoBaking = true; // 防 proxy 歸零觸發 GizmoTransformChanged 重入
|
||||||
|
foreach (ModelNodeViewModel l in root.Leaves())
|
||||||
|
if (l.BodyVisual is not null)
|
||||||
|
l.BodyVisual.Transform = Transform3D.Identity; // 清暫時位移(用 Identity,不可 null)
|
||||||
|
if (_gizmoProxy is not null) _gizmoProxy.Transform = Transform3D.Identity; // 綁定 → 操作器歸零
|
||||||
|
_gizmoBaking = false;
|
||||||
|
_gizmoDragActive = false;
|
||||||
|
_edgesSuspended = false;
|
||||||
|
SetEdgesActive(true);
|
||||||
|
|
||||||
|
TransformRoot(root, RigidAlign.ToModOp(m), m); // 烘進 B-rep(量測精度不受影響)
|
||||||
|
|
||||||
|
// 操作器移到變換後的新中心
|
||||||
|
Rect3D b = root.Bounds;
|
||||||
|
if (!b.IsEmpty)
|
||||||
|
{
|
||||||
|
var center = new Point3D(b.X + b.SizeX / 2, b.Y + b.SizeY / 2, b.Z + b.SizeZ / 2);
|
||||||
|
foreach (HelixToolkit.Wpf.Manipulator man in _gizmoParts)
|
||||||
|
man.Position = center;
|
||||||
|
}
|
||||||
|
StatusText = $"已套用操作器變換「{root.Name}」(可用 🧩干涉 驗證)";
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 干涉檢查 ────────────────────────────────────────────────
|
// ─── 干涉檢查 ────────────────────────────────────────────────
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
Reference in New Issue
Block a user