feat: v0.5.0 feature round - measurement, view, tree, section, export upgrades

Measurement / interaction:
- Circle-center snap in Snap(): click near a circular edge to snap to its
  center (hole-to-hole pitch measuring); nearest of vertex vs circle wins
- Esc cancels pending multi-step measurement, then exits mode, then gizmo;
  hotkeys P/D/E/F/C/A/M toggle measure modes (ignored while typing)
- Volume/centroid via mesh signed volume (open-shell detection rejects
  unreliable results); tree context menu adds it as a measurement

View / UI:
- Standard views (Iso/Front/Top/Right, Z-up) + orthographic toggle;
  gizmo overlay camera now switches type to match main camera
- GridSplitters for tree/measurement panels; node tooltip shows AABB LxWxH
- Tree name filter (matches + ancestors + subtree, auto-expand);
  context menu: isolate node / invert visibility / show all
- Settings persistence (window bounds, unit, 10-file MRU) in
  %LOCALAPPDATA%/STPViewer/settings.json; recent-files dropdown

Files / section:
- Interference accepts >=2 visible files, checks all pairs
- Export STEP: visible Solids/Shells at current aligned positions via
  CADability ExportStep; SmokeTest --export-test round-trip verifies
- Custom section plane by picking 3 points (axis combo 3-pt); plane
  position generalized to AABB-corner projection; numeric position input
- Bump version to 0.5.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 08:34:20 +08:00
co-authored by Claude Fable 5
parent e355cb4363
commit c657417d6b
15 changed files with 890 additions and 118 deletions
+53
View File
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CADability;
using CADability.GeoObject;
using STPViewer.Services;
using IOPath = System.IO.Path; // CADability.GeoObject.Path(曲線)撞名
namespace SmokeTest;
/// <summary>
/// STEP 匯出往返驗證(v0.5.0 匯出 STEP 功能的無 UI 驗證):
/// 匯入來源 STEP → 走與 MainViewModel.ExportStepFile 相同路徑寫出新檔 → 重新匯入比對實體數。
/// </summary>
public static class ExportTest
{
public static int Run(string sourcePath, string outPath)
{
Console.WriteLine($"=== ExportTest: {IOPath.GetFileName(sourcePath)} → {IOPath.GetFileName(outPath)} ===");
ImportedFileData data = new StepImportService().Import(sourcePath);
var geos = Collect(data.Root).Where(g => g is Solid or Shell).ToList();
Console.WriteLine($" 來源 Solid/Shell 物件 = {geos.Count}Solids = {data.Root.SolidCount}");
Project project = Project.CreateSimpleProject();
Model model = project.GetActiveModel();
foreach (IGeoObject g in geos) model.Add(g);
new ExportStep().WriteToFile(outPath, project);
var fi = new FileInfo(outPath);
Console.WriteLine($" 已寫出({fi.Length:N0} bytes");
if (fi.Length < 100)
{
Console.WriteLine("ExportTest: FAIL(輸出檔近乎空白)");
return 1;
}
ImportedFileData back = new StepImportService().Import(outPath);
Console.WriteLine($" 回讀 Solids = {back.Root.SolidCount}, Faces = {back.Root.FaceCount}");
bool ok = back.Root.SolidCount == data.Root.SolidCount && back.Root.FaceCount > 0;
Console.WriteLine(ok
? "ExportTest: PASS(實體數一致)"
: $"ExportTest: FAIL(來源 Solids {data.Root.SolidCount} ↔ 回讀 {back.Root.SolidCount}");
return ok ? 0 : 1;
}
private static IEnumerable<IGeoObject> Collect(ImportedNode n)
{
foreach (IGeoObject g in n.SourceGeos) yield return g;
foreach (ImportedNode c in n.Children)
foreach (IGeoObject g in Collect(c))
yield return g;
}
}
+3
View File
@@ -35,6 +35,9 @@ if (args[0] == "--align-test")
if (args[0] == "--bbox")
return SmokeTest.BBoxDump.Run(args[1]);
if (args[0] == "--export-test")
return SmokeTest.ExportTest.Run(args[1], args[2]) == 0 ? 0 : 2;
var service = new StepImportService { Progress = s => Console.WriteLine($" [階段] {s}") };
int failed = 0;