using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Media3D; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using HelixToolkit.Wpf; using STPViewer.Models; using STPViewer.Services; namespace STPViewer.ViewModels; // ─── 剖面(CPU 網格裁切;v0.4.0 起裁切在背景執行緒平行計算,UI 不凍結)── public partial class MainViewModel { private readonly DispatcherTimer _sectionTimer; private RectangleVisual3D? _sectionPlaneVisual; private Point3D _sectionPlanePoint; // SectionEnabled 時的剖切平面(合併邊線重建用) private Vector3D _sectionNormal; // 背景裁切 guard:進行中又有新變更(拉滑桿/換軸/零件平移)→ 完成後用最新參數重跑一輪。 // 不要改回同步呼叫 ClipMesh —— 大檔(64k 面)整場景裁切會凍結 UI 數秒 private bool _sectionApplying; private bool _sectionReapply; [ObservableProperty] private bool sectionEnabled; /// 0=X 1=Y 2=Z [ObservableProperty] private int sectionAxisIndex; /// 剖面位置(沿軸 0~100%) [ObservableProperty] private double sectionPosition = 50; [ObservableProperty] private bool sectionFlip; // ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)── private const int CustomAxisIndex = 3; private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點) private readonly List _sectionPicks = new(); private readonly List _sectionPickOverlays = new(); partial void OnSectionEnabledChanged(bool value) { if (!value) CancelSectionPickIfActive(quiet: true); ScheduleSection(); } partial void OnSectionAxisIndexChanged(int value) { if (value == CustomAxisIndex) { if (_customNormal is null) StatusText = "3點剖面:請在模型上點 3 個點定義剖切平面(Esc 取消)"; } else { // 離開自訂平面 → 丟棄舊平面(再選「3點」即重新定義) _customNormal = null; ClearSectionPicks(); } ScheduleSection(); } partial void OnSectionPositionChanged(double value) { if (value < 0) { SectionPosition = 0; return; } // 位置數值輸入框防呆 if (value > 100) { SectionPosition = 100; return; } ScheduleSection(); } partial void OnSectionFlipChanged(bool value) => ScheduleSection(); /// 3點剖面定義模式中的點擊(OnViewportClick 轉入)。回傳 true = 已消費此點擊。 public bool HandleSectionPlanePick(System.Windows.Point position) { if (!SectionEnabled || SectionAxisIndex != CustomAxisIndex || _customNormal is not null) return false; if (_viewport is null) return true; Point3D? hit = null; foreach (var h in _viewport.Viewport.FindHits(position)) { if (h.Model is null) continue; if (_faceMap.ContainsKey(h.Model) || _mergedFaceRanges.ContainsKey(h.Model)) { hit = h.Position; break; } } if (hit is null) { StatusText = $"3點剖面:未命中模型表面(已選 {_sectionPicks.Count}/3 點,Esc 取消)"; return true; } if (_sectionPicks.Count == 2 && Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], hit.Value - _sectionPicks[0]).LengthSquared < 1e-12) { StatusText = "3點剖面:三點共線,請改選不共線的第 3 點"; return true; } _sectionPicks.Add(hit.Value); double markerR = Math.Clamp(SceneDiagonal() * 0.004, 0.01, 5.0); var marker = new SphereVisual3D { Center = hit.Value, Radius = markerR, Fill = System.Windows.Media.Brushes.DodgerBlue }; _sectionPickOverlays.Add(marker); _viewport.Children.Add(marker); if (_sectionPicks.Count < 3) { StatusText = $"3點剖面:已選 {_sectionPicks.Count}/3 點"; return true; } Vector3D n = Vector3D.CrossProduct(_sectionPicks[1] - _sectionPicks[0], _sectionPicks[2] - _sectionPicks[0]); ClearSectionPicks(); if (n.LengthSquared < 1e-12) { StatusText = "3點剖面:三點退化(共線),請重新點選"; return true; } n.Normalize(); _customNormal = n; StatusText = $"3點剖面:平面已定義(法向 {n.X:F2}, {n.Y:F2}, {n.Z:F2})— 用位置滑桿掃剖面"; ApplySection(); return true; } /// 取消 3 點平面定義(Esc / 關剖面)。回傳 true = 有取消動作。 private bool CancelSectionPickIfActive(bool quiet = false) { bool picking = SectionEnabled && SectionAxisIndex == CustomAxisIndex && _customNormal is null; if (_sectionPicks.Count == 0 && !picking) return false; ClearSectionPicks(); if (SectionAxisIndex == CustomAxisIndex && _customNormal is null) SectionAxisIndex = 0; // 回 X 軸 if (!quiet) StatusText = "已取消 3 點剖面定義"; return true; } private void ClearSectionPicks() { foreach (Visual3D v in _sectionPickOverlays) _viewport?.Children.Remove(v); _sectionPickOverlays.Clear(); _sectionPicks.Clear(); } private void ScheduleSection() { _sectionTimer.Stop(); _sectionTimer.Start(); } private async void ApplySection() { if (_viewport is null) return; if (!SectionEnabled) { // 還原原始幾何(換回 fi.Mesh 參照,便宜、同步即可)。 // 若背景裁切還在跑,其 await 之後會看到 SectionEnabled=false 而放棄套用 foreach ((Model3D model, FaceInfo fi) in _faceMap) if (model is GeometryModel3D gm && !ReferenceEquals(gm.Geometry, fi.Mesh)) gm.Geometry = fi.Mesh; foreach (ModelNodeViewModel root in Roots) RefreshRootEdges(root); if (_sectionPlaneVisual is not null) { _viewport.Children.Remove(_sectionPlaneVisual); _sectionPlaneVisual = null; } ApplyRenderMode(); // 剖面關閉 → 瀏覽模式可切回合併網格 StatusText = "剖面已關閉"; return; } if (_sectionApplying) { _sectionReapply = true; return; } _sectionApplying = true; try { do { _sectionReapply = false; Rect3D bounds = UnionBounds(); if (bounds.IsEmpty) return; Vector3D axis; if (SectionAxisIndex == CustomAxisIndex) { if (_customNormal is null) return; // 平面尚未定義(等使用者點 3 點) axis = _customNormal.Value; } else { axis = SectionAxisIndex switch { 0 => new Vector3D(1, 0, 0), 1 => new Vector3D(0, 1, 0), _ => new Vector3D(0, 0, 1), }; } double t = SectionPosition / 100.0; Point3D min = bounds.Location; var size = new Vector3D(bounds.SizeX, bounds.SizeY, bounds.SizeZ); Point3D center = min + size / 2; // 平面位置:場景 AABB 8 個角投影到法向的 [dmin, dmax],滑桿 t 內插 //(軸向法向 = 與舊版逐軸計算等價;任意法向也成立) double dmin = double.MaxValue, dmax = double.MinValue; for (int ci = 0; ci < 8; ci++) { var corner = new Point3D( (ci & 1) == 0 ? min.X : min.X + size.X, (ci & 2) == 0 ? min.Y : min.Y + size.Y, (ci & 4) == 0 ? min.Z : min.Z + size.Z); double d = Vector3D.DotProduct((Vector3D)corner, axis); if (d < dmin) dmin = d; if (d > dmax) dmax = d; } double dStar = dmin + (dmax - dmin) * t; Point3D planePoint = center + axis * (dStar - Vector3D.DotProduct((Vector3D)center, axis)); Vector3D normal = SectionFlip ? -axis : axis; // UI 執行緒快照(原始 mesh 皆已 Freeze,可跨執行緒),背景平行裁切,回 UI 一次換上。 // 裁切期間若參數又變 / 零件被平移(TransformRoot 會呼叫 ApplySection)→ // _sectionReapply 讓迴圈用最新的 fi.Mesh 與平面重跑,最終狀態必為最新 var models = new List(_faceMap.Count); var sources = new List(_faceMap.Count); foreach ((Model3D model, FaceInfo fi) in _faceMap) if (model is GeometryModel3D gm) { models.Add(gm); sources.Add(fi.Mesh); } MeshGeometry3D[] clipped = await Task.Run(() => { var result = new MeshGeometry3D[sources.Count]; Parallel.For(0, sources.Count, i => result[i] = SectionService.ClipMesh(sources[i], planePoint, normal)); return result; }); if (!SectionEnabled) return; // 裁切期間被關掉 → 還原分支已處理,丟棄結果 for (int i = 0; i < models.Count; i++) models[i].Geometry = clipped[i]; // 已移除檔案的殘留 model 賦值無害(不在視覺樹上) _sectionPlanePoint = planePoint; _sectionNormal = normal; foreach (ModelNodeViewModel root in Roots) RefreshRootEdges(root); ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何) UpdateSectionPlaneVisual(planePoint, axis, size); string axisName = SectionAxisIndex == CustomAxisIndex ? "自訂平面" : $"{"XYZ"[SectionAxisIndex]} 軸"; StatusText = $"剖面:{axisName} {SectionPosition:F0}%" + (SectionFlip ? "(反向)" : ""); } while (_sectionReapply); } finally { _sectionApplying = false; } } private void UpdateSectionPlaneVisual(Point3D planePoint, Vector3D axis, Vector3D size) { if (_sectionPlaneVisual is not null) _viewport!.Children.Remove(_sectionPlaneVisual); (Vector3D lenDir, double len, double wid) = SectionAxisIndex switch { 0 => (new Vector3D(0, 1, 0), size.Y, size.Z), 1 => (new Vector3D(1, 0, 0), size.X, size.Z), 2 => (new Vector3D(1, 0, 0), size.X, size.Y), _ => (AnyPerpendicular(axis), size.Length * 0.8, size.Length * 0.8), // 自訂平面 }; lenDir.Normalize(); _sectionPlaneVisual = new RectangleVisual3D { Origin = planePoint, Normal = axis, LengthDirection = lenDir, Length = len * 1.05, Width = wid * 1.05, Fill = new SolidColorBrush(Color.FromArgb(40, 30, 120, 255)), }; _viewport!.Children.Add(_sectionPlaneVisual); } /// 取任一與 n 垂直的單位向量(自訂平面的指示矩形方向用) private static Vector3D AnyPerpendicular(Vector3D n) { Vector3D v = Math.Abs(n.Z) < 0.9 ? Vector3D.CrossProduct(n, new Vector3D(0, 0, 1)) : Vector3D.CrossProduct(n, new Vector3D(1, 0, 0)); v.Normalize(); return v; } }