diff --git a/.gitignore b/.gitignore
index c50ced5..5d07cc3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,9 +6,9 @@ Publish/ # publish-winapp output on case-sensitive systems
*.user
.vs/
-# Local development assets
+# AI 協作素材不入 git
For_AI/
-# CAD samples are stored locally in For_AI/
+# 客戶 CAD 料號檔不入 git(避免外流);測試模型放 For_AI/
*.stp
*.step
diff --git a/src/STPViewer/AboutDialog.xaml b/src/STPViewer/AboutDialog.xaml
index e257666..3568ae4 100644
--- a/src/STPViewer/AboutDialog.xaml
+++ b/src/STPViewer/AboutDialog.xaml
@@ -35,7 +35,7 @@
-
+
@@ -78,7 +78,7 @@
-
+
-/// The About window shows application information on the left and release history on the right.
-/// Add each new release at the start of Changelog.
+/// 關於視窗:左 = 程式資訊 / 開發者 / 技術棧,右 = 版本紀錄(寫給一般使用者看,非技術 changelog)。
+/// 樣式參考 ETTerms 的 AboutView。發新版時在 Changelog 陣列最上面加一筆。
///
public partial class AboutDialog : Window
{
@@ -26,7 +26,7 @@ public partial class AboutDialog : Window
private record ChangelogEntry(string Version, DateOnly Date, string Title, string[] Changes);
- // Release history shown to users, newest first.
+ // ── 版本紀錄(新版加最上面;口吻:一般使用者看得懂,講「你會感覺到什麼」而非實作細節)──
private static readonly ChangelogEntry[] Changelog =
{
new("0.7.1", new DateOnly(2026, 7, 12), "Ui.About.Release071.Title",
diff --git a/src/STPViewer/App.xaml.cs b/src/STPViewer/App.xaml.cs
index 2f087bb..8a4cfff 100644
--- a/src/STPViewer/App.xaml.cs
+++ b/src/STPViewer/App.xaml.cs
@@ -8,8 +8,8 @@ using STPViewer.Services;
namespace STPViewer;
///
-/// Log unhandled UI and background exceptions to
-/// %LOCALAPPDATA%\STPViewer\error.log and show a message for UI errors.
+/// 全域例外處理:UI 執行緒未攔截例外 → 記 log + 訊息框後盡量存活(不閃退);
+/// 背景 Task / 非 UI 執行緒例外 → 記 log。log 位於 %LOCALAPPDATA%\STPViewer\error.log。
///
public partial class App : Application
{
@@ -22,7 +22,8 @@ public partial class App : Application
LocalizationService.Instance.SetLanguage(SettingsService.Load().Language);
base.OnStartup(e);
- // Report UI exceptions and keep the window open.
+ // UI 執行緒(含 async void 回拋)的未攔截例外:記 log、提示、標記 Handled 讓程式存活。
+ // 若後續狀態已壞使用者可自行重啟;至少量測結果 / 已載模型不會直接蒸發。
DispatcherUnhandledException += (_, args) =>
{
LogException("DispatcherUnhandledException", args.Exception);
@@ -32,14 +33,14 @@ public partial class App : Application
args.Handled = true;
};
- // Log unobserved task exceptions.
+ // 沒被 await 的背景 Task 例外(finalizer 階段才浮出):記 log 並標記已觀察,避免行程終止
TaskScheduler.UnobservedTaskException += (_, args) =>
{
LogException("UnobservedTaskException", args.Exception);
args.SetObserved();
};
- // Log fatal exceptions on non-UI threads.
+ // 其他非 UI 執行緒的致命例外:無法阻止終止,但至少留下記錄
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
LogException("AppDomainUnhandledException", args.ExceptionObject as Exception);
}
diff --git a/src/STPViewer/Converters/EnumToBoolConverter.cs b/src/STPViewer/Converters/EnumToBoolConverter.cs
index b830c59..00e0f06 100644
--- a/src/STPViewer/Converters/EnumToBoolConverter.cs
+++ b/src/STPViewer/Converters/EnumToBoolConverter.cs
@@ -6,8 +6,8 @@ using STPViewer.Models;
namespace STPViewer.Converters;
///
-/// Convert MeasureMode to ToggleButton.IsChecked using the mode name
-/// in ConverterParameter. Unchecking returns to None.
+/// MeasureMode ↔ ToggleButton.IsChecked。
+/// ConverterParameter 為模式名稱字串;取消勾選時回到 None。
///
public class EnumToBoolConverter : IValueConverter
{
diff --git a/src/STPViewer/Converters/FileNameConverter.cs b/src/STPViewer/Converters/FileNameConverter.cs
index 3b0461a..924ea12 100644
--- a/src/STPViewer/Converters/FileNameConverter.cs
+++ b/src/STPViewer/Converters/FileNameConverter.cs
@@ -4,7 +4,7 @@ using System.Windows.Data;
namespace STPViewer.Converters;
-/// Show file names in the recent-files menu while retaining full paths as tooltips.
+/// 完整路徑 → 檔名(選單「最近檔案」顯示用;tooltip 仍給完整路徑)
public class FileNameConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
diff --git a/src/STPViewer/MainWindow.xaml b/src/STPViewer/MainWindow.xaml
index 4b981b8..39d2058 100644
--- a/src/STPViewer/MainWindow.xaml
+++ b/src/STPViewer/MainWindow.xaml
@@ -28,7 +28,7 @@
-
+
-
+
-
+
@@ -280,7 +280,7 @@
-
+
@@ -300,7 +300,7 @@
-
+
-
+
-
+
@@ -426,7 +427,7 @@
VerticalAlignment="Stretch" Background="#FFD8D8D8"
ToolTip="{loc:Loc Ui.Main.DragResizePanel}" />
-
+
diff --git a/src/STPViewer/MainWindow.xaml.cs b/src/STPViewer/MainWindow.xaml.cs
index d9f51e7..3d1976d 100644
--- a/src/STPViewer/MainWindow.xaml.cs
+++ b/src/STPViewer/MainWindow.xaml.cs
@@ -18,7 +18,7 @@ public partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
- // Read the version from the project at runtime.
+ // 版號單一來源 = csproj ;標題執行時讀,避免寫死
UpdateTitle();
DataContext = _vm;
_vm.Attach(viewport);
@@ -26,7 +26,7 @@ public partial class MainWindow : Window
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
- // Restore window settings here and units/recent files in the view model.
+ // 使用者設定:視窗位置大小(View 職責)+ 單位/MRU(VM 職責)
_settings = SettingsService.Load();
ApplyWindowSettings();
_vm.LoadSettings(_settings);
@@ -38,7 +38,7 @@ public partial class MainWindow : Window
_vm.DisposeLocalization();
};
- // The gizmo overlay needs handled mouse-up events from the manipulator.
+ // Gizmo 操作器放在疊圖層;manipulator 會把 MouseUp 標 handled,需 handledEventsToo 才收得到
gizmoOverlay.AddHandler(MouseLeftButtonUpEvent,
new MouseButtonEventHandler((_, _) => _vm.OnGizmoMouseUp()), handledEventsToo: true);
}
@@ -58,7 +58,7 @@ public partial class MainWindow : Window
Width = _settings.WindowWidth;
Height = _settings.WindowHeight;
}
- // Restore the position only when it is within the current virtual desktop.
+ // 位置要落在目前虛擬桌面內才還原(避免拔掉外接螢幕後視窗開在螢幕外)
if (!double.IsNaN(_settings.WindowLeft) && !double.IsNaN(_settings.WindowTop) &&
_settings.WindowLeft >= SystemParameters.VirtualScreenLeft - 50 &&
_settings.WindowTop >= SystemParameters.VirtualScreenTop - 50 &&
@@ -87,7 +87,7 @@ public partial class MainWindow : Window
SettingsService.Save(_settings);
}
- /// Open files passed on the command line: STPViewer.exe a.stp b.stl …
+ /// 支援命令列帶檔開啟:STPViewer.exe a.stp b.stl …
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var files = System.Environment.GetCommandLineArgs().Skip(1)
@@ -96,7 +96,7 @@ public partial class MainWindow : Window
await _vm.ImportFilesAsync(files);
}
- /// Handle Esc and measurement shortcuts without intercepting text inputs.
+ /// Esc / 量測模式快速鍵(焦點在輸入框時不攔截,樹搜尋、剖面數值可正常打字)
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.FocusedElement is TextBoxBase or ComboBox) return;
@@ -111,7 +111,7 @@ public partial class MainWindow : Window
e.Handled = true;
}
- /// Build the recent-files menu from the view model's current list.
+ /// 最近檔案下拉(▾):即時從 VM 的 MRU 清單組選單
private void Recent_Click(object sender, RoutedEventArgs e)
{
var menu = new ContextMenu
@@ -145,14 +145,14 @@ public partial class MainWindow : Window
private void Viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
- // Shift+left-click pans the view.
+ // Shift+左鍵 = 平移(PanGesture2),不觸發量測/拖曳
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) return;
if (_vm.CurrentMode == STPViewer.Models.MeasureMode.Drag)
{
if (_vm.OnDragStart(e.GetPosition(viewport.Viewport)))
{
- viewport.CaptureMouse(); // Keep receiving move/up events outside the window.
+ viewport.CaptureMouse(); // 拖出視窗外也持續收到 Move/Up
e.Handled = true;
}
return;
@@ -173,7 +173,7 @@ public partial class MainWindow : Window
}
private void Viewport_LostMouseCapture(object sender, MouseEventArgs e) =>
- _vm.OnDragEnd(); // Commit the current position if capture is interrupted.
+ _vm.OnDragEnd(); // 捕捉意外中斷(Alt+Tab 等)→ 以目前位置定格,不留懸空 Transform
private void Window_DragOver(object sender, DragEventArgs e)
{
diff --git a/src/STPViewer/Models/FaceInfo.cs b/src/STPViewer/Models/FaceInfo.cs
index 24b2b8c..7bcfdfb 100644
--- a/src/STPViewer/Models/FaceInfo.cs
+++ b/src/STPViewer/Models/FaceInfo.cs
@@ -4,13 +4,13 @@ using STPViewer.ViewModels;
namespace STPViewer.Models;
-/// Maps a rendered GeometryModel3D back to a B-rep face.
+/// 渲染物件(GeometryModel3D)反查 B-rep Face 的對照資料
public class FaceInfo
{
- /// B-rep face, or null for mesh-only sources such as STL.
+ /// B-rep 面;STL 等純網格來源為 null(僅支援點/距離/面距量測)
public Face? BrepFace { get; init; }
- /// Uncut mesh used for area and section restoration; replaced after translation.
+ /// 原始(未剖切)三角網格 — 面積計算與剖面還原用;零件平移後會換成新 mesh
public required MeshGeometry3D Mesh { get; set; }
public required ModelNodeViewModel Owner { get; init; }
diff --git a/src/STPViewer/Models/MeasureMode.cs b/src/STPViewer/Models/MeasureMode.cs
index 5ab2435..6c49cb2 100644
--- a/src/STPViewer/Models/MeasureMode.cs
+++ b/src/STPViewer/Models/MeasureMode.cs
@@ -1,6 +1,6 @@
namespace STPViewer.Models;
-/// Measurement modes; None is the browsing mode.
+/// 量測模式(None = 純瀏覽)
public enum MeasureMode
{
None,
@@ -9,11 +9,11 @@ public enum MeasureMode
Edge,
Face,
Circle,
- Angle, // Angle between two faces or edges.
- FaceDistance, // Shortest face-to-face distance (mesh approximation).
- Align, // Translate a part so the first point meets the second.
- Align3, // Rotate and translate from three source and target points.
- Drag, // Move a part in the screen plane; commit on release.
- Interference, // Result type rather than a picking mode.
- Volume, // Volume/centroid result triggered from the tree menu.
+ Angle, // 兩面 / 兩邊夾角
+ FaceDistance, // 面到面最短距離(網格近似)
+ Align, // 兩點對齊:平移零件使點 1 貼到點 2
+ Align3, // 三點對齊:旋轉+平移(來源 3 點 → 目標 3 點)
+ Drag, // 拖曳模式:左鍵按住零件沿螢幕平面拖動(放開才烘進 B-rep)
+ Interference, // 干涉檢查結果(非點擊模式,僅作為結果類型)
+ Volume, // 體積/質心(樹右鍵選單觸發,非點擊模式,僅作為結果類型)
}
diff --git a/src/STPViewer/Models/MeasurementResult.cs b/src/STPViewer/Models/MeasurementResult.cs
index b1349fb..bcecd1e 100644
--- a/src/STPViewer/Models/MeasurementResult.cs
+++ b/src/STPViewer/Models/MeasurementResult.cs
@@ -7,23 +7,23 @@ using HelixToolkit.Wpf;
namespace STPViewer.Models;
///
-/// A measurement stored in millimeters. Its title, details, and 3D labels
-/// update when the display unit changes.
+/// 一筆量測結果。內部以 mm 儲存原始值,Title/Detail 依目前單位即時產生;
+/// 切換單位時 3D 標籤(DynamicLabels)也同步改字。
///
public class MeasurementResult : ObservableObject
{
public MeasureMode Kind { get; init; }
- /// Create the list title for a display unit.
+ /// 依單位產生標題(清單粗體列)
public required Func TitleFor { get; init; }
- /// Create the detailed text for a display unit.
+ /// 依單位產生多行明細
public required Func DetailFor { get; init; }
- /// View markers removed with this measurement.
+ /// 視圖中的標記(刪除量測時一併移除)
public List Overlays { get; } = new();
- /// 3D labels whose text changes with the display unit.
+ /// 單位相關的 3D 文字標籤(切換單位時更新 Text)
public List<(BillboardTextVisual3D Label, Func TextFor)> DynamicLabels { get; } = new();
private UnitSystem _unit = UnitSystem.Millimeter;
diff --git a/src/STPViewer/Models/UnitSystem.cs b/src/STPViewer/Models/UnitSystem.cs
index 9e9e8d6..0c08b56 100644
--- a/src/STPViewer/Models/UnitSystem.cs
+++ b/src/STPViewer/Models/UnitSystem.cs
@@ -8,28 +8,28 @@ public enum UnitSystem
Inch,
}
-/// Format measurements stored in millimeters for the selected display unit.
+/// 量測值格式化(內部一律 mm,顯示時換算)
public static class Units
{
private const double MmPerInch = 25.4;
- /// Length.
+ /// 長度
public static string L(double mm, UnitSystem u) =>
u == UnitSystem.Millimeter ? $"{mm:F3} mm" : $"{mm / MmPerInch:F4} in";
- /// Area.
+ /// 面積
public static string A(double mm2, UnitSystem u) =>
u == UnitSystem.Millimeter ? $"{mm2:N3} mm²" : $"{mm2 / (MmPerInch * MmPerInch):N4} in²";
- /// Volume.
+ /// 體積
public static string V(double mm3, UnitSystem u) =>
u == UnitSystem.Millimeter ? $"{mm3:N2} mm³" : $"{mm3 / (MmPerInch * MmPerInch * MmPerInch):N4} in³";
- /// Coordinate component without a unit suffix.
+ /// 座標分量(無單位字尾)
public static string C(double mm, UnitSystem u) =>
u == UnitSystem.Millimeter ? $"{mm:F3}" : $"{mm / MmPerInch:F4}";
- /// Coordinate point (x, y, z).
+ /// 座標點 (x, y, z)
public static string P(Point3D p, UnitSystem u) =>
$"({C(p.X, u)}, {C(p.Y, u)}, {C(p.Z, u)})";
}
diff --git a/src/STPViewer/Services/InterferenceService.cs b/src/STPViewer/Services/InterferenceService.cs
index c599b37..2979d9d 100644
--- a/src/STPViewer/Services/InterferenceService.cs
+++ b/src/STPViewer/Services/InterferenceService.cs
@@ -6,7 +6,8 @@ using System.Windows.Media.Media3D;
namespace STPViewer.Services;
///
-/// Interference result: Segments holds intersection lines, or GapA/GapB the approximate closest points.
+/// 干涉檢查結果:Intersects=true 時 Segments 為干涉交線;
+/// false 時 GapA/GapB 為最近點對(網格近似)。
///
public record InterferenceResult(
bool Intersects,
@@ -17,8 +18,10 @@ public record InterferenceResult(
double GapDistance);
///
-/// Checks intersections between two triangle mesh sets using a uniform spatial grid.
-/// Coplanar contact without penetration is excluded.
+/// 兩組三角網格的干涉檢查:
+/// 三角形-三角形相交(區間法,回傳交線段)+ 均勻網格空間加速。
+/// 共面接觸(無穿透)不會被判為干涉(近似限制)。
+/// 設計為在背景執行緒呼叫。
///
public static class InterferenceService
{
@@ -60,7 +63,7 @@ public static class InterferenceService
return new InterferenceResult(false, 0, segments, ga, gb, d);
}
- // Spatial data
+ // ─── 資料結構 ────────────────────────────────────────────────
private readonly struct Triangle
{
@@ -97,7 +100,7 @@ public static class InterferenceService
a.Min.Y <= b.Max.Y && a.Max.Y >= b.Min.Y &&
a.Min.Z <= b.Max.Z && a.Max.Z >= b.Min.Z;
- /// Uniform grid from cells to triangle indices.
+ /// 均勻網格:cell → 三角形索引
private sealed class TriGrid
{
private readonly Dictionary<(int, int, int), List> _cells = new();
@@ -105,7 +108,7 @@ public static class InterferenceService
public TriGrid(Triangle[] tris)
{
- // Size cells from the scene diagonal, with bounds for extreme sizes.
+ // cell 大小:整體對角線 / 64(夾限避免極端值)
Point3D min = tris[0].Min, max = tris[0].Max;
foreach (Triangle t in tris)
{
@@ -143,7 +146,7 @@ public static class InterferenceService
}
}
- // Triangle intersection
+ // ─── 三角形相交(區間法,回傳交線段)────────────────────────
private static bool TriTriIntersect(in Triangle ta, in Triangle tb, out Point3D s0, out Point3D s1)
{
@@ -164,12 +167,12 @@ public static class InterferenceService
if ((db0 > 0 && db1 > 0 && db2 > 0) || (db0 < 0 && db1 < 0 && db2 < 0)) return false;
Vector3D dir = Vector3D.CrossProduct(n1, n2);
- if (dir.LengthSquared < 1e-24) return false; // Coplanar contact.
+ if (dir.LengthSquared < 1e-24) return false; // 共面:不視為穿透(近似)
if (!PlaneCrossSegment(ta, da0, da1, da2, out Point3D p0, out Point3D p1)) return false;
if (!PlaneCrossSegment(tb, db0, db1, db2, out Point3D q0, out Point3D q1)) return false;
- // Project onto the intersection direction and overlap the intervals.
+ // 投影到交線方向取重疊區間
double tp0 = Vector3D.DotProduct(dir, (Vector3D)p0);
double tp1 = Vector3D.DotProduct(dir, (Vector3D)p1);
double tq0 = Vector3D.DotProduct(dir, (Vector3D)q0);
@@ -179,14 +182,14 @@ public static class InterferenceService
double lo = Math.Max(tp0, tq0);
double hi = Math.Min(tp1, tq1);
- if (lo >= hi) return false;
+ if (lo >= hi) return false; // 無重疊(或僅點接觸)
s0 = LerpOnSegment(p0, p1, tp0, tp1, lo);
s1 = LerpOnSegment(p0, p1, tp0, tp1, hi);
return true;
}
- /// Finds the triangle-plane intersection segment from signed vertex distances.
+ /// 三角形與平面(頂點符號 d0..d2 已算好)的交線段(兩個交點)
private static bool PlaneCrossSegment(in Triangle t, double d0, double d1, double d2,
out Point3D p0, out Point3D p1)
{
@@ -205,8 +208,8 @@ public static class InterferenceService
Span pts, ref int n)
{
if (n >= 3) return;
- if (da == 0 && db == 0) return;
- if (da == 0) { pts[n++] = a; return; }
+ if (da == 0 && db == 0) return; // 邊在平面上 → 共面情形,略過
+ if (da == 0) { pts[n++] = a; return; } // 頂點剛好在平面上
if (da * db < 0)
pts[n++] = a + (b - a) * (da / (da - db));
}
@@ -218,11 +221,11 @@ public static class InterferenceService
return p0 + (p1 - p0) * f;
}
- // Approximate gap when meshes do not intersect
+ // ─── 無干涉時的近似最小間隙 ─────────────────────────────────
private static (Point3D a, Point3D b, double d) ApproxMinDistance(Triangle[] trisA, Triangle[] trisB)
{
- // Sample vertices for an initial closest pair.
+ // 1) 抽樣頂點對頂點求初值
double best = double.MaxValue;
Point3D pa = default, pb = default;
int strideA = Math.Max(1, trisA.Length / 2000);
@@ -234,13 +237,14 @@ public static class InterferenceService
if (d < best) { best = d; pa = trisA[i].A; pb = trisB[j].A; }
}
- // Refine against opposing triangles, rejecting distant AABBs.
+ // 2) 以最佳點為中心,精修:點 → 對方所有三角形(AABB 平方距離快速拒絕 + 平行分段,
+ // 初值來自步驟 1 已相當接近 → 絕大多數三角形被 AABB 直接刷掉)
(pb, best) = ClosestOnTriangles(pa, trisB, pb, best);
(pa, best) = ClosestOnTriangles(pb, trisA, pa, best);
return (pa, pb, best);
}
- /// Refines the closest point from an initial distance.
+ /// 點 p 到三角形集的最近點(帶初值;回傳更新後的最近點與距離)
private static (Point3D pt, double d) ClosestOnTriangles(Point3D p, Triangle[] tris, Point3D seedPt, double seedD)
{
object gate = new();
@@ -252,7 +256,7 @@ public static class InterferenceService
(i, _, local) =>
{
Triangle t = tris[i];
- // Reject triangles whose AABB cannot improve the current distance.
+ // AABB 平方距離下界 ≥ 目前最佳 → 不可能更近,略過(絕大多數三角形在此被刷掉)
double dx = Math.Max(0, Math.Max(t.Min.X - p.X, p.X - t.Max.X));
double dy = Math.Max(0, Math.Max(t.Min.Y - p.Y, p.Y - t.Max.Y));
double dz = Math.Max(0, Math.Max(t.Min.Z - p.Z, p.Z - t.Max.Z));
@@ -272,7 +276,7 @@ public static class InterferenceService
return (bestPt, bestD);
}
- /// Closest point on a triangle, following Ericson's construction.
+ /// 點到三角形最近點(Ericson, Real-Time Collision Detection)
internal static Point3D ClosestPointOnTriangle(Point3D p, Point3D a, Point3D b, Point3D c)
{
Vector3D ab = b - a, ac = c - a, ap = p - a;
diff --git a/src/STPViewer/Services/MeasurementService.cs b/src/STPViewer/Services/MeasurementService.cs
index 17402a6..ab4826f 100644
--- a/src/STPViewer/Services/MeasurementService.cs
+++ b/src/STPViewer/Services/MeasurementService.cs
@@ -10,13 +10,13 @@ using static STPViewer.Services.LocalizationService;
namespace STPViewer.Services;
-/// One direction selected for angle measurement.
+/// 角度量測的單次拾取:邊方向或面法向量
public record DirectionPick(Point3D At, Vector3D Direction, string Desc);
///
-/// Computes measurements and their viewport overlays.
-/// B-rep provides exact edge lengths and radii; mesh values are approximate.
-/// Text is deferred so unit and language changes update existing results.
+/// 點/距離/邊/面/圓/角度/面距 量測幾何計算 + 視圖 overlay 產生。
+/// 邊長、圓半徑以 B-rep 精確值為準;面積與面距用三角網格近似。
+/// 所有文字以 Func<UnitSystem,string> 延後產生,支援 mm/inch 即時切換。
///
public class MeasurementService
{
@@ -30,11 +30,13 @@ public class MeasurementService
private static GeoPoint ToGeo(Point3D p) => new(p.X, p.Y, p.Z);
private static Point3D ToP3(GeoPoint p) => new(p.x, p.y, p.z);
- // Picking
+ // ─── 拾取輔助 ────────────────────────────────────────────────
///
- /// Snaps to the nearest vertex or circular edge within tolerance.
- /// A circular edge snaps to its center for hole pitch measurements.
+ /// 命中點吸附:點到圓形邊附近(容差內)→ 吸附**圓心**(量兩孔 pitch 的關鍵,v0.5.0);
+ /// 否則吸附最近的 B-rep 頂點(容差內);都沒有 → 回傳原始表面點。
+ /// 圓心與頂點都在容差內時,比「命中點到圓形邊」與「命中點到頂點」誰近(圓弧端點常與圓重疊,
+ /// 點在圓邊上時圓心優先勝出才符合直覺)。
///
public Point3D Snap(FaceInfo fi, Point3D hit, double tolerance)
{
@@ -52,7 +54,7 @@ public class MeasurementService
}
catch { }
- // Compare distance to the circle edge before selecting its center.
+ // 圓形邊:以「命中點到邊曲線」的距離參與比較,勝出時吸附到圓心
try
{
foreach (Edge e in fi.BrepFace.AllEdges)
@@ -71,13 +73,14 @@ public class MeasurementService
}
///
- /// Uses a nearby straight edge for direction, otherwise a face normal.
+ /// 角度量測拾取:命中點附近有直線邊(容差內)→ 邊方向;
+ /// 否則取面法向量(B-rep 精確;無 B-rep 時用網格法向量)。
///
public DirectionPick PickDirection(FaceInfo fi, Point3D hit, Vector3D meshNormal, double tolerance)
{
if (fi.BrepFace is not null)
{
- // Prefer a nearby straight edge.
+ // 1) 近距離直線邊優先
try
{
foreach (Edge e in fi.BrepFace.AllEdges)
@@ -95,7 +98,7 @@ public class MeasurementService
}
catch { }
- // Use the B-rep face normal when available.
+ // 2) 面法向量(精確)
try
{
GeoPoint2D uv = fi.BrepFace.Surface.PositionOf(ToGeo(hit));
@@ -110,7 +113,7 @@ public class MeasurementService
return new DirectionPick(hit, mn, "Runtime.DirectionMeshFace");
}
- // Point and distance
+ // ─── 點 / 距離 ───────────────────────────────────────────────
public MeasurementResult MeasurePoint(Point3D p, string label, double markerR)
{
@@ -146,9 +149,9 @@ public class MeasurementService
return r;
}
- // Edge and circle
+ // ─── 邊 / 圓 ─────────────────────────────────────────────────
- /// Measures the nearest edge, or the nearest circular edge when circlesOnly is true.
+ /// 量測命中面上最近的邊。circlesOnly=true 時只找圓形邊。找不到回傳 null。
public MeasurementResult? MeasureEdge(FaceInfo fi, Point3D hit, string label, double markerR, bool circlesOnly)
{
if (fi.BrepFace is null) return null;
@@ -242,11 +245,11 @@ public class MeasurementService
return r;
}
- // Face
+ // ─── 面 ──────────────────────────────────────────────────────
public MeasurementResult MeasureFace(FaceInfo fi, Point3D hit, string label, double markerR)
{
- // Area is estimated from the triangle mesh.
+ // 面積:三角網格加總(近似值,三角化精度內)
double area = MeshArea(fi.Mesh);
int triCount = fi.Mesh.TriangleIndices.Count / 3;
Func surfaceDesc = fi.BrepFace is not null
@@ -260,7 +263,7 @@ public class MeasurementService
DetailFor = u => Format("Runtime.FaceDetail", Units.A(area, u), surfaceDesc(u), triCount),
};
- // Highlight the outer edges and add a label.
+ // overlay:外輪廓 highlight + 標籤
if (fi.BrepFace is not null)
{
try
@@ -322,7 +325,7 @@ public class MeasurementService
var other => other,
};
- // Angle
+ // ─── 角度(兩面 / 兩邊夾角)─────────────────────────────────
public MeasurementResult MeasureAngle(DirectionPick a, DirectionPick b, string label, double markerR, double arrowLen)
{
@@ -357,7 +360,7 @@ public class MeasurementService
Thickness = 2,
};
- // Face distance
+ // ─── 面到面最短距離(網格近似)──────────────────────────────
public MeasurementResult MeasureFaceDistance(FaceInfo a, FaceInfo b, string label, double markerR)
{
@@ -381,7 +384,7 @@ public class MeasurementService
return r;
}
- /// Approximates the closest points by probing vertices against triangles in both directions.
+ /// 兩網格最近點對:頂點→三角形雙向(頂點過多時抽樣,結果為近似值)
private static (Point3D a, Point3D b) MeshMinDistance(MeshGeometry3D ma, MeshGeometry3D mb)
{
double best = double.MaxValue;
@@ -414,18 +417,19 @@ public class MeasurementService
return (pa, pb);
}
- // Volume and centroid
+ // ─── 體積 / 質心(封閉網格 signed volume)────────────────────
///
- /// Computes signed volume and centroid from origin-based tetrahedra.
- /// The result is reliable only for closed meshes.
+ /// 網格體積與質心:各三角形與原點構成的四面體有向體積加總(divergence theorem)。
+ /// 只對「封閉」網格有意義(開放殼會得到不可靠的值 → IsClosedReliable=false 供呼叫端提示)。
+ /// 多個 mesh(多實體)直接加總。
///
public static (double VolumeMm3, Point3D Centroid, bool Reliable) MeshVolume(
System.Collections.Generic.IEnumerable meshes)
{
- double vol6 = 0;
- double absVol6 = 0;
- double cx = 0, cy = 0, cz = 0;
+ double vol6 = 0; // Σ 6×有向體積
+ double absVol6 = 0; // Σ |6×有向體積|(封閉性 sanity check 用)
+ double cx = 0, cy = 0, cz = 0; // Σ 四面體質心 × 6×有向體積
foreach (MeshGeometry3D mesh in meshes)
{
Point3DCollection pos = mesh.Positions;
@@ -439,14 +443,14 @@ public class MeasurementService
+ a.Z * (b.X * c.Y - b.Y * c.X);
vol6 += v;
absVol6 += Math.Abs(v);
- // Tetrahedron centroid = (a + b + c) / 4.
+ // 四面體(a,b,c,原點)質心 = (a+b+c)/4
cx += (a.X + b.X + c.X) / 4 * v;
cy += (a.Y + b.Y + c.Y) / 4 * v;
cz += (a.Z + b.Z + c.Z) / 4 * v;
}
}
double volume = Math.Abs(vol6) / 6;
- // Large signed cancellation suggests an open mesh or inconsistent normals.
+ // 有向體積相對正負抵銷比例過高 → 可能是開放殼/法向不一致,數值不可靠
bool reliable = absVol6 > 0 && Math.Abs(vol6) / absVol6 > 0.01 && volume > 1e-9;
Point3D centroid = Math.Abs(vol6) > 1e-12
? new Point3D(cx / vol6, cy / vol6, cz / vol6)
@@ -454,7 +458,7 @@ public class MeasurementService
return (volume, centroid, reliable);
}
- // Shares the triangle closest-point calculation with InterferenceService.
+ // 點到三角形最近點:共用 InterferenceService.ClosestPointOnTriangle(v0.4.0 去重複)
// ─── overlay helpers ─────────────────────────────────────────
diff --git a/src/STPViewer/Services/RigidAlign.cs b/src/STPViewer/Services/RigidAlign.cs
index b889971..0c113b7 100644
--- a/src/STPViewer/Services/RigidAlign.cs
+++ b/src/STPViewer/Services/RigidAlign.cs
@@ -4,14 +4,15 @@ using System.Windows.Media.Media3D;
namespace STPViewer.Services;
///
-/// Rigid transformations for three-point alignment and rotation.
-/// converts WPF to CADability's matrix convention.
+/// 三點對齊 / 旋轉的剛體變換數學。
+/// 同一個變換需要兩種表示:WPF (列向量約定,給網格/邊線)與
+/// CADability (行向量約定,給 B-rep Modify)— 用 轉換。
///
public static class RigidAlign
{
///
- /// Computes a rigid transform from three source points to three target points.
- /// Returns false for collinear or degenerate points.
+ /// 來源 3 點 → 目標 3 點 的剛體變換(不縮放):
+ /// p1→q1 精確貼合、p1p2 方向對齊 q1q2、三點平面對齊。共線/退化回傳 false。
///
public static bool TryRigidTransform(
Point3D p1, Point3D p2, Point3D p3,
@@ -21,7 +22,7 @@ public static class RigidAlign
if (!TryFrame(p1, p2, p3, out Vector3D xp, out Vector3D yp, out Vector3D zp)) return false;
if (!TryFrame(q1, q2, q3, out Vector3D xq, out Vector3D yq, out Vector3D zq)) return false;
- // R = Bq · Bpᵀ, transposed for Matrix3D's convention.
+ // R = Bq · Bpᵀ(行向量約定);WPF Matrix3D 為列向量約定 → 轉置擺放
double R(int r, int c)
{
double xpc = c == 0 ? xp.X : c == 1 ? xp.Y : xp.Z;
@@ -46,7 +47,7 @@ public static class RigidAlign
return true;
}
- /// Converts a WPF matrix to a CADability ModOp.
+ /// WPF Matrix3D(列向量約定)→ CADability ModOp(行向量約定,3×4)
public static CADability.ModOp ToModOp(Matrix3D m) => new(new double[3, 4]
{
{ m.M11, m.M21, m.M31, m.OffsetX },
@@ -57,7 +58,7 @@ public static class RigidAlign
public static bool Collinear(Point3D a, Point3D b, Point3D c) =>
Vector3D.CrossProduct(b - a, c - a).LengthSquared < 1e-12;
- /// Builds an orthogonal frame from three points; returns false for collinear points.
+ /// 三點 → 正交座標架(x=1→2 方向、z=平面法向、y=z×x);共線回傳 false
private static bool TryFrame(Point3D a, Point3D b, Point3D c,
out Vector3D x, out Vector3D y, out Vector3D z)
{
diff --git a/src/STPViewer/Services/SectionService.cs b/src/STPViewer/Services/SectionService.cs
index b11f68f..abea6e5 100644
--- a/src/STPViewer/Services/SectionService.cs
+++ b/src/STPViewer/Services/SectionService.cs
@@ -4,8 +4,8 @@ using System.Windows.Media.Media3D;
namespace STPViewer.Services;
///
-/// Clips meshes and line segments to the half-space dot(p - p0, n) ≤ 0.
-/// The original geometry remains available for restoration.
+/// 剖面:把三角網格 / 線段集用半空間裁切(保留 dot(p - p0, n) ≤ 0 的一側)。
+/// WPF 3D 沒有 GPU clip plane,採 CPU 裁切後置換 Geometry(原始 mesh 保留可還原)。
///
public static class SectionService
{
@@ -23,7 +23,7 @@ public static class SectionService
Point3DCollection pos = src.Positions;
Int32Collection idx = src.TriangleIndices;
- // Cache signed distances for each vertex.
+ // 頂點距離快取
var dist = new double[pos.Count];
for (int i = 0; i < pos.Count; i++)
dist[i] = Vector3D.DotProduct(pos[i] - planePoint, normal);
@@ -47,7 +47,7 @@ public static class SectionService
continue;
}
- // Clip the polygon and triangulate it as a fan.
+ // 部分在保留側:Sutherland-Hodgman 裁出 3 或 4 邊形,扇形分割
int n = 0;
for (int k = 0; k < 3; k++)
{
@@ -78,7 +78,7 @@ public static class SectionService
idx.Add(i); idx.Add(i + 1); idx.Add(i + 2);
}
- /// Clips paired line segment endpoints.
+ /// 裁切線段集(成對端點)
public static Point3DCollection ClipSegments(Point3DCollection segments, Point3D planePoint, Vector3D normal)
{
var result = new Point3DCollection();
diff --git a/src/STPViewer/Services/SettingsService.cs b/src/STPViewer/Services/SettingsService.cs
index 9a5cb77..6f5af38 100644
--- a/src/STPViewer/Services/SettingsService.cs
+++ b/src/STPViewer/Services/SettingsService.cs
@@ -5,7 +5,7 @@ using System.Text.Json;
namespace STPViewer.Services;
-/// User settings stored in %LOCALAPPDATA%\STPViewer\settings.json.
+/// 使用者設定(%LOCALAPPDATA%\STPViewer\settings.json)
public class AppSettings
{
public double WindowWidth { get; set; } = double.NaN;
@@ -17,11 +17,11 @@ public class AppSettings
public string? Language { get; set; }
public List RecentFiles { get; set; } = new();
- /// Visible quick bar keys; null uses registry defaults.
+ /// 快速工具列顯示的按鈕 key 清單;null = 用預設(QuickBarViewModel.Registry)
public List? QuickBarKeys { get; set; }
}
-/// Loads defaults for missing or invalid settings and ignores save failures.
+/// 設定讀寫:壞檔/缺檔一律回預設值,儲存失敗靜默(設定不值得讓程式閃退)
public static class SettingsService
{
public const int MaxRecentFiles = 10;
diff --git a/src/STPViewer/Services/StepImportService.cs b/src/STPViewer/Services/StepImportService.cs
index 7b3118f..978ca32 100644
--- a/src/STPViewer/Services/StepImportService.cs
+++ b/src/STPViewer/Services/StepImportService.cs
@@ -6,15 +6,15 @@ using System.Windows.Media;
using System.Windows.Media.Media3D;
using CADability;
using CADability.GeoObject;
-using IOPath = System.IO.Path;
+using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
using static STPViewer.Services.LocalizationService;
namespace STPViewer.Services;
-/// A frozen triangle mesh for one B-rep face; BrepFace is null for STL.
+/// 單一 B-rep Face 的三角網格(mesh 已 Freeze,可跨執行緒);STL 來源 BrepFace 為 null
public record ImportedFace(Face? BrepFace, MeshGeometry3D Mesh);
-/// An imported assembly group or geometry leaf.
+/// 匯入後的階層節點:group(裝配)或 leaf(零件幾何)
public class ImportedNode
{
public string Name { get; set; } = "";
@@ -22,15 +22,15 @@ public class ImportedNode
public int NameNumber { get; set; }
public List Children { get; } = new();
- // Leaf geometry
+ // ── leaf 幾何 ──
public List Faces { get; } = new();
- public Point3DCollection? EdgeSegments { get; set; } // Frozen paired endpoints.
- public bool HasBrep { get; set; } = true; // False for STL and curves.
+ public Point3DCollection? EdgeSegments { get; set; } // 輪廓線(成對端點,已 Freeze)
+ public bool HasBrep { get; set; } = true; // STL / 純曲線 = false
- /// Source B-rep objects used for whole-part transforms.
+ /// 來源 B-rep 物件(Solid/Shell/Face/曲線)— 零件平移時整體 Modify 用
public List SourceGeos { get; } = new();
- // Group totals are calculated by Aggregate.
+ // ── 統計(group 節點由 Aggregate 彙總)──
public int SolidCount { get; set; }
public int FaceCount { get; set; }
public int TriangleCount { get; set; }
@@ -42,12 +42,12 @@ public class ImportedNode
public record ImportedFileData(string FilePath, ImportedNode Root);
///
-/// Imports STEP, STL and DXF files on a background thread.
-/// Returned WPF Freezables are frozen.
+/// CAD 檔匯入(STEP / STL / DXF)。STEP 以 HierarchyToBlocks 還原裝配樹。
+/// 設計為在背景執行緒呼叫;回傳的 Freezable 全部已 Freeze。
///
public class StepImportService
{
- /// Import progress for the status bar.
+ /// 匯入階段回報(耗時分解),UI 可顯示於狀態列;null 則靜默
public Action? Progress { get; set; }
public Action>? ProgressTextFor { get; set; }
@@ -57,14 +57,15 @@ public class StepImportService
ProgressTextFor?.Invoke(textFor);
}
- private long _triTicks, _edgeTicks; // Accumulated tessellation and edge sampling ticks.
+ private long _triTicks, _edgeTicks; // 三角化 / 邊取樣 累計耗時(TimeSpan ticks,避免逐面 ms 截斷歸零)
private double TriMs => new TimeSpan(_triTicks).TotalMilliseconds;
private double EdgeMs => new TimeSpan(_edgeTicks).TotalMilliseconds;
- /// Geometry work queued per leaf, then run in parallel. Faces within a leaf share edges.
+ /// 延後的 leaf 幾何工作(三角化+邊取樣):樹走訪時收集,之後平行執行。
+ /// 平行粒度 = 整個 leaf(同 leaf 的面共用 Edge,不可面級平行)。
private readonly List _leafWork = new();
- /// Faces to retry sequentially after parallel tessellation.
+ /// 平行三角化偶發失敗的面(CADability 跨 leaf 仍有少量共享狀態)→ 平行結束後循序重試
private readonly System.Collections.Concurrent.ConcurrentBag<(ImportedNode Leaf, HashSet EdgeSet, List<(Face F, double Prec)> Failed)> _retry = new();
public ImportedFileData Import(string filePath)
@@ -97,7 +98,7 @@ public class StepImportService
});
_leafWork.Clear();
- // Retry failed faces sequentially after the parallel pass.
+ // 平行下偶發失敗的面:競爭已結束,循序重試一輪(FinishLeaf 也延到此時才跑)
int retried = 0;
foreach ((ImportedNode leaf, HashSet edgeSet, var failed) in _retry)
{
@@ -136,7 +137,12 @@ public class StepImportService
BuildChildren(root, EnumerateGeo(list));
}
- // STL is read directly as mesh geometry without B-rep objects.
+ // ─── 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)
{
@@ -161,7 +167,7 @@ public class StepImportService
NameNumber = g.NameNumber,
HasBrep = false,
SolidCount = 1,
- FaceCount = tris,
+ FaceCount = tris, // STL 無 B-rep 面 → 以三角形數代表
TriangleCount = tris,
Bounds = BoundsOfMesh(mesh),
};
@@ -173,7 +179,7 @@ public class StepImportService
ReportProgress(() => Format("Runtime.StlParsed", elapsed, meshCount));
}
- // DXF
+ // ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
private void ImportDxfFile(string path, ImportedNode root)
{
@@ -184,7 +190,7 @@ public class StepImportService
BuildChildren(root, EnumerateGeo(model.AllObjects));
}
- // Assembly traversal
+ // ─── 階層走訪 ────────────────────────────────────────────────
private static IEnumerable EnumerateGeo(System.Collections.IEnumerable list)
{
@@ -201,7 +207,7 @@ public class StepImportService
private void BuildChildren(ImportedNode parent, IEnumerable objects)
{
var looseFaces = new List();
- var looseCurves = new List();
+ var looseCurves = new List(); // 皆為 ICurve
foreach (IGeoObject go in objects)
{
@@ -245,7 +251,7 @@ public class StepImportService
if (failed.Count > 0) _retry.Add((leaf, edgeSet, failed));
else FinishLeaf(leaf, edgeSet);
});
- parent.Children.Add(leaf);
+ parent.Children.Add(leaf); // 三角化延後,空 leaf 由 Prune 收掉
}
if (looseCurves.Count > 0)
{
@@ -269,7 +275,7 @@ public class StepImportService
private ImportedNode? BlockNode(Block block)
{
- // Collapse a block containing one solid into a leaf.
+ // Block 只有單一同名 Solid → 折疊成 leaf(STEP 常見的 product→solid 包裝)
if (block.NumChildren == 1 && block.Child(0) is Solid s)
{
ImportedNode leaf = SolidNode(s);
@@ -290,7 +296,7 @@ public class StepImportService
BuildChildren(node, ChildrenOf(block));
if (node.Children.Count == 0) return null;
- // Drop a redundant group when it has one child with the same name.
+ // 單鏈折疊:group 只有一個子節點且名稱相同/空白 → 去掉中間層
if (node.Children.Count == 1)
{
ImportedNode only = node.Children[0];
@@ -354,7 +360,7 @@ public class StepImportService
return leaf;
}
- // Geometry processing
+ // ─── 幾何處理 ────────────────────────────────────────────────
private static BoundingCube SafeBounds(Func get)
{
@@ -362,7 +368,7 @@ public class StepImportService
catch { return new BoundingCube(); }
}
- /// Tessellation precision: bounding diagonal × 0.0015, clamped to [0.02, 0.5] mm.
+ /// 三角化精度依物件大小調整:對角線 × 0.0015,限制在 [0.02, 0.5] mm
private static double PrecisionFor(BoundingCube bc)
{
double diag;
@@ -372,7 +378,7 @@ public class StepImportService
return Math.Clamp(diag * 0.0015, 0.02, 0.5);
}
- /// False when tessellation fails and the face needs a retry.
+ /// false = 三角化失敗(平行下偶發,可循序重試)
private bool AddFace(Face face, double precision, ImportedNode leaf, HashSet edgeSet)
{
GeoPoint[] points; int[] indices;
@@ -383,7 +389,7 @@ public class StepImportService
}
catch
{
- return false;
+ return false; // 個別面三角化失敗時跳過,不讓整檔失敗
}
finally
{
@@ -411,7 +417,7 @@ public class StepImportService
return true;
}
- /// Completes edge sampling, counts and bounds for one leaf.
+ /// leaf 收尾:邊取樣 + 統計 + 邊界
private void FinishLeaf(ImportedNode leaf, HashSet edgeSet)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
@@ -448,7 +454,7 @@ public class StepImportService
return segments;
}
- /// Samples curves into paired line endpoints.
+ /// 把曲線取樣成線段對(直線 1 段、曲線 12 段)
private static void AppendCurveSegments(Point3DCollection segments, ICurve curve)
{
int n = curve is Line ? 1 : 12;
@@ -483,7 +489,7 @@ public class StepImportService
Math.Max(maxX - minX, 1e-6), Math.Max(maxY - minY, 1e-6), Math.Max(maxZ - minZ, 1e-6));
}
- /// Removes nodes left empty after tessellation.
+ /// 移除沒有任何幾何的節點(延後三角化全失敗的 leaf、因此變空的 group)
private static void Prune(ImportedNode node)
{
for (int i = node.Children.Count - 1; i >= 0; i--)
@@ -495,7 +501,7 @@ public class StepImportService
}
}
- /// Aggregates counts and bounds from children.
+ /// group 節點統計彙總(bottom-up)
private static void Aggregate(ImportedNode node)
{
if (node.IsLeaf) return;
diff --git a/src/STPViewer/Services/StlExportService.cs b/src/STPViewer/Services/StlExportService.cs
index c8a16d0..510a0c7 100644
--- a/src/STPViewer/Services/StlExportService.cs
+++ b/src/STPViewer/Services/StlExportService.cs
@@ -11,22 +11,27 @@ using CADability.GeoObject;
namespace STPViewer.Services;
///
-/// Mesh quality: Current uses the displayed mesh; Fine retessellates B-rep geometry.
+/// 網格精度:Current = 目前顯示網格;Fine = 由 B-rep 以較細精度重新三角化(無 B-rep 的來源維持現有網格)。
+/// 不提供「較粗」:CADability 的 GetTriangulation 對「比快取粗」的精度要求會直接回傳既有較細快取,
+/// 匯入時已三角化過 → 要求粗化實際上拿到同一份網格(實測驗證),故誠實不提供。
///
public enum StlMeshQuality { Current, Fine }
///
-/// Writes WPF triangle meshes as binary or ASCII STL; scale=1 uses millimeters.
+/// STL 匯出:WPF 三角網格 → binary / ASCII STL。
+/// STL 本身無單位,約定輸出 mm(scale=1);inch 交接用 scale = 1/25.4。
///
public static class StlExportService
{
///
- /// Tessellation factor relative to import precision; 0.15 produces a finer mesh.
+ /// 三角化精度係數(相對匯入預設):Fine 較細(三角形多、曲面平滑)。
+ /// 實測 CADability 三角形數對精度非單調(0.4× 重算反而比 1× 少 — 重算網格較有效率),
+ /// 要明顯更細需 ≤0.15×(test.stp:1× = 30,784 → 0.1× = 42,220 三角形)。
///
public static double PrecisionFactor(StlMeshQuality q) =>
q == StlMeshQuality.Fine ? 0.15 : 1.0;
- /// Estimated file size: 84 + 50 bytes per binary triangle, about 230 per ASCII triangle.
+ /// 預估檔案大小:binary = 84 + 50×n;ASCII 每三角形約 230 bytes
public static long EstimateBytes(long triangles, bool ascii) =>
ascii ? 20 + triangles * 230 : 84 + triangles * 50;
@@ -39,7 +44,8 @@ public static class StlExportService
};
///
- /// Writes meshes to one STL file, skipping zero-area triangles. Returns the triangle count.
+ /// 把多個網格寫成一個 STL 檔。退化(零面積)三角形濾除。
+ /// 回傳實際寫出的三角形數。
///
public static int Write(string path, IReadOnlyList meshes, bool ascii, double scale) =>
ascii ? WriteAscii(path, meshes, scale) : WriteBinary(path, meshes, scale);
@@ -49,11 +55,11 @@ public static class StlExportService
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 16);
using var bw = new BinaryWriter(fs);
- // Avoid "solid" in the header; some readers would mistake it for ASCII.
+ // 80-byte header(不可以 "solid" 開頭,部分讀取器會誤判為 ASCII)
var header = new byte[80];
Encoding.ASCII.GetBytes("STPViewer binary STL (unit: mm x scale)").CopyTo(header, 0);
bw.Write(header);
- bw.Write(0u); // Backfilled after writing triangles.
+ bw.Write(0u); // 三角形數 placeholder,寫完回頭補
int count = 0;
foreach (MeshGeometry3D mesh in meshes)
@@ -119,7 +125,7 @@ public static class StlExportService
sw.WriteLine(string.Format(inv, " vertex {0:e6} {1:e6} {2:e6}",
p.X * scale, p.Y * scale, p.Z * scale));
- /// False for a zero-area triangle.
+ /// false = 退化三角形(面積 ≈ 0)
private static bool TryNormal(Point3D a, Point3D b, Point3D c, out Vector3D n)
{
n = Vector3D.CrossProduct(b - a, c - a);
@@ -129,11 +135,12 @@ public static class StlExportService
return true;
}
- // B-rep retessellation
+ // ─── B-rep 重新三角化(精細/較粗匯出用)────────────────────────
///
- /// Retessellates B-rep geometry into frozen meshes, one per face.
- /// Call sequentially because CADability tessellation is not thread-safe.
+ /// 由 B-rep 幾何以指定精度係數重新三角化,回傳每面一個 frozen mesh。
+ /// CADability 非執行緒安全 → 呼叫端維持循序(可在單一背景執行緒)。
+ /// 個別面三角化失敗時跳過(與匯入一致的容錯策略)。
///
public static List Tessellate(IEnumerable geos, double precisionFactor)
{
@@ -163,7 +170,9 @@ public static class StlExportService
}
///
- /// Clamp the import precision before applying factor so Fine is finer than the cached mesh.
+ /// 匯入預設精度(與 StepImportService.PrecisionFor 同款 clamp)再乘 factor —
+ /// 先 clamp 再乘,保證 factor<1 時嚴格比匯入快取細(否則大殼在匯入被 0.5 上限截住,
+ /// 直接乘在 clamp 前會算出比快取粗的值 → CADability 回傳快取、精細無效)。
///
private static double PrecisionFor(BoundingCube bc, double factor)
{
diff --git a/src/STPViewer/Services/StlMeshReader.cs b/src/STPViewer/Services/StlMeshReader.cs
index e71a6aa..78b40b1 100644
--- a/src/STPViewer/Services/StlMeshReader.cs
+++ b/src/STPViewer/Services/StlMeshReader.cs
@@ -8,11 +8,14 @@ using System.Windows.Media.Media3D;
namespace STPViewer.Services;
///
-/// Reads binary and ASCII STL triangles directly into WPF meshes.
+/// 直接解析 STL 三角網格(binary / ASCII)→ WPF mesh,**不經過 CADability**。
+/// CADability 的 ImportSTL 會為每個三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),
+/// 大檔(百萬三角形)需數十分鐘且吃光記憶體;STL 本就無 B-rep,直接讀三角形即可。
+/// 實測 100k 三角形:CADability 約 41,000 ms,直接解析約 11 ms。
///
public static class StlMeshReader
{
- /// Mesh for one solid section; vertices remain unwelded.
+ /// 一個 solid 區塊的網格(頂點未焊接:每三角形 3 個獨立頂點,與 STL 原生一致)
public record MeshGroup(string Name, Point3DCollection Positions, Int32Collection Indices,
string? NameKey = null, int NameNumber = 0);
@@ -22,12 +25,13 @@ public static class StlMeshReader
}
///
- /// Binary STL has exactly 84 + 50 × triangle count bytes; its header may also start with "solid".
+ /// binary STL 判定:檔案大小必須剛好等於 84 + 50×count(count = offset 80 的 uint32)。
+ /// 這是最可靠的判別法 — binary 的 80-byte header 常也以 "solid" 開頭,不能只看開頭字串。
///
private static bool IsBinary(string path)
{
var fi = new FileInfo(path);
- if (fi.Length < 84) return false;
+ if (fi.Length < 84) return false; // 太小,當 ASCII 讓後續拋有意義的錯
Span head = stackalloc byte[84];
using (FileStream fs = File.OpenRead(path))
{
@@ -55,8 +59,8 @@ public static class StlMeshReader
int off = 84, vi = 0;
for (uint t = 0; t < count; t++)
{
- if (off + 50 > bytes.Length) break;
- off += 12; // WPF computes the normal.
+ if (off + 50 > bytes.Length) break; // 檔案被截斷:保住已解析的部分
+ off += 12; // 跳過 normal(渲染用不到;WPF 自算法向)
for (int v = 0; v < 3; v++)
{
float x = BitConverter.ToSingle(span[off..]);
@@ -72,7 +76,8 @@ public static class StlMeshReader
}
///
- /// ASCII STL: read vertex triples and start a new group at each solid section.
+ /// ASCII STL:逐行掃 "vertex x y z",每 3 個湊成一個三角形;"solid <name>" 起新群組。
+ /// ASCII 大檔罕見(同樣三角數約為 binary 的 5 倍體積),故以正確性為主。
///
private static List ReadAscii(string path)
{
diff --git a/src/STPViewer/StlExportDialog.xaml.cs b/src/STPViewer/StlExportDialog.xaml.cs
index 7ad9081..bb3b0f2 100644
--- a/src/STPViewer/StlExportDialog.xaml.cs
+++ b/src/STPViewer/StlExportDialog.xaml.cs
@@ -3,7 +3,7 @@ using STPViewer.ViewModels;
namespace STPViewer;
-/// STL export settings dialog; the view model owns the options.
+/// STL 匯出參數對話框 — 選項邏輯在 StlExportViewModel,這裡只做確認/取消
public partial class StlExportDialog : Window
{
public StlExportViewModel ViewModel { get; }
diff --git a/src/STPViewer/ViewModels/MainViewModel.Drag.cs b/src/STPViewer/ViewModels/MainViewModel.Drag.cs
index f91f8b7..c23331c 100644
--- a/src/STPViewer/ViewModels/MainViewModel.Drag.cs
+++ b/src/STPViewer/ViewModels/MainViewModel.Drag.cs
@@ -6,22 +6,22 @@ using static STPViewer.Services.LocalizationService;
namespace STPViewer.ViewModels;
-// Drag mode
+// ─── 拖曳模式(左鍵按住零件沿螢幕平面拖;放開才烘進 B-rep)──────────
public partial class MainViewModel
{
- // Preview with a transform, then commit to B-rep on release.
+ // 拖曳模式:暫時 Transform 跟著滑鼠,放開才一次性烘進 B-rep(TranslateRoot)
private ModelNodeViewModel? _dragRoot;
- private Point3D _dragAnchor;
- private Vector3D _dragApplied;
+ private Point3D _dragAnchor; // 命中點(世界座標),拖曳平面的錨點
+ private Vector3D _dragApplied; // 目前累計位移
private TranslateTransform3D? _dragTransform;
- /// Starts dragging a hit part; returns true when the mouse should be captured.
+ /// 左鍵按下:命中零件則開始拖曳(回傳 true 表示要捕捉滑鼠)
public bool OnDragStart(Point position)
{
if (_viewport is null || IsBusy || CurrentMode != MeasureMode.Drag || _dragRoot is not null)
return false;
- // Hits can come from merged meshes or section faces.
+ // 兩種渲染內容都可能在場:合併網格查 _mergedMap、逐面(剖面時)查 _faceMap
ModelNodeViewModel? leaf = null;
Point3D anchor = default;
foreach (var h in _viewport.Viewport.FindHits(position))
@@ -42,14 +42,14 @@ public partial class MainViewModel
if (l.BodyVisual is not null)
l.BodyVisual.Transform = _dragTransform;
- // Hide edges while moving to avoid rebuilding them each frame.
+ // 拖曳中隱藏邊線(LinesVisual3D 隨 Transform 變更逐幀重建會卡)
_edgesSuspended = true;
SetEdgesActive(false);
SetStatus(() => Format("Runtime.Dragging", root.Name));
return true;
}
- /// Projects pointer movement onto the camera-facing plane through the anchor.
+ /// 滑鼠移動:把 2D 位移投影到「過錨點、面向相機」的平面上 → 暫時 Transform
public void OnDragMove(Point position)
{
if (_viewport?.Camera is not System.Windows.Media.Media3D.ProjectionCamera cam ||
@@ -63,14 +63,15 @@ public partial class MainViewModel
_dragTransform.OffsetZ = _dragApplied.Z;
}
- /// Commits the temporary transform to B-rep on release.
+ /// 放開:移除暫時 Transform,一次性烘進 B-rep(量測精度不受拖曳影響)
public bool OnDragEnd()
{
if (_dragRoot is null) return false;
ModelNodeViewModel root = _dragRoot;
Vector3D delta = _dragApplied;
- // HelixToolkit hit testing requires Identity rather than a null transform.
+ // 清除暫時位移:必須用 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;
@@ -78,11 +79,11 @@ public partial class MainViewModel
_dragTransform = null;
_dragApplied = default;
_edgesSuspended = false;
- SetEdgesActive(true);
+ SetEdgesActive(true); // 恢復所有檔案的邊線(被拖檔案的點隨後由 TranslateRoot 更新,期間不會渲染)
if (delta.LengthSquared > 1e-12)
{
- TranslateRoot(root, delta);
+ TranslateRoot(root, delta); // 烘進 B-rep + 網格 + 邊線 + 邊界(量測清空)
SetStatus(() => Format("Runtime.Dragged", root.Name, delta.X, delta.Y, delta.Z));
}
return true;
diff --git a/src/STPViewer/ViewModels/MainViewModel.Export.cs b/src/STPViewer/ViewModels/MainViewModel.Export.cs
index e113215..a7b0f59 100644
--- a/src/STPViewer/ViewModels/MainViewModel.Export.cs
+++ b/src/STPViewer/ViewModels/MainViewModel.Export.cs
@@ -16,7 +16,7 @@ using static STPViewer.Services.LocalizationService;
namespace STPViewer.ViewModels;
-// CSV, STEP, STL, and screenshot export
+// ─── 匯出(量測 CSV / 視圖 PNG 截圖 / STEP / STL)────────────────────
public partial class MainViewModel
{
[RelayCommand]
@@ -42,14 +42,16 @@ public partial class MainViewModel
static string Esc(string s) => $"\"{s.Replace("\"", "\"\"")}\"";
sb.AppendLine($"{Esc(m.Title)},{Esc(Get($"Runtime.Kind{m.Kind}"))},{Esc(m.Detail.Replace("\n", " | "))}");
}
- File.WriteAllText(dlg.FileName, sb.ToString(), new UTF8Encoding(true)); // BOM for Excel.
+ File.WriteAllText(dlg.FileName, sb.ToString(), new UTF8Encoding(true)); // BOM:Excel 中文不亂碼
int count = Measurements.Count;
string fileName = Path.GetFileName(dlg.FileName);
SetStatus(() => Format("Runtime.CsvExported", count, fileName));
}
///
- /// Exports visible B-rep Solid and Shell objects at their current positions.
+ /// 把可見檔案(目前位置,含對齊/旋轉/拖曳結果)匯出成「新的」STEP 檔 —
+ /// 對齊插合後的配合結果可交接給 CAD;不動任何原始檔(唯讀原則不變)。
+ /// 只含 B-rep Solid/Shell(STL 網格、DXF 線架構無法進 STEP B-rep)。
///
[RelayCommand(CanExecute = nameof(IsIdle))]
private void ExportStepFile()
@@ -75,7 +77,7 @@ public partial class MainViewModel
try
{
- // B-rep objects already contain their transformed positions.
+ // B-rep 已含目前位置(TransformRoot 對 Solid/Shell 整體 Modify)→ 直接寫出
CADability.Project project = CADability.Project.CreateSimpleProject();
CADability.Model model = project.GetActiveModel();
foreach (CADability.GeoObject.IGeoObject g in geos) model.Add(g);
@@ -92,12 +94,13 @@ public partial class MainViewModel
}
///
- /// Exports visible meshes as STL using the selected format, scale and quality.
+ /// 匯出可見檔案(目前位置)為 STL 網格檔 — 先開參數對話框(範圍/格式/單位/精度 + 三角形數與大小預估)
+ /// 確認後才選路徑寫檔。精細/較粗 = 由 B-rep 重新三角化(背景執行緒);STL/DXF 來源一律用現有網格。
///
[RelayCommand(CanExecute = nameof(IsIdle))]
private async Task ExportStlFile()
{
- // Only visible leaves with meshes can be exported.
+ // 可匯出 = 可見 leaf 且有網格(DXF 純線架構無網格 → 排除)
var groups = Roots.Where(r => r.IsVisible)
.Select(r => (Root: r, Leaves: r.Leaves()
.Where(l => l.IsVisible && GetMergedMesh(l) is not null).ToList()))
@@ -113,7 +116,7 @@ public partial class MainViewModel
.Sum(l => GetMergedMesh(l)!.TriangleIndices.Count / 3L);
bool hasBrep = groups.SelectMany(g => g.Leaves).Any(l => l.HasBrep && l.SourceGeos.Count > 0);
- // Collect export options.
+ // 參數對話框(確認再匯出)
var vm = new StlExportViewModel(groups.Count, triangles, hasBrep);
var dialog = new StlExportDialog(vm) { Owner = Application.Current.MainWindow };
if (dialog.ShowDialog() != true || vm.Scale is not double scale) return;
@@ -128,7 +131,8 @@ public partial class MainViewModel
};
if (dlg.ShowDialog() != true) return;
- // Snapshot WPF geometry on the UI thread before background export.
+ // UI 執行緒先快照:GeometryModel3D/Model3DGroup 是 DispatcherObject 不可跨執行緒,
+ // 背景只能碰 frozen MeshGeometry3D 與 CADability B-rep(SourceGeos)
var snapshot = groups
.Select(g => (g.Root.Name, Leaves: g.Leaves
.Select(l => (Mesh: GetMergedMesh(l)!, l.HasBrep, l.SourceGeos))
@@ -182,14 +186,16 @@ public partial class MainViewModel
}
}
- /// Current merged mesh for an exportable leaf.
+ /// leaf 的合併網格(frozen、未剖切、目前位置)— STL 匯出的「目前顯示網格」來源
private static MeshGeometry3D? GetMergedMesh(ModelNodeViewModel leaf) =>
leaf.MergedContent?.Children.Count > 0
? (leaf.MergedContent.Children[0] as GeometryModel3D)?.Geometry as MeshGeometry3D
: null;
///
- /// Selects frozen current meshes or sequentially retessellates B-rep geometry.
+ /// 收集要寫進 STL 的網格(背景執行緒;輸入是 UI 快照):
+ /// 精細且有 B-rep → 重新三角化(循序,CADability 非執行緒安全);
+ /// 其餘(目前網格 / STL 來源)→ 快照的合併網格(frozen,可安全跨執行緒讀取)。
///
private static List CollectMeshes(
IReadOnlyList<(MeshGeometry3D Mesh, bool HasBrep, List SourceGeos)> leaves,
@@ -202,7 +208,7 @@ public partial class MainViewModel
{
List tess = StlExportService.Tessellate(sourceGeos, precisionFactor);
if (tess.Count > 0) { meshes.AddRange(tess); continue; }
- // Fall back to the current mesh if retessellation yields nothing.
+ // 重新三角化全數失敗 → 退回現有網格,不讓零件憑空消失
}
meshes.Add(mesh);
}
@@ -229,7 +235,7 @@ public partial class MainViewModel
};
if (dlg.ShowDialog() != true) return;
- const double scale = 2.0;
+ const double scale = 2.0; // 2x 解析度
var rtb = new RenderTargetBitmap(
(int)(_viewport.ActualWidth * scale), (int)(_viewport.ActualHeight * scale),
96 * scale, 96 * scale, PixelFormats.Pbgra32);
diff --git a/src/STPViewer/ViewModels/MainViewModel.Gizmo.cs b/src/STPViewer/ViewModels/MainViewModel.Gizmo.cs
index 5ac6a5d..144e2ef 100644
--- a/src/STPViewer/ViewModels/MainViewModel.Gizmo.cs
+++ b/src/STPViewer/ViewModels/MainViewModel.Gizmo.cs
@@ -8,37 +8,39 @@ using static STPViewer.Services.LocalizationService;
namespace STPViewer.ViewModels;
-// Gizmo translation and rotation
+// ─── Gizmo 三軸操作器(XYZ 箭頭 + 旋轉環;每次放開滑鼠烘進 B-rep)──────
public partial class MainViewModel
{
[ObservableProperty]
private bool gizmoEnabled;
private readonly List _gizmoParts = new();
- private ModelVisual3D? _gizmoProxy;
+ private ModelVisual3D? _gizmoProxy; // 操作器綁定的代理 visual(其 Transform = 使用者拖出的變換)
private ModelNodeViewModel? _gizmoTarget;
- private bool _gizmoDragActive;
- private bool _gizmoBaking;
- private bool _gizmoBakePending;
+ private bool _gizmoDragActive; // 拖動中(邊線已暫停)
+ private bool _gizmoBaking; // 烘焙中,忽略 Transform 變更回呼
+ private bool _gizmoBakePending; // 已排程烘焙,防同一次放開重複觸發
- // A synchronized overlay viewport keeps the gizmo above scene geometry.
+ // 操作器疊圖層:另一個透明 Viewport3D 疊在主視窗上、相機同步、只放操作器 →
+ // 操作器不在主場景,永不被實體遮擋(always-on-top);空白處滑鼠穿透回主視窗
private System.Windows.Controls.Viewport3D? _overlayViewport;
- private ProjectionCamera? _overlayCamera;
+ private ProjectionCamera? _overlayCamera; // 型別跟隨主相機(透視/正交切換,v0.5.0)
- /// Attaches the transparent gizmo overlay viewport.
+ /// 注入操作器疊圖層(透明 Viewport3D,疊在主視窗上、永遠最上層)
public void AttachOverlay(System.Windows.Controls.Viewport3D overlay)
{
_overlayViewport = overlay;
_overlayCamera = new PerspectiveCamera();
overlay.Camera = _overlayCamera;
- // Manipulator materials need lighting.
+ // 操作器材質需打光(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();
}
///
- /// Keeps the overlay camera in sync with the main camera and projection mode.
+ /// 疊圖層相機跟隨主相機(每次主相機變更時呼叫,讓操作器疊在正確螢幕位置)。
+ /// 主相機可能在透視/正交間切換(Orthographic 屬性)→ 疊圖層相機「型別」也要跟著換,投影才一致。
///
private void SyncOverlayCamera()
{
@@ -77,7 +79,7 @@ public partial class MainViewModel
partial void OnSelectedNodeChanged(ModelNodeViewModel? value)
{
- if (GizmoEnabled) UpdateGizmo();
+ if (GizmoEnabled) UpdateGizmo(); // 換選取 → 操作器跟著換目標
}
private void UpdateGizmo()
@@ -94,7 +96,7 @@ public partial class MainViewModel
return;
}
_gizmoTarget = root;
- SyncOverlayCamera();
+ SyncOverlayCamera(); // 操作器出現前先對齊相機
Rect3D b = root.Bounds;
var center = new Point3D(b.X + b.SizeX / 2, b.Y + b.SizeY / 2, b.Z + b.SizeZ / 2);
@@ -111,13 +113,13 @@ public partial class MainViewModel
man.Position = center;
man.Bind(_gizmoProxy);
_gizmoParts.Add(man);
- _overlayViewport!.Children.Add(man);
+ _overlayViewport!.Children.Add(man); // 放疊圖層 → 永不被實體遮擋
}
- // Translation arrows: X red, Y green, Z blue.
+ // 平移箭頭(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 });
- // Rotation rings
+ // 旋轉環
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 });
@@ -142,7 +144,7 @@ public partial class MainViewModel
_gizmoParts.Clear();
_gizmoProxy = null;
- if (_gizmoTarget is not 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;
@@ -155,7 +157,7 @@ public partial class MainViewModel
}
}
- /// Applies the proxy transform to target visuals during dragging.
+ /// 操作器拖動中:把代理的 Transform 套到目標檔案的所有 BodyVisual(暫時、GPU 端)
private void GizmoTransformChanged(object? sender, EventArgs e)
{
if (_gizmoBaking || _gizmoTarget is null || _gizmoProxy is null) return;
@@ -164,21 +166,21 @@ public partial class MainViewModel
{
_gizmoDragActive = true;
_edgesSuspended = true;
- SetEdgesActive(false);
+ SetEdgesActive(false); // 邊線逐幀重建會卡,拖動中暫停
}
foreach (ModelNodeViewModel l in _gizmoTarget.Leaves())
if (l.BodyVisual is not null)
l.BodyVisual.Transform = t;
}
- /// Commits the accumulated transform and resets the gizmo on mouse release.
+ /// 滑鼠放開(MainWindow 轉發,handledEventsToo):把累積變換烘進 B-rep 並重置操作器
public void OnGizmoMouseUp()
{
if (_gizmoTarget is null || _gizmoProxy is null || _gizmoBakePending) return;
Matrix3D m = _gizmoProxy.Transform?.Value ?? Matrix3D.Identity;
- if (m.IsIdentity) return;
+ if (m.IsIdentity) return; // 只是點一下、沒拖操作器 → 不烘焙
- // Commit after the manipulator completes its mouse-up handling.
+ // 延後到 manipulator 自身的 mouse-up 處理(釋放捕捉等)完成後再烘焙,避免在其事件中改動視覺樹造成 reentrancy
_gizmoBakePending = true;
_viewport?.Dispatcher.BeginInvoke(new Action(() => BakeGizmo(m)),
System.Windows.Threading.DispatcherPriority.Background);
@@ -190,19 +192,19 @@ public partial class MainViewModel
if (_gizmoTarget is null) return;
ModelNodeViewModel root = _gizmoTarget;
- _gizmoBaking = true;
+ _gizmoBaking = true; // 防 proxy 歸零觸發 GizmoTransformChanged 重入
foreach (ModelNodeViewModel l in root.Leaves())
if (l.BodyVisual is not null)
- l.BodyVisual.Transform = Transform3D.Identity;
- if (_gizmoProxy is not null) _gizmoProxy.Transform = Transform3D.Identity;
+ 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, Services.RigidAlign.ToModOp(m), m);
+ TransformRoot(root, Services.RigidAlign.ToModOp(m), m); // 烘進 B-rep(量測精度不受影響)
- // Move the gizmo to the transformed center.
+ // 操作器移到變換後的新中心
Rect3D b = root.Bounds;
if (!b.IsEmpty)
{
diff --git a/src/STPViewer/ViewModels/MainViewModel.Interference.cs b/src/STPViewer/ViewModels/MainViewModel.Interference.cs
index a49a2ca..93dd8f6 100644
--- a/src/STPViewer/ViewModels/MainViewModel.Interference.cs
+++ b/src/STPViewer/ViewModels/MainViewModel.Interference.cs
@@ -13,7 +13,7 @@ using static STPViewer.Services.LocalizationService;
namespace STPViewer.ViewModels;
-// Interference checking
+// ─── 干涉檢查 ────────────────────────────────────────────────────────
public partial class MainViewModel
{
[RelayCommand(CanExecute = nameof(IsIdle))]
@@ -35,7 +35,7 @@ public partial class MainViewModel
.Select(m => _faceMap[(GeometryModel3D)m].Mesh)
.ToList();
- // Snapshot meshes before background calculations.
+ // 網格快照(開跑後零件被改走也不會半途換料;v0.4.0 起運算期間指令已鎖,這裡是雙保險)
var meshes = visibleRoots.ToDictionary(r => r, MeshesOf);
IsBusy = true;
diff --git a/src/STPViewer/ViewModels/MainViewModel.Section.cs b/src/STPViewer/ViewModels/MainViewModel.Section.cs
index 618cecd..8d2e6db 100644
--- a/src/STPViewer/ViewModels/MainViewModel.Section.cs
+++ b/src/STPViewer/ViewModels/MainViewModel.Section.cs
@@ -13,15 +13,16 @@ using static STPViewer.Services.LocalizationService;
namespace STPViewer.ViewModels;
-// Section clipping
+// ─── 剖面(CPU 網格裁切;v0.4.0 起裁切在背景執行緒平行計算,UI 不凍結)──
public partial class MainViewModel
{
private readonly DispatcherTimer _sectionTimer;
private RectangleVisual3D? _sectionPlaneVisual;
- private Point3D _sectionPlanePoint;
+ private Point3D _sectionPlanePoint; // SectionEnabled 時的剖切平面(合併邊線重建用)
private Vector3D _sectionNormal;
- // Reapply clipping if parameters change while background work is running.
+ // 背景裁切 guard:進行中又有新變更(拉滑桿/換軸/零件平移)→ 完成後用最新參數重跑一輪。
+ // 不要改回同步呼叫 ClipMesh —— 大檔(64k 面)整場景裁切會凍結 UI 數秒
private bool _sectionApplying;
private bool _sectionReapply;
@@ -32,20 +33,20 @@ public partial class MainViewModel
[ObservableProperty]
private int sectionAxisIndex;
- /// Section position along the selected axis, from 0 to 100%.
+ /// 剖面位置(沿軸 0~100%)
[ObservableProperty]
private double sectionPosition = 50;
[ObservableProperty]
private bool sectionFlip;
- /// Closes the section parameter bar.
+ /// 剖面參數列的「✕ 關閉」按鈕(參數列只在剖面開啟時顯示)
[RelayCommand]
private void CloseSection() => SectionEnabled = false;
- // Three-point custom cutting plane
+ // ── 3點自訂剖切平面(SectionAxisIndex == 3,v0.5.0)──
private const int CustomAxisIndex = 3;
- private Vector3D? _customNormal;
+ private Vector3D? _customNormal; // null = 尚未定義(等使用者點 3 點)
private readonly List _sectionPicks = new();
private readonly List _sectionPickOverlays = new();
@@ -64,7 +65,7 @@ public partial class MainViewModel
}
else
{
- // Leaving custom mode discards the old plane.
+ // 離開自訂平面 → 丟棄舊平面(再選「3點」即重新定義)
_customNormal = null;
ClearSectionPicks();
}
@@ -73,14 +74,14 @@ public partial class MainViewModel
partial void OnSectionPositionChanged(double value)
{
- if (value < 0) { SectionPosition = 0; return; }
+ if (value < 0) { SectionPosition = 0; return; } // 位置數值輸入框防呆
if (value > 100) { SectionPosition = 100; return; }
ScheduleSection();
}
partial void OnSectionFlipChanged(bool value) => ScheduleSection();
- /// Handles a custom plane pick; returns true when the click is consumed.
+ /// 3點剖面定義模式中的點擊(OnViewportClick 轉入)。回傳 true = 已消費此點擊。
public bool HandleSectionPlanePick(System.Windows.Point position)
{
if (!SectionEnabled || SectionAxisIndex != CustomAxisIndex || _customNormal is not null) return false;
@@ -136,14 +137,14 @@ public partial class MainViewModel
return true;
}
- /// Cancels custom plane picking; returns true if it was active.
+ /// 取消 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;
+ SectionAxisIndex = 0; // 回 X 軸
if (!quiet) SetStatus(() => Get("Runtime.SectionCancelled"));
return true;
}
@@ -167,7 +168,8 @@ public partial class MainViewModel
if (!SectionEnabled)
{
- // Restore original meshes. Background clipping checks SectionEnabled before applying.
+ // 還原原始幾何(換回 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;
@@ -177,7 +179,7 @@ public partial class MainViewModel
_viewport.Children.Remove(_sectionPlaneVisual);
_sectionPlaneVisual = null;
}
- ApplyRenderMode();
+ ApplyRenderMode(); // 剖面關閉 → 瀏覽模式可切回合併網格
SetStatus(() => Get("Runtime.SectionClosed"));
return;
}
@@ -196,7 +198,7 @@ public partial class MainViewModel
Vector3D axis;
if (SectionAxisIndex == CustomAxisIndex)
{
- if (_customNormal is null) return;
+ if (_customNormal is null) return; // 平面尚未定義(等使用者點 3 點)
axis = _customNormal.Value;
}
else
@@ -212,7 +214,8 @@ public partial class MainViewModel
Point3D min = bounds.Location;
var size = new Vector3D(bounds.SizeX, bounds.SizeY, bounds.SizeZ);
Point3D center = min + size / 2;
- // Interpolate along the projected bounds of the scene AABB.
+ // 平面位置:場景 AABB 8 個角投影到法向的 [dmin, dmax],滑桿 t 內插
+ //(軸向法向 = 與舊版逐軸計算等價;任意法向也成立)
double dmin = double.MaxValue, dmax = double.MinValue;
for (int ci = 0; ci < 8; ci++)
{
@@ -228,7 +231,9 @@ public partial class MainViewModel
Point3D planePoint = center + axis * (dStar - Vector3D.DotProduct((Vector3D)center, axis));
Vector3D normal = SectionFlip ? -axis : axis;
- // Clip frozen meshes in the background, then apply on the UI thread.
+ // 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)
@@ -246,16 +251,16 @@ public partial class MainViewModel
return result;
});
- if (!SectionEnabled) return;
+ if (!SectionEnabled) return; // 裁切期間被關掉 → 還原分支已處理,丟棄結果
for (int i = 0; i < models.Count; i++)
- models[i].Geometry = clipped[i];
+ models[i].Geometry = clipped[i]; // 已移除檔案的殘留 model 賦值無害(不在視覺樹上)
_sectionPlanePoint = planePoint;
_sectionNormal = normal;
foreach (ModelNodeViewModel root in Roots) RefreshRootEdges(root);
- ApplyRenderMode();
+ ApplyRenderMode(); // 剖面開啟 → 改用逐面(裁切後幾何)
UpdateSectionPlaneVisual(planePoint, axis, size);
int axisIndex = SectionAxisIndex;
double position = SectionPosition;
@@ -296,7 +301,7 @@ public partial class MainViewModel
_viewport!.Children.Add(_sectionPlaneVisual);
}
- /// Returns a unit vector perpendicular to n for the custom plane visual.
+ /// 取任一與 n 垂直的單位向量(自訂平面的指示矩形方向用)
private static Vector3D AnyPerpendicular(Vector3D n)
{
Vector3D v = Math.Abs(n.Z) < 0.9
diff --git a/src/STPViewer/ViewModels/MainViewModel.cs b/src/STPViewer/ViewModels/MainViewModel.cs
index 0a53617..f767721 100644
--- a/src/STPViewer/ViewModels/MainViewModel.cs
+++ b/src/STPViewer/ViewModels/MainViewModel.cs
@@ -23,33 +23,34 @@ public partial class MainViewModel : ObservableObject
private readonly StepImportService _importService = new();
private readonly MeasurementService _measureService = new();
- /// Maps rendered face models to their B-rep face data for hit testing.
+ /// 渲染物件 → B-rep Face 反查表(HitTest 用;overlay 不在表內所以自然被忽略)
private readonly Dictionary _faceMap = new();
- /// Maps merged meshes to leaves for drag hit testing.
+ /// 合併網格 → leaf 反查表(拖曳模式在瀏覽渲染下 hit-test 用)
private readonly Dictionary _mergedMap = new();
- /// Vertex boundaries and faces used to resolve hits on merged meshes.
- /// The order matches BuildMergedMesh; transforms do not change vertex counts.
+ /// 合併網格 → (每面頂點起始邊界, 對應 FaceInfo)。命中合併網格時用三角形頂點 index 二分搜尋反查回是哪個面,
+ /// 讓量測模式也能 render 合併網格(62 個 model)而非逐面(數萬個 model)。starts 長度 = faces+1,最後一格為總頂點數。
+ /// 邊界由匯入時依面序串接而成,與 BuildMergedMesh 串接順序一致;平移不改各面頂點數,故邊界永久有效。
private readonly Dictionary _mergedFaceRanges = new();
private HelixViewport3D? _viewport;
private int _paletteIndex;
private readonly Dictionary _counters = new();
- // First picks for multi-step measurements.
- private Point3D? _pendingPoint;
- private DirectionPick? _pendingDirection;
- private FaceInfo? _pendingFace;
- private (ModelNodeViewModel Root, Point3D Point)? _pendingAlign;
- private readonly List<(ModelNodeViewModel Root, Point3D P)> _align3 = new();
+ // 兩段式量測的第一次拾取暫存
+ private Point3D? _pendingPoint; // 距離
+ private DirectionPick? _pendingDirection; // 角度
+ private FaceInfo? _pendingFace; // 面距
+ private (ModelNodeViewModel Root, Point3D Point)? _pendingAlign; // 兩點對齊
+ private readonly List<(ModelNodeViewModel Root, Point3D P)> _align3 = new(); // 三點對齊(前3=來源、後3=目標)
private readonly List _pendingOverlays = new();
- /// Selected tree node used as the target for commands.
+ /// 樹面板目前選取的節點(MainWindow SelectedItemChanged 轉發;旋轉指令的目標)
[ObservableProperty]
private ModelNodeViewModel? selectedNode;
- // Suspend edge rendering during camera interaction.
+ // 互動中暫停邊線:轉動/縮放/平移時隱藏 LinesVisual3D,停下再顯示 → 大組件流暢
private readonly DispatcherTimer _interactionTimer;
private bool _edgesSuspended;
@@ -88,7 +89,7 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty]
private bool useInch;
- /// Tree name filter; does not affect viewport visibility.
+ /// 樹面板名稱過濾字串(只影響樹顯示,不影響 3D 可見性)
[ObservableProperty]
private string? treeFilter;
@@ -97,16 +98,16 @@ public partial class MainViewModel : ObservableObject
foreach (ModelNodeViewModel root in Roots) root.ApplyFilter(value);
}
- /// Recently opened files saved in settings.json.
+ /// 最近開啟檔案(MRU,最多 10 筆;隨 settings.json 保存)
public ObservableCollection RecentFiles { get; } = new();
public ObservableCollection Roots { get; } = new();
public ObservableCollection Measurements { get; } = new();
- /// Quick bar button selections.
+ /// 快速工具列自訂(選單列有全部功能,快速列只放勾選的常用按鈕)
public QuickBarViewModel QuickBar { get; } = new();
- // User settings
+ // ─── 使用者設定(視窗由 MainWindow 處理;VM 管單位 + MRU)─────
public void LoadSettings(AppSettings s)
{
@@ -139,18 +140,23 @@ public partial class MainViewModel : ObservableObject
_sectionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
_sectionTimer.Tick += (_, _) => { _sectionTimer.Stop(); ApplySection(); };
- // Restore edges after camera movement stops.
+ // 相機停止移動 180ms 後恢復邊線
_interactionTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(180) };
_interactionTimer.Tick += (_, _) => { _interactionTimer.Stop(); ResumeEdges(); };
}
private UnitSystem CurrentUnit => UseInch ? UnitSystem.Inch : UnitSystem.Millimeter;
- /// Attaches the viewport from MainWindow.
+ /// 由 MainWindow 注入 viewport(桌面工具,不做過度抽象)
public void Attach(HelixViewport3D viewport)
{
_viewport = viewport;
- // The viewport event also catches camera instance replacements by HelixToolkit.
+ // 相機一動就暫停邊線/逐面(轉動/縮放/平移皆觸發),停下 180ms 再恢復。
+ // 兩個事件都訂閱,互補保底:
+ // 1) viewport.CameraChanged(控制項層級 routed event)— 相機實例若被 Helix 內部換掉也接得住;
+ // 2) Camera.Changed(Freezable,相機 Position/方向一被改就觸發)— 確保每次移動都收到。
+ // 舊寫法只訂 Camera.Changed 且包在 null 檢查裡,萬一相機實例被換就永久失聯 → 暫停機制全失效。
+ // 重複觸發 OnCameraMoved 無害(idempotent,內部有 _edgesSuspended guard)。
viewport.CameraChanged += (_, _) => OnCameraMoved();
if (viewport.Camera is not null)
viewport.Camera.Changed += (_, _) => OnCameraMoved();
@@ -158,7 +164,7 @@ public partial class MainViewModel : ObservableObject
private void OnCameraMoved()
{
- SyncOverlayCamera();
+ SyncOverlayCamera(); // 操作器疊圖層跟著主相機
if (!_edgesSuspended) { _edgesSuspended = true; SetEdgesActive(false); }
_interactionTimer.Stop();
_interactionTimer.Start();
@@ -170,7 +176,7 @@ public partial class MainViewModel : ObservableObject
SetEdgesActive(true);
}
- /// Shows or hides merged root edges without rebuilding points.
+ /// 只切換 root 合併邊線的 viewport 掛載(不重建 Points)
private void SetEdgesActive(bool active)
{
if (_viewport is null) return;
@@ -179,10 +185,10 @@ public partial class MainViewModel : ObservableObject
SyncVisual(root.EdgeVisual, active && root.EdgeVisual.Points.Count > 0);
}
- /// Disables scene-changing commands while background work runs.
+ /// IsBusy 時停用會改動幾何/場景的指令(匯入、旋轉、干涉),避免背景運算期間狀態被改走造成結果錯位
private bool IsIdle() => !IsBusy;
- // Import
+ // ─── 匯入 ────────────────────────────────────────────────────
[RelayCommand(CanExecute = nameof(IsIdle))]
private async Task ImportAsync()
@@ -197,24 +203,25 @@ public partial class MainViewModel : ObservableObject
await ImportFilesAsync(dlg.FileNames);
}
- /// Imports a recently opened file.
+ /// MRU 下拉點選(檔案可能已被移走 → ImportFilesAsync 會報匯入失敗)
[RelayCommand(CanExecute = nameof(IsIdle))]
private Task ImportRecentAsync(string path) => ImportFilesAsync(new[] { path });
public async Task ImportFilesAsync(IEnumerable paths)
{
if (_viewport is null) return;
- // Reject overlapping imports because StepImportService has shared state.
+ // 拖放/命令列可繞過指令 CanExecute 直接進來 → 匯入或背景運算期間直接擋掉,
+ // 避免兩批匯入交錯(StepImportService 有共享狀態,非重入安全)
if (IsBusy) { SetStatus(() => Get("Runtime.ImportBusy")); return; }
int ok = 0, fail = 0;
- IsBusy = true;
+ IsBusy = true; // 整批匯入期間維持 busy(v0.4.0:不再逐檔開關,杜絕檔案之間的重入空窗)
try
{
foreach (string path in paths)
{
string name = Path.GetFileNameWithoutExtension(path);
SetStatus(() => Format("Runtime.Importing", name));
- // Marshal progress from the import thread to the UI thread.
+ // 匯入階段回報(從背景執行緒來 → 切回 UI 執行緒)
_importService.ProgressTextFor = textFor =>
System.Windows.Application.Current?.Dispatcher.BeginInvoke(
() => SetStatus(() => Format("Runtime.ImportProgress", name, textFor())));
@@ -257,13 +264,13 @@ public partial class MainViewModel : ObservableObject
ModelNodeViewModel root = BuildNode(data.Root, isRoot: true, data.FilePath, name);
- // Disable edges by default for very large files.
+ // 邊線總量過大 → 整檔預設關閉(cascade;使用者可再開)
int totalSegments = root.Leaves().Sum(l => (l.OriginalEdgePoints?.Count ?? 0) / 2);
if (totalSegments > 30_000)
root.ShowEdges = false;
Roots.Add(root);
- if (!string.IsNullOrWhiteSpace(TreeFilter)) root.ApplyFilter(TreeFilter);
+ if (!string.IsNullOrWhiteSpace(TreeFilter)) root.ApplyFilter(TreeFilter); // 新檔套用目前過濾
foreach (ModelNodeViewModel leaf in root.Leaves())
{
leaf.VisualStateChanged += _ => SyncLeafVisuals(root);
@@ -271,7 +278,7 @@ public partial class MainViewModel : ObservableObject
_viewport!.Children.Add(leaf.BodyVisual);
}
- // One merged edge visual per file avoids per-leaf rebuilds.
+ // 整檔合併成「一條」LinesVisual3D(轉動時只重建 1 條,避免逐零件 N 條造成卡頓)
if (root.Leaves().Any(l => l.OriginalEdgePoints is { Count: > 0 }))
{
root.EdgeVisual = new LinesVisual3D
@@ -300,7 +307,7 @@ public partial class MainViewModel : ObservableObject
if (n.Faces.Count > 0)
{
- // Share material across leaf faces for color changes.
+ // leaf 共用材質(換色時只改 Brush)
var diffuseBrush = new SolidColorBrush(vm.Color);
diffuseBrush.Freeze();
var diffuse = new DiffuseMaterial(diffuseBrush);
@@ -311,7 +318,8 @@ public partial class MainViewModel : ObservableObject
backMat.Freeze();
vm.SharedMaterial = diffuse;
- // Keep one model per face for section picking and record merged-mesh face boundaries.
+ // 一個 B-rep Face = 一個 GeometryModel3D,登錄反查表(剖面模式逐面拾取用)。
+ // 同時依面序記錄每面頂點起始邊界 + 對應 FaceInfo,供「命中合併網格反查面」(量測模式拾取用)。
var group = new Model3DGroup();
var faceInfos = new FaceInfo[n.Faces.Count];
var vertStarts = new int[n.Faces.Count + 1];
@@ -330,9 +338,11 @@ public partial class MainViewModel : ObservableObject
vertStarts[n.Faces.Count] = vacc;
vm.FacesContent = group;
- // BuildMergedMesh concatenates faces in the same order as vertStarts.
+ // 合併網格:整零件所有面併成 1 個 GeometryModel3D(draw call 從每面 1 個降為每檔 1 個)。
+ // BuildMergedMesh 依 n.Faces 同序串接(baseIdx += positions.Count),與上面 vertStarts 對齊 → 命中頂點 index 可反查回面。
var mergedModel = new GeometryModel3D(BuildMergedMesh(n.Faces), matGroup);
- // Closed solids render front faces only; open shells and STL render both sides.
+ // 封閉實體背面不可見 → 不設 BackMaterial(WPF 兩面渲染成本砍半);開放殼/STL 仍需背面。
+ // 拾取不受材質影響(WPF 3D hit-test 純幾何、不剔背面),故孔內壁仍可命中。
if (n.SolidCount == 0 || !n.HasBrep)
mergedModel.BackMaterial = backMat;
vm.MergedContent = new Model3DGroup();
@@ -340,14 +350,14 @@ public partial class MainViewModel : ObservableObject
_mergedMap[mergedModel] = vm;
_mergedFaceRanges[mergedModel] = (vertStarts, faceInfos);
- // Use merged meshes except in section mode, which needs separate clipped faces.
+ // 非剖面一律顯示合併網格(瀏覽/量測都是,量測靠 _mergedFaceRanges 反查面);只有剖面才逐面(裁切後幾何)
vm.BodyVisual = new ModelVisual3D
{
Content = SectionEnabled ? group : vm.MergedContent,
};
}
- // Leaves hold edge data; roots hold the merged edge visual.
+ // 邊線端點僅保存於 leaf(資料);實際 LinesVisual3D 由 root 合併建立(見 BuildRoot)
if (n.EdgeSegments is { Count: > 0 })
vm.OriginalEdgePoints = n.EdgeSegments;
@@ -366,7 +376,8 @@ public partial class MainViewModel : ObservableObject
}
///
- /// Rebuilds one edge visual from visible leaves, clipping segments in section mode.
+ /// 重建整檔的合併邊線:收集所有「可見且開邊線」的 leaf 端點為單一 Point3DCollection。
+ /// 剖面開啟時即時裁切。一檔只有一條 LinesVisual3D → 轉動視角不再逐零件重建。
///
private void RefreshRootEdges(ModelNodeViewModel root)
{
@@ -393,9 +404,9 @@ public partial class MainViewModel : ObservableObject
else if (!shouldShow && contains) _viewport.Children.Remove(visual);
}
- // Merged meshes
+ // ─── 合併網格(瀏覽模式降 draw call)────────────────────────
- /// Concatenates face meshes in the given order.
+ /// 把多個面網格依序併成單一 MeshGeometry3D(索引位移後串接;順序 = 呼叫端給的面序)
private static MeshGeometry3D MergeMeshes(IReadOnlyList meshes)
{
int nv = 0, nt = 0;
@@ -422,7 +433,7 @@ public partial class MainViewModel : ObservableObject
return mesh;
}
- /// Builds the import mesh in face order for merged hit resolution.
+ /// 匯入時建合併網格(依 faces 面序串接,與 _mergedFaceRanges 的頂點邊界對齊)
private static MeshGeometry3D BuildMergedMesh(IReadOnlyList faces)
{
var meshes = new List(faces.Count);
@@ -430,7 +441,8 @@ public partial class MainViewModel : ObservableObject
return MergeMeshes(meshes);
}
- /// Rebuilds a merged mesh after a transform without changing face vertex boundaries.
+ /// 重建合併網格(位置變更後,如零件平移)。由 leaf 逐面 fi.Mesh(未剖切原始)依 FacesContent 子序串接,
+ /// 與匯入時 BuildMergedMesh 的面序一致 → _mergedFaceRanges 頂點邊界維持有效。
private void RebuildMerged(ModelNodeViewModel leaf)
{
if (leaf.MergedContent is null || leaf.FacesContent is null) return;
@@ -442,11 +454,13 @@ public partial class MainViewModel : ObservableObject
}
///
- /// Uses merged meshes for browsing and measurement; section mode uses separate clipped faces.
+ /// 依目前狀態切換每個 leaf 的渲染內容:
+ /// 非剖面(瀏覽 + 量測皆是)→ 合併網格(每檔 1 個 model,快);量測拾取靠命中合併網格反查面(_mergedFaceRanges)。
+ /// 剖面 → 逐面(要顯示各面裁切後的幾何)。
///
private void ApplyRenderMode()
{
- // Measurement resolves merged-mesh hits back to their source faces.
+ // 量測不再需要逐面渲染(拾取打合併網格反查面)→ 只有剖面才逐面。永不在量測模式掛數萬個 GeometryModel3D。
bool merged = !SectionEnabled;
foreach (ModelNodeViewModel leaf in Roots.SelectMany(r => r.Leaves()))
{
@@ -457,7 +471,7 @@ public partial class MainViewModel : ObservableObject
}
}
- // Node commands
+ // ─── 節點操作 ────────────────────────────────────────────────
[RelayCommand]
private void ZoomNode(ModelNodeViewModel node)
@@ -469,14 +483,14 @@ public partial class MainViewModel : ObservableObject
[RelayCommand]
private void ZoomAll() => _viewport?.ZoomExtents(400);
- /// Standard views with Z up.
+ /// 標準視圖(機構慣例 Z 向上):Iso / Front / Back / Top / Bottom / Left / Right
[RelayCommand]
private void SetStandardView(string name)
{
if (_viewport?.Camera is not ProjectionCamera cam) return;
(Vector3D dir, Vector3D up) = name switch
{
- "Front" => (new Vector3D(0, 1, 0), new Vector3D(0, 0, 1)),
+ "Front" => (new Vector3D(0, 1, 0), new Vector3D(0, 0, 1)), // 從 -Y 往 +Y 看
"Back" => (new Vector3D(0, -1, 0), new Vector3D(0, 0, 1)),
"Top" => (new Vector3D(0, 0, -1), new Vector3D(0, 1, 0)),
"Bottom" => (new Vector3D(0, 0, 1), new Vector3D(0, -1, 0)),
@@ -490,13 +504,13 @@ public partial class MainViewModel : ObservableObject
: new Point3D(b.X + b.SizeX / 2, b.Y + b.SizeY / 2, b.Z + b.SizeZ / 2);
double dist = Math.Max(SceneDiagonal() * 1.8, 10);
cam.Position = center - dir * dist;
- cam.LookDirection = dir * dist;
+ cam.LookDirection = dir * dist; // 目標點 = Position + LookDirection = 場景中心
cam.UpDirection = up;
_viewport.ZoomExtents(400);
SetStatus(() => Format("Runtime.View", Get($"Runtime.View{name}")));
}
- /// Toggles orthographic projection while preserving the view angle.
+ /// 正交投影切換(平行投影,量尺寸不受透視變形;Helix 保持視角換相機型別)
[ObservableProperty]
private bool orthographicView;
@@ -512,16 +526,16 @@ public partial class MainViewModel : ObservableObject
{
if (_viewport is null || !root.IsRoot) return;
- if (ReferenceEquals(root, _gizmoTarget)) GizmoEnabled = false;
+ if (ReferenceEquals(root, _gizmoTarget)) GizmoEnabled = false; // 目標被移除 → 收掉操作器
- // Clear measurements that may be attached to the removed file.
+ // 量測 overlay 可能標在此檔模型上 → 一併清除(簡化:全清)
if (Measurements.Count > 0) ClearMeasurements();
foreach (ModelNodeViewModel leaf in root.Leaves())
{
if (leaf.BodyVisual is not null)
_viewport.Children.Remove(leaf.BodyVisual);
- if (leaf.FacesContent is not null)
+ if (leaf.FacesContent is not null) // 逐面群組才在 _faceMap(與目前顯示哪種內容無關)
foreach (Model3D m in leaf.FacesContent.Children)
_faceMap.Remove(m);
if (leaf.MergedContent is not null)
@@ -538,9 +552,9 @@ public partial class MainViewModel : ObservableObject
SetStatus(() => Format("Runtime.Removed", removedName));
}
- // Visibility and volume
+ // ─── 顯示控制 / 體積(樹右鍵選單)────────────────────────────
- /// Shows only this node until ShowAll is called.
+ /// 只顯示此節點(其餘全部隱藏;「全部顯示」復原)
[RelayCommand]
private void IsolateNode(ModelNodeViewModel node)
{
@@ -564,14 +578,15 @@ public partial class MainViewModel : ObservableObject
SetStatus(() => Get("Runtime.AllVisible"));
}
- /// Sets visibility for every node, including mixed child states.
+ /// 逐節點顯式遞迴設定可見性。不能只設 root(setter 同值不觸發 cascade,
+ /// 之前的隔離操作可能留下「父關子開」的混合狀態)
private static void SetVisibleRecursive(ModelNodeViewModel node, bool visible)
{
node.IsVisible = visible;
foreach (ModelNodeViewModel c in node.Children) SetVisibleRecursive(c, visible);
}
- /// Adds a volume and centroid measurement for a closed mesh.
+ /// 體積/質心(網格 signed volume,封閉實體才可靠)→ 加進量測結果 + 質心標記
[RelayCommand]
private void MeasureVolume(ModelNodeViewModel node)
{
@@ -621,12 +636,12 @@ public partial class MainViewModel : ObservableObject
AddMeasurement(m);
}
- // Measurement
+ // ─── 量測 ────────────────────────────────────────────────────
partial void OnCurrentModeChanged(MeasureMode value)
{
CancelPending();
- ApplyRenderMode();
+ ApplyRenderMode(); // 非剖面一律合併網格(量測拾取靠反查面);不再因切量測模式而掛逐面
string? key = value switch
{
MeasureMode.None => "Runtime.ModeBrowse",
@@ -652,10 +667,10 @@ public partial class MainViewModel : ObservableObject
SetStatus(() => Get(value ? "Runtime.UnitInch" : "Runtime.UnitMillimeter"));
}
- /// Cancels a pending pick, then exits measurement mode or closes the gizmo.
+ /// Esc(MainWindow 轉發):先取消進行中的多段量測(保留模式),再按退回瀏覽模式;再退操作器
public void OnEscape()
{
- if (CancelSectionPickIfActive()) return;
+ if (CancelSectionPickIfActive()) return; // 3點剖面定義中 → 先取消定義
bool hadPending = _pendingPoint is not null || _pendingDirection is not null ||
_pendingFace is not null || _pendingAlign is not null || _align3.Count > 0;
if (hadPending)
@@ -673,7 +688,7 @@ public partial class MainViewModel : ObservableObject
}
}
- /// Handles measurement hotkeys; repeating a key returns to browse mode.
+ /// 量測模式快速鍵(MainWindow 轉發;無修飾鍵)。同鍵再按 = 退回瀏覽。回傳 false 表示非快速鍵。
public bool OnHotKey(System.Windows.Input.Key key)
{
MeasureMode? m = key switch
@@ -692,11 +707,11 @@ public partial class MainViewModel : ObservableObject
return true;
}
- /// Handles left-clicks forwarded by MainWindow.
+ /// MainWindow 滑鼠左鍵點擊轉發進來
public void OnViewportClick(Point position)
{
if (_viewport is null || IsBusy) return;
- if (HandleSectionPlanePick(position)) return;
+ if (HandleSectionPlanePick(position)) return; // 3點剖面定義模式優先吃點擊(v0.5.0)
if (CurrentMode is MeasureMode.None or MeasureMode.Drag) return;
var hits = _viewport.Viewport.FindHits(position);
@@ -706,13 +721,13 @@ public partial class MainViewModel : ObservableObject
foreach (var h in hits)
{
if (h.Model is null) continue;
- // Section mode renders individual faces.
+ // 剖面模式:渲染逐面 → 直接查 _faceMap
if (_faceMap.TryGetValue(h.Model, out FaceInfo? f))
{
fi = f; hitPoint = h.Position; hitNormal = h.Normal;
break;
}
- // Other modes resolve merged-mesh vertex indices to faces.
+ // 量測/瀏覽模式:渲染合併網格 → 用命中三角形頂點 index 反查回是哪個面
if (h.RayHit is not null && _mergedFaceRanges.TryGetValue(h.Model, out var range))
{
FaceInfo? mf = ResolveMergedFace(range, h.RayHit.VertexIndex1);
@@ -740,13 +755,13 @@ public partial class MainViewModel : ObservableObject
}
}
- /// Finds the source face for a merged-mesh vertex by binary search.
+ /// 命中合併網格的某頂點 index → 反查回是哪個面(依面序的頂點起始邊界二分搜尋)。
private static FaceInfo? ResolveMergedFace((int[] Starts, FaceInfo[] Faces) range, int vertexIndex)
{
int[] starts = range.Starts;
FaceInfo[] faces = range.Faces;
if (vertexIndex < 0 || faces.Length == 0 || vertexIndex >= starts[faces.Length]) return null;
- // Find the largest face start not exceeding the hit vertex index.
+ // 找最大的 k 使 starts[k] <= vertexIndex(face k 佔頂點 [starts[k], starts[k+1]))
int lo = 0, hi = faces.Length - 1, ans = 0;
while (lo <= hi)
{
@@ -763,7 +778,7 @@ public partial class MainViewModel : ObservableObject
double snapTol = diag * 0.02;
double markerR = Math.Clamp(diag * 0.004, 0.01, 5.0);
- // STL has no B-rep edges or faces for these modes.
+ // 無 B-rep 來源(STL)只支援部分模式
if (fi.BrepFace is null && CurrentMode is MeasureMode.Edge or MeasureMode.Circle or MeasureMode.Face)
{
SetStatus(() => Get("Runtime.MeshMeasurementLimited"));
@@ -885,7 +900,7 @@ public partial class MainViewModel : ObservableObject
ModelNodeViewModel? root = FindRootOf(fi.Owner);
if (root is null) { SetStatus(() => Get("Runtime.RootNotFound")); break; }
- if (_align3.Count < 3)
+ if (_align3.Count < 3) // 來源 3 點(要移動的檔案)
{
if (_align3.Count > 0 && !ReferenceEquals(_align3[0].Root, root))
{
@@ -905,7 +920,7 @@ public partial class MainViewModel : ObservableObject
? Format("Runtime.Align3SourcePoint", sourceCount, root.Name)
: Get("Runtime.Align3SourceComplete"));
}
- else
+ else // 目標 3 點(另一個檔案,順序對應來源點)
{
if (ReferenceEquals(_align3[0].Root, root))
{
@@ -1007,7 +1022,7 @@ public partial class MainViewModel : ObservableObject
_align3.Clear();
}
- // Rigid part transforms
+ // ─── 零件剛體變換(兩點對齊=平移、旋轉 90°、三點對齊=旋轉+平移)──
private void TranslateRoot(ModelNodeViewModel root, Vector3D v)
{
@@ -1018,17 +1033,19 @@ public partial class MainViewModel : ObservableObject
}
///
- /// Transforms B-rep objects, meshes, edges and bounds for one file.
- /// op and m must represent the same transform in CADability and WPF conventions.
+ /// 對整個檔案(root)套剛體變換:B-rep 用 ModOp 整體 Modify(量測維持精確)、
+ /// 渲染網格 / 合併網格 / 邊線端點同步重算、邊界重新計算。
+ /// op 與 m 必須是同一個變換(CADability 與 WPF 兩種表示)。
///
private void TransformRoot(ModelNodeViewModel root, CADability.ModOp op, Matrix3D m)
{
if (_viewport is null) return;
- ClearMeasurements();
+ ClearMeasurements(); // 量測標記位置已失效
var leaves = root.Leaves().ToList();
- // Transform whole B-rep objects sequentially to avoid shared-edge changes.
+ // B-rep:對 Solid/Shell 整體 Modify(避免共用邊被逐面重複變換)。
+ // CADability 物件非執行緒安全 → 維持循序
foreach (ModelNodeViewModel leaf in leaves)
foreach (CADability.GeoObject.IGeoObject g in leaf.SourceGeos)
{
@@ -1036,7 +1053,7 @@ public partial class MainViewModel : ObservableObject
catch { }
}
- // Transform independent frozen face meshes in parallel.
+ // 渲染網格:面級平行變換(來源/輸出皆 frozen、各面獨立 → 安全;大檔 64k 面吃滿多核)
var faceItems = new List<(GeometryModel3D Gm, FaceInfo Fi)>();
foreach (ModelNodeViewModel leaf in leaves)
if (leaf.FacesContent is not null)
@@ -1046,7 +1063,7 @@ public partial class MainViewModel : ObservableObject
var moved = new MeshGeometry3D[faceItems.Count];
Parallel.For(0, faceItems.Count, i => moved[i] = TransformMesh(faceItems[i].Fi.Mesh, m));
- for (int i = 0; i < faceItems.Count; i++)
+ for (int i = 0; i < faceItems.Count; i++) // 視覺樹賦值回 UI 執行緒循序做
{
faceItems[i].Fi.Mesh = moved[i];
faceItems[i].Gm.Geometry = moved[i];
@@ -1055,15 +1072,15 @@ public partial class MainViewModel : ObservableObject
foreach (ModelNodeViewModel leaf in leaves)
{
if (leaf.FacesContent is not null)
- RebuildMerged(leaf);
+ RebuildMerged(leaf); // 合併網格同步
- // Update edge data before rebuilding the merged visual.
+ // 邊線端點(資料);實際合併線於迴圈後重建
if (leaf.OriginalEdgePoints is not null)
leaf.OriginalEdgePoints = TransformPoints(leaf.OriginalEdgePoints, m);
}
RecomputeBounds(root);
- if (SectionEnabled) ApplySection();
+ if (SectionEnabled) ApplySection(); // 內含 RefreshRootEdges
else RefreshRootEdges(root);
}
@@ -1074,7 +1091,7 @@ public partial class MainViewModel : ObservableObject
var mesh = new MeshGeometry3D
{
Positions = positions,
- TriangleIndices = src.TriangleIndices,
+ TriangleIndices = src.TriangleIndices, // frozen,可共用(法向量缺省由 WPF 自算)
};
mesh.Freeze();
return mesh;
@@ -1088,7 +1105,7 @@ public partial class MainViewModel : ObservableObject
return pts;
}
- /// Recomputes bounds from transformed meshes and edges.
+ /// 旋轉後 AABB 不能只平移 → 由網格/邊線重新計算(bottom-up)
private Rect3D RecomputeBounds(ModelNodeViewModel node)
{
Rect3D b = Rect3D.Empty;
@@ -1110,7 +1127,7 @@ public partial class MainViewModel : ObservableObject
return b;
}
- // Rotate selected file
+ // ─── 旋轉 90°(樹面板選取的檔案,繞其中心)──────────────────
[RelayCommand(CanExecute = nameof(IsIdle))]
private void RotateRoot(string axisName)
@@ -1144,9 +1161,9 @@ public partial class MainViewModel : ObservableObject
private static bool ContainsNode(ModelNodeViewModel parent, ModelNodeViewModel target) =>
parent.Children.Any(c => ReferenceEquals(c, target) || ContainsNode(c, target));
- // Three-point alignment math is in RigidAlign.
+ // 三點對齊的剛體變換數學在 Services/RigidAlign.cs(SmokeTest --align-test 驗證)
- // Shared helpers
+ // ─── 共用 ────────────────────────────────────────────────────
private Rect3D UnionBounds()
{
diff --git a/src/STPViewer/ViewModels/ModelNodeViewModel.cs b/src/STPViewer/ViewModels/ModelNodeViewModel.cs
index 2489dd6..efce4d3 100644
--- a/src/STPViewer/ViewModels/ModelNodeViewModel.cs
+++ b/src/STPViewer/ViewModels/ModelNodeViewModel.cs
@@ -12,30 +12,30 @@ using STPViewer.Services;
namespace STPViewer.ViewModels;
///
-/// Assembly tree node: imported file, STEP group, or geometry leaf.
-/// Visibility, edges, and color cascade to children.
+/// 裝配樹節點:root = 匯入檔、group = STEP 裝配(Block)、leaf = 零件幾何。
+/// 可見性 / 邊線 / 顏色設定向下層疊(cascade)。
///
public partial class ModelNodeViewModel : ObservableObject
{
- /// Palette used when assigning and cycling leaf colors.
+ /// 調色盤(每個 leaf 依序取色,點色塊循環切換)
public static readonly Color[] Palette =
{
- Color.FromRgb(0x90, 0xA4, 0xAE),
- Color.FromRgb(0x4F, 0xC3, 0xF7),
- Color.FromRgb(0xFF, 0xB7, 0x4D),
- Color.FromRgb(0x81, 0xC7, 0x84),
- Color.FromRgb(0xBA, 0x68, 0xC8),
- Color.FromRgb(0xE5, 0x73, 0x73),
- Color.FromRgb(0xFF, 0xD5, 0x4F),
- Color.FromRgb(0x4D, 0xB6, 0xAC),
- Color.FromRgb(0xA1, 0x88, 0x7F),
- Color.FromRgb(0x79, 0x86, 0xCB),
+ Color.FromRgb(0x90, 0xA4, 0xAE), // 藍灰(金屬感)
+ Color.FromRgb(0x4F, 0xC3, 0xF7), // 淺藍
+ Color.FromRgb(0xFF, 0xB7, 0x4D), // 橙
+ Color.FromRgb(0x81, 0xC7, 0x84), // 綠
+ Color.FromRgb(0xBA, 0x68, 0xC8), // 紫
+ Color.FromRgb(0xE5, 0x73, 0x73), // 紅
+ Color.FromRgb(0xFF, 0xD5, 0x4F), // 黃
+ Color.FromRgb(0x4D, 0xB6, 0xAC), // 藍綠
+ Color.FromRgb(0xA1, 0x88, 0x7F), // 棕
+ Color.FromRgb(0x79, 0x86, 0xCB), // 靛藍
};
[ObservableProperty]
private bool isVisible = true;
- /// Outline visibility.
+ /// 輪廓邊線開關(邊線量過大的檔案匯入時自動關閉,避免轉動視角卡頓)
[ObservableProperty]
private bool showEdges = true;
@@ -55,7 +55,7 @@ public partial class ModelNodeViewModel : ObservableObject
public string? NameKey { get; init; }
public int NameNumber { get; init; }
- /// Set only on imported file roots.
+ /// root(檔案層級)才有值
public string? FilePath { get; init; }
public bool IsRoot => FilePath is not null;
@@ -66,7 +66,7 @@ public partial class ModelNodeViewModel : ObservableObject
public int FaceCount { get; init; }
public int TriangleCount { get; init; }
- /// World-space bounds updated after part transforms.
+ /// 世界座標邊界(零件平移/旋轉後由 RecomputeBounds 更新;連動 Stats/Tooltip 的外形尺寸)
public Rect3D Bounds
{
get => _bounds;
@@ -80,15 +80,15 @@ public partial class ModelNodeViewModel : ObservableObject
}
private Rect3D _bounds = Rect3D.Empty;
- /// Axis-aligned size in millimeters.
+ /// 外形尺寸 L×W×H(AABB,mm)
public string SizeText => Bounds.IsEmpty
? ""
: $"{Bounds.SizeX:F1} × {Bounds.SizeY:F1} × {Bounds.SizeZ:F1} mm";
- /// Source B-rep objects transformed as whole parts.
+ /// 來源 B-rep 物件(零件平移時整體 Modify,避免共用邊被重複位移)
public List SourceGeos { get; } = new();
- /// False for STL or curve-only sources without B-rep geometry.
+ /// false = STL/曲線等無 B-rep 來源(邊/面/圓量測不可用)
public bool HasBrep { get; init; } = true;
public string Stats =>
@@ -106,13 +106,14 @@ public partial class ModelNodeViewModel : ObservableObject
foreach (ModelNodeViewModel child in Children) child.RefreshLocalizedText();
}
- // Tree filtering does not change viewport visibility.
+ // ── 樹搜尋/過濾(只影響樹面板顯示,不影響 3D 可見性)──
[ObservableProperty]
private bool isFilterVisible = true;
///
- /// Shows matching nodes, their ancestors and descendants; expands matching branches.
+ /// 套用名稱過濾:符合者 + 其祖先 + 其子樹 顯示。空字串 = 全顯。
+ /// 過濾中有符合的子孫時自動展開。回傳此節點是否顯示。
///
public bool ApplyFilter(string? filter, bool ancestorMatched = false)
{
@@ -126,17 +127,17 @@ public partial class ModelNodeViewModel : ObservableObject
return IsFilterVisible;
}
- // Viewport objects
- public ModelVisual3D? BodyVisual { get; set; }
- public Model3DGroup? FacesContent { get; set; }
- public Model3DGroup? MergedContent { get; set; }
- public LinesVisual3D? EdgeVisual { get; set; }
+ // ── 視覺物件 ──
+ public ModelVisual3D? BodyVisual { get; set; } // leaf:面網格容器(內容在兩種模式間切換)
+ public Model3DGroup? FacesContent { get; set; } // leaf:逐面 GeometryModel3D(剖面用;量測打合併網格)
+ public Model3DGroup? MergedContent { get; set; } // leaf:整零件合併成 1 個 GeometryModel3D(瀏覽+量測,draw call 大減)
+ public LinesVisual3D? EdgeVisual { get; set; } // root:整檔合併邊線(一檔一條,避免轉動卡頓)
public DiffuseMaterial? SharedMaterial { get; set; }
- /// Unclipped edge endpoints used for section restoration.
+ /// 原始(未剖切)邊線端點 — 剖面還原用
public Point3DCollection? OriginalEdgePoints { get; set; }
- /// Signals the ViewModel to update leaf viewport objects.
+ /// leaf 的可見性/邊線狀態變更(MainViewModel 同步 viewport)
public event Action? VisualStateChanged;
private int _paletteIndex;
@@ -147,7 +148,7 @@ public partial class ModelNodeViewModel : ObservableObject
color = Palette[_paletteIndex];
}
- /// All descendant leaves, including this node when it is a leaf.
+ /// 所有 leaf 後代(含自身若為 leaf)
public IEnumerable Leaves()
{
if (IsLeafNode) { yield return this; yield break; }
diff --git a/src/STPViewer/ViewModels/QuickBarViewModel.cs b/src/STPViewer/ViewModels/QuickBarViewModel.cs
index f40888a..c465358 100644
--- a/src/STPViewer/ViewModels/QuickBarViewModel.cs
+++ b/src/STPViewer/ViewModels/QuickBarViewModel.cs
@@ -7,7 +7,7 @@ using STPViewer.Services;
namespace STPViewer.ViewModels;
-/// Visibility toggle for one quick bar button.
+/// 快速列單一按鈕的顯示開關(選單「快速列 → 自訂按鈕」的 checkbox 項)
public partial class QuickBarItemViewModel : ObservableObject
{
public string Key { get; }
@@ -30,11 +30,13 @@ public partial class QuickBarItemViewModel : ObservableObject
}
///
-/// Quick bar button registry and saved visibility selections.
+/// 快速工具列自訂:所有功能都在選單列,快速列只放使用者勾選的常用按鈕。
+/// XAML 以索引子綁定:Visibility="{Binding QuickBar[Point].IsChecked, Converter=…}"。
+/// 勾選狀態隨 settings.json(QuickBarKeys)保存;null = 用預設。
///
public partial class QuickBarViewModel : ObservableObject
{
- /// Button order, label key, and default visibility.
+ /// 註冊表 = 快速列順序 + 選單顯示名 + 是否預設顯示(一般 user 常用)
private static readonly (string Key, string Label, bool Default)[] Registry =
{
("Import", "QuickBar.Import", true),
@@ -72,7 +74,7 @@ public partial class QuickBarViewModel : ObservableObject
private readonly Dictionary _byKey;
- /// All buttons in quick bar order.
+ /// 依快速列順序的全部項目(選單自訂清單用)
public IReadOnlyList Items { get; }
public QuickBarViewModel()
@@ -81,10 +83,10 @@ public partial class QuickBarViewModel : ObservableObject
_byKey = Items.ToDictionary(i => i.Key);
}
- /// Indexer used by XAML visibility bindings.
+ /// XAML 索引子綁定入口:QuickBar[Point].IsChecked
public QuickBarItemViewModel this[string key] => _byKey[key];
- /// Loads saved visibility keys; null retains defaults.
+ /// 套用保存的勾選清單;null = 維持預設(含未知 key 容錯:新版本新增按鈕用預設值)
public void Load(List? visibleKeys)
{
if (visibleKeys is null) return;
@@ -93,7 +95,7 @@ public partial class QuickBarViewModel : ObservableObject
item.IsChecked = set.Contains(item.Key);
}
- /// Selected keys saved in settings.json.
+ /// 目前勾選清單(存 settings.json)
public List ToKeys() =>
Items.Where(i => i.IsChecked).Select(i => i.Key).ToList();
diff --git a/src/STPViewer/ViewModels/StlExportViewModel.cs b/src/STPViewer/ViewModels/StlExportViewModel.cs
index 0980533..64584fe 100644
--- a/src/STPViewer/ViewModels/StlExportViewModel.cs
+++ b/src/STPViewer/ViewModels/StlExportViewModel.cs
@@ -5,17 +5,18 @@ using STPViewer.Services;
namespace STPViewer.ViewModels;
///
-/// Options and live size estimate for the STL export dialog.
+/// STL 匯出參數對話框的 ViewModel:範圍 / 格式 / 單位縮放 / 網格精度 + 即時摘要(三角形數、預估大小)。
+/// 對話框按「匯出」後由 MainViewModel.Export 讀取選項執行。
///
public partial class StlExportViewModel : ObservableObject
{
- /// Number of visible files available for export.
+ /// 可見(可匯出)檔案數
public int FileCount { get; }
- /// Triangle count of the current display meshes.
+ /// 目前顯示網格的三角形總數(精細/較粗會重新三角化,實際數量以結果為準)
public long TriangleCount { get; }
- /// Whether B-rep geometry is available for retessellation.
+ /// 是否有 B-rep 來源(全 STL/DXF 時精度選項無意義 → 停用)
public bool HasBrep { get; }
public StlExportViewModel(int fileCount, long triangleCount, bool hasBrep)
@@ -25,17 +26,17 @@ public partial class StlExportViewModel : ObservableObject
HasBrep = hasBrep;
}
- /// 0 = merged STL; 1 = one STL per visible file.
+ /// 0 = 全部可見合併成單一 STL;1 = 每個可見檔案各一個 STL
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Summary))]
private int scopeIndex;
- /// 0 = binary; 1 = ASCII.
+ /// 0 = Binary(建議,檔案小);1 = ASCII(文字可讀,檔案大)
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Summary))]
private int formatIndex;
- /// 0 = millimeters; 1 = inches; 2 = custom scale.
+ /// 0 = mm(1:1);1 = inch(÷25.4);2 = 自訂縮放
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Summary))]
[NotifyPropertyChangedFor(nameof(IsCustomScale))]
@@ -47,7 +48,7 @@ public partial class StlExportViewModel : ObservableObject
[NotifyPropertyChangedFor(nameof(CanConfirm))]
private string customScaleText = "1.0";
- /// 0 = current mesh; 1 = finer B-rep tessellation.
+ /// 0 = 目前顯示網格;1 = 精細(B-rep 重新三角化)
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Summary))]
private int qualityIndex;
@@ -58,7 +59,7 @@ public partial class StlExportViewModel : ObservableObject
public bool PerFile => ScopeIndex == 1;
public StlMeshQuality Quality => (StlMeshQuality)QualityIndex;
- /// Coordinate scale, or null for an invalid custom value.
+ /// 座標縮放;自訂值無效時回 null(擋確認)
public double? Scale => UnitIndex switch
{
0 => 1.0,
diff --git a/tools/SmokeTest/AlignTest.cs b/tools/SmokeTest/AlignTest.cs
index 53d440d..bed931a 100644
--- a/tools/SmokeTest/AlignTest.cs
+++ b/tools/SmokeTest/AlignTest.cs
@@ -1,4 +1,5 @@
-// Three-point alignment checks: known transform, Matrix3D/ModOp agreement, and collinear rejection.
+// 三點對齊剛體變換數學驗證:
+// (1) 已知變換還原 (2) Matrix3D ↔ ModOp 兩種表示一致 (3) 共線拒絕
using System.Windows.Media.Media3D;
using STPViewer.Services;
@@ -10,29 +11,29 @@ internal static class AlignTest
{
int failures = 0;
- // Recover a known 90-degree Z rotation followed by translation (5,-3,2).
+ // ── (1) 已知剛體變換:繞 Z 軸 90° + 平移 (5,-3,2),三點求解後應還原同一變換 ──
var truth = Matrix3D.Identity;
truth.RotateAt(new Quaternion(new Vector3D(0, 0, 1), 90), new Point3D(2, 1, 0));
truth.Translate(new Vector3D(5, -3, 2));
var p1 = new Point3D(0, 0, 0);
var p2 = new Point3D(10, 0, 0);
- var p3 = new Point3D(0, 7, 3); // Not collinear with the first two points.
+ var p3 = new Point3D(0, 7, 3); // 不共線、不在同平面軸上
Point3D q1 = truth.Transform(p1), q2 = truth.Transform(p2), q3 = truth.Transform(p3);
bool ok = RigidAlign.TryRigidTransform(p1, p2, p3, q1, q2, q3, out Matrix3D m);
failures += Check("three-point transform solved", ok, true);
- // Corresponding points must coincide.
+ // 三個對應點要精確貼合
failures += CheckPoint("p1→q1", m.Transform(p1), q1);
failures += CheckPoint("p2→q2", m.Transform(p2), q2);
failures += CheckPoint("p3→q3", m.Transform(p3), q3);
- // An independent fourth point checks the full rigid transform.
+ // 剛體變換唯一性:任意第 4 點也應該跟著正確變換
var r = new Point3D(-4, 12, 8);
failures += CheckPoint("fourth point", m.Transform(r), truth.Transform(r));
- // CADability ModOp and WPF Matrix3D must transform points identically.
+ // ── (2) ToModOp:CADability ModOp 與 WPF Matrix3D 對同一點要算出同樣結果 ──
CADability.ModOp op = RigidAlign.ToModOp(m);
foreach (Point3D p in new[] { p1, p2, p3, r, new Point3D(1.5, -2.25, 9) })
{
@@ -47,7 +48,7 @@ internal static class AlignTest
}
System.Console.WriteLine($" {(failures == 0 ? "PASS" : "----")}: ModOp/Matrix3D agree at five points");
- // Collinear source points must be rejected.
+ // ── (3) 共線拒絕 ──
bool collinearRejected = !RigidAlign.TryRigidTransform(
new Point3D(0, 0, 0), new Point3D(1, 0, 0), new Point3D(2, 0, 0), // Collinear.
q1, q2, q3, out _);
diff --git a/tools/SmokeTest/ClipTest.cs b/tools/SmokeTest/ClipTest.cs
index dcd1236..7b9e7a6 100644
--- a/tools/SmokeTest/ClipTest.cs
+++ b/tools/SmokeTest/ClipTest.cs
@@ -1,4 +1,4 @@
-// Section clipping checks the retained side and triangle area.
+// 剖面裁切數學驗證:對單位幾何裁切,檢查保留側與面積守恆
using System.Windows.Media;
using System.Windows.Media.Media3D;
using STPViewer.Services;
@@ -11,7 +11,7 @@ internal static class ClipTest
{
int failures = 0;
- // A single triangle with area 50.
+ // 單一三角形 (0,0,0)(10,0,0)(0,10,0),面積 50
var mesh = new MeshGeometry3D
{
Positions = new Point3DCollection
@@ -22,25 +22,25 @@ internal static class ClipTest
};
mesh.Freeze();
- // A plane at x=20 retains the full triangle.
+ // case 1: 平面 x=20,全保留
var m1 = SectionService.ClipMesh(mesh, new Point3D(20, 0, 0), new Vector3D(1, 0, 0));
failures += Check("fully retained", Area(m1), 50.0);
- // A plane at x=-1 removes the full triangle.
+ // case 2: 平面 x=-1,全裁掉
var m2 = SectionService.ClipMesh(mesh, new Point3D(-1, 0, 0), new Vector3D(1, 0, 0));
failures += Check("fully removed", Area(m2), 0.0);
- // At x=5, the retained trapezoid has area 50 - 12.5 = 37.5.
+ // case 3: 平面 x=5 → 保留 x≤5:梯形面積 = 50 - 右側小三角形(底5高5)/2 = 50 - 12.5 = 37.5
var m3 = SectionService.ClipMesh(mesh, new Point3D(5, 0, 0), new Vector3D(1, 0, 0));
failures += Check("half clipped (x<=5)", Area(m3), 37.5);
foreach (Point3D p in m3.Positions)
if (p.X > 5 + 1e-9) { Console.WriteLine($" FAIL: vertex x={p.X} is outside retained side"); failures++; }
- // Reverse the plane normal to retain the smaller triangle.
+ // case 4: 反向(保留 x≥5)→ 12.5
var m4 = SectionService.ClipMesh(mesh, new Point3D(5, 0, 0), new Vector3D(-1, 0, 0));
failures += Check("half clipped (x>=5)", Area(m4), 12.5);
- // Clip a line segment.
+ // case 5: 線段裁切
var segs = new Point3DCollection { new(0, 0, 0), new(10, 0, 0) };
Point3DCollection s1 = SectionService.ClipSegments(segs, new Point3D(4, 0, 0), new Vector3D(1, 0, 0));
double segLen = s1.Count == 2 ? (s1[1] - s1[0]).Length : -1;
diff --git a/tools/SmokeTest/ExportTest.cs b/tools/SmokeTest/ExportTest.cs
index 29ac70a..5c76d1a 100644
--- a/tools/SmokeTest/ExportTest.cs
+++ b/tools/SmokeTest/ExportTest.cs
@@ -5,13 +5,13 @@ using System.Linq;
using CADability;
using CADability.GeoObject;
using STPViewer.Services;
-using IOPath = System.IO.Path; // Avoid the CADability.GeoObject.Path name collision.
+using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
namespace SmokeTest;
///
-/// Headless STEP export round trip: import, export through the same CADability path,
-/// then import the result and compare solid counts.
+/// STEP 匯出往返驗證(v0.5.0 匯出 STEP 功能的無 UI 驗證):
+/// 匯入來源 STEP → 走與 MainViewModel.ExportStepFile 相同路徑寫出新檔 → 重新匯入比對實體數。
///
public static class ExportTest
{
diff --git a/tools/SmokeTest/InterferenceTest.cs b/tools/SmokeTest/InterferenceTest.cs
index 448de73..3766f84 100644
--- a/tools/SmokeTest/InterferenceTest.cs
+++ b/tools/SmokeTest/InterferenceTest.cs
@@ -1,4 +1,4 @@
-// Interference checks for intersecting, separated, and touching meshes.
+// 干涉檢查數學驗證:相交 / 分離 / 貼合三種情境
using System.Windows.Media;
using System.Windows.Media.Media3D;
using STPViewer.Services;
@@ -11,14 +11,14 @@ internal static class InterferenceTest
{
int failures = 0;
- // Two tetrahedra overlap when B is offset by 5 along X.
+ // 兩個互穿的四面體(B 比 A 平移 +5,邊長 10 → 必相交)
MeshGeometry3D a = Tetra(new Vector3D(0, 0, 0));
MeshGeometry3D b = Tetra(new Vector3D(5, 0, 0));
InterferenceResult r1 = InterferenceService.Check(new[] { a }, new[] { b });
failures += Check("overlap intersects", r1.Intersects, true);
failures += Check("overlap has intersection segments", r1.Segments.Count > 0, true);
- // B at X=30 leaves a gap of about 20.
+ // 分離(B 平移 +30,gap 應 ≈ 30 - 10 = 20)
MeshGeometry3D c = Tetra(new Vector3D(30, 0, 0));
InterferenceResult r2 = InterferenceService.Check(new[] { a }, new[] { c });
failures += Check("separated meshes do not intersect", r2.Intersects, false);
@@ -26,7 +26,7 @@ internal static class InterferenceTest
Console.WriteLine($" {(gapOk ? "PASS" : "FAIL")}: separated gap = {r2.GapDistance:F4} (expected ~20)");
if (!gapOk) failures++;
- // B at X=10 touches a vertex without penetrating A.
+ // 貼合(B 平移 +10,頂點剛好接觸 → 不算穿透、gap ≈ 0)
MeshGeometry3D d = Tetra(new Vector3D(10, 0, 0));
InterferenceResult r3 = InterferenceService.Check(new[] { a }, new[] { d });
failures += Check("touching is not penetration", r3.Intersects, false);
@@ -45,7 +45,7 @@ internal static class InterferenceTest
return ok ? 0 : 1;
}
- /// A tetrahedron with side length 10, translated by offset.
+ /// 邊長 10 的四面體,offset 平移
private static MeshGeometry3D Tetra(Vector3D offset)
{
Point3D P(double x, double y, double z) => new Point3D(x, y, z) + offset;
diff --git a/tools/SmokeTest/MakeDxf.cs b/tools/SmokeTest/MakeDxf.cs
index 496222f..8acde72 100644
--- a/tools/SmokeTest/MakeDxf.cs
+++ b/tools/SmokeTest/MakeDxf.cs
@@ -1,4 +1,4 @@
-// Generate a small standard DXF test file with the bundled netDxf library.
+// 測試輔助:用 CADability 內嵌的 netDxf 產生標準 DXF 測試檔
using netDxf;
using netDxf.Entities;
diff --git a/tools/SmokeTest/MakeStl.cs b/tools/SmokeTest/MakeStl.cs
index 6601516..5f57dd4 100644
--- a/tools/SmokeTest/MakeStl.cs
+++ b/tools/SmokeTest/MakeStl.cs
@@ -2,12 +2,12 @@ using System.IO;
namespace SmokeTest;
-/// Generate a binary STL grid with two triangles per cell and noncoplanar vertices.
+/// 產生測試用 binary STL:一片 gridN×gridN 的網格,每格 2 個三角形(波浪起伏,避免全共面)。
internal static class MakeStl
{
public static void Run(string path, int triCount)
{
- // Two triangles per cell; use a square grid large enough for the requested count.
+ // 每格 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;
diff --git a/tools/SmokeTest/Program.cs b/tools/SmokeTest/Program.cs
index c91283d..0b75c24 100644
--- a/tools/SmokeTest/Program.cs
+++ b/tools/SmokeTest/Program.cs
@@ -1,4 +1,4 @@
-// Headless smoke tests for CAD import, triangulation, and the assembly tree.
+// 無 UI smoke test:驗證 CAD 檔解析 → 三角化 → 裝配樹 整條匯入管線
using System.Diagnostics;
using System.IO;
using STPViewer.Services;
diff --git a/tools/SmokeTest/StlExportTest.cs b/tools/SmokeTest/StlExportTest.cs
index 905200b..2c04be6 100644
--- a/tools/SmokeTest/StlExportTest.cs
+++ b/tools/SmokeTest/StlExportTest.cs
@@ -5,13 +5,15 @@ using System.Text;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using STPViewer.Services;
-using IOPath = System.IO.Path; // Avoid the CADability.GeoObject.Path name collision.
+using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
namespace SmokeTest;
///
-/// Headless STL export checks:
-/// binary round trip, ASCII facet structure, and degenerate triangle filtering.
+/// STL 匯出驗證(無 UI):
+/// 1) binary — 已知立方體網格寫出 → 解析 header/三角形數/座標範圍比對(含縮放)
+/// 2) ASCII — facet 數與 solid/endsolid 結構
+/// 3) 退化三角形(零面積)自動濾除
///
public static class StlExportTest
{
@@ -22,9 +24,9 @@ public static class StlExportTest
string dir = IOPath.Combine(IOPath.GetTempPath(), "stpviewer_stl_test");
Directory.CreateDirectory(dir);
- MeshGeometry3D cube = BuildCube(10); // 10x10x10 mm, 12 triangles.
+ MeshGeometry3D cube = BuildCube(10); // 10×10×10 mm、12 三角形
- // Binary triangle count and scaled coordinate range.
+ // 1) binary:三角形數 + 縮放後座標範圍
{
string f = IOPath.Combine(dir, "cube_bin.stl");
double scale = 1.0 / 25.4; // mm to inches.
@@ -35,7 +37,7 @@ public static class StlExportTest
Report(ref failed, ok, $"binary round trip: 12 triangles, inch range [{min:F4}, {max:F4}]");
}
- // ASCII facet count and structure.
+ // 2) ASCII:facet 數 + 結構
{
string f = IOPath.Combine(dir, "cube_ascii.stl");
int written = StlExportService.Write(f, new[] { cube }, ascii: true, scale: 1.0);
@@ -47,12 +49,12 @@ public static class StlExportTest
Report(ref failed, ok, $"ASCII structure valid, facets = {facets}");
}
- // A zero-area triangle must be filtered from the export.
+ // 3) 退化三角形濾除:加一個零面積三角形 → 仍是 12
{
var degenerate = new MeshGeometry3D();
degenerate.Positions.Add(new Point3D(0, 0, 0));
degenerate.Positions.Add(new Point3D(1, 1, 1));
- degenerate.Positions.Add(new Point3D(2, 2, 2)); // Collinear: zero area.
+ degenerate.Positions.Add(new Point3D(2, 2, 2)); // 共線 → 面積 0
degenerate.TriangleIndices.Add(0);
degenerate.TriangleIndices.Add(1);
degenerate.TriangleIndices.Add(2);
@@ -65,7 +67,8 @@ public static class StlExportTest
Report(ref failed, ok, $"degenerate triangle filtered ({written} written)");
}
- // Optional real STEP input: retessellate B-rep geometry at fine precision.
+ // 4)(可選)真實 STEP:B-rep 精細重新三角化 → 寫 binary STL 驗證
+ //(不測「較粗」:CADability GetTriangulation 對比快取粗的精度回傳既有快取,粗化無效 → 功能已移除)
if (stepPath is not null)
{
var data = new StepImportService().Import(stepPath);
@@ -80,7 +83,7 @@ public static class StlExportTest
(uint count, _, _) = ReadBinary(f);
Report(ref failed, meshes.Count > 0 && written > 0 && count == written,
$"fine retessellation: {meshes.Count} faces, {written:N0} triangles");
- // CADability's triangle count is not monotonic in precision; 0.15x is measurably finer.
+ // CADability 三角形數對精度非單調(0.4× 反而比 1× 少),Fine=0.15× 實測明顯更細
Report(ref failed, written > importTris,
$"fine ({written:N0}) > imported default ({importTris:N0})");
}
@@ -103,7 +106,7 @@ public static class StlExportTest
if (!ok) failed++;
}
- /// Axis-aligned cube from 0 to size, with 12 triangles.
+ /// 0..size 的軸對齊立方體,12 個三角形(法向不講究,STL 寫出時自算)
private static MeshGeometry3D BuildCube(double s)
{
var m = new MeshGeometry3D();
diff --git a/tools/SmokeTest/TreeDump.cs b/tools/SmokeTest/TreeDump.cs
index c27bde7..af7ae9a 100644
--- a/tools/SmokeTest/TreeDump.cs
+++ b/tools/SmokeTest/TreeDump.cs
@@ -1,4 +1,4 @@
-// Print the STEP object hierarchy after import with HierarchyToBlocks enabled.
+// 實驗工具:印出 STEP 匯入後的物件階層(HierarchyToBlocks=true)
using CADability;
using CADability.GeoObject;