feat(perf): direct STL parsing for near-instant large-file load (v0.7.1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgHf5MW39U3Xo8y2uA229K
This commit is contained in:
@@ -119,13 +119,21 @@ dotnet publish src/STPViewer -c Release -o publish/STPViewer
|
||||
`_edgesSuspended` 為真時 `RefreshRootEdges` 不把線掛回。轉動/縮放/平移時不付邊線重建成本。
|
||||
**只訂 `Camera.Changed` 不夠**:`Attach` 在建構式呼叫,相機實例若被 Helix 換掉訂閱會孤兒化 → 暫停永不觸發;故加訂控制項層級事件保底(重複觸發 `OnCameraMoved` 無害,有 guard)
|
||||
- CADability `ImportStep` 對少數 AP242 檔案支援不完整;匯入失敗要 catch 顯示訊息,不可閃退
|
||||
- ⚡ **STL 直接解析(`StlMeshReader`,v0.7.1),不經過 CADability `ImportSTL`**。CADability 會為 STL 每個三角形
|
||||
建一個 B-rep Face(含 Edge/Vertex 拓樸)→ 百萬三角形要數十分鐘且吃光記憶體(實測 100k 三角形:CADability ~41 s
|
||||
vs 直接解析 ~11 ms;75 MB/150 萬三角形整條匯入管線 ~0.77 s)。binary/ASCII 皆自解;binary 判定用
|
||||
**檔案大小 == 84 + 50×count**(header 常也以 "solid" 開頭,不能只看字串)。頂點不焊接(STL 原生就無共用頂點)。
|
||||
**後果**:STL leaf **沒有 `SourceGeos`(CADability B-rep)** → (1) 不進 STEP 匯出(STL 本就非 B-rep 幾何,符合原設計意圖;
|
||||
v0.7.0 以前會把 STL shell 當 Shell 混進 STEP,現移除);(2) 剛體變換照常 — `TransformRoot` 的 SourceGeos 迴圈空轉、
|
||||
網格層照樣變換;(3) 量測走網格近似不受影響。**不要為了「STL 也要進 STEP」而改回 CADability ImportSTL**(會退回龜速)
|
||||
- IGES 無 reader;STL 無 B-rep(FaceInfo.BrepFace == null 的分支要保留)
|
||||
- 不寫入原始檔(唯讀工具);WPF 限 Windows,不要嘗試移植 vbox/Linux
|
||||
- 干涉檢查需剛好 2 個可見檔案(樹面板勾選);共面貼合(無穿透)不算干涉、gap≈0 視為配合(match)
|
||||
- SmokeTest 工具:`--tree`(裝配樹)、`--clip-test`(剖切數學)、`--interference-test`(干涉相交/分離/貼合)、
|
||||
`--align-test`(三點對齊剛體變換 + ModOp↔Matrix3D 一致性)、`--make-dxf`(產測試檔)、
|
||||
`--export-test <in.stp> <out.stp>`(STEP 匯出往返:寫出→回讀比對實體數)、
|
||||
`--stl-export-test [file.stp]`(STL binary/ASCII 往返 + 退化濾除;給 stp 加測 B-rep 精細重算)
|
||||
`--stl-export-test [file.stp]`(STL binary/ASCII 往返 + 退化濾除;給 stp 加測 B-rep 精細重算)、
|
||||
`--make-stl <out.stl> [triCount]`(產測試用 binary STL,預設 150 萬三角形,量 STL 匯入效能用)
|
||||
- **絕不要用 PowerShell regex/Set-Content 改 .cs 檔** — Windows PowerShell 5.1 預設編碼會把 UTF-8 中文弄成亂碼(已踩過,靠反編譯 DLL 救回)。文字取代一律用 Edit 工具
|
||||
|
||||
## For_AI/
|
||||
|
||||
@@ -28,6 +28,12 @@ public partial class AboutDialog : Window
|
||||
// ── 版本紀錄(新版加最上面;口吻:一般使用者看得懂,講「你會感覺到什麼」而非實作細節)──
|
||||
private static readonly ChangelogEntry[] Changelog =
|
||||
{
|
||||
new("0.7.1", new DateOnly(2026, 7, 12), "大型 STL 秒開",
|
||||
new[]
|
||||
{
|
||||
"• 開啟大型 STL 網格檔(數十 MB、上百萬三角形)現在幾乎瞬間完成,不用再等好幾分鐘。",
|
||||
"• 例如一個 75 MB、150 萬個三角形的 STL,以前要等到天荒地老,現在不到 1 秒就顯示出來。",
|
||||
}),
|
||||
new("0.7.0", new DateOnly(2026, 7, 4), "介面大整理 — 選單列+可自訂快速列",
|
||||
new[]
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Version>0.7.0</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
||||
<Version>0.7.1</Version> <!-- 單一版號來源:標題與 publish-winapp 打包資料夾都讀這個 -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -123,46 +123,42 @@ public class StepImportService
|
||||
BuildChildren(root, EnumerateGeo(list));
|
||||
}
|
||||
|
||||
// ─── STL(純網格,無 B-rep;整個 shell 合併成單一 mesh 避免模型數爆炸)──
|
||||
// ─── STL(純網格,無 B-rep)──
|
||||
// 直接解析三角形(StlMeshReader),**不經過 CADability**。CADability 的 ImportSTL 會為每個
|
||||
// 三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),百萬三角形需數十分鐘且吃光記憶體
|
||||
// (實測 100k 三角形:CADability ~41 s,直接解析 ~11 ms)。STL 本就無 B-rep,讀三角形即可。
|
||||
// ⚠️ 因此 STL leaf 沒有 SourceGeos(CADability B-rep)→ 不進 STEP 匯出(本就非 STEP B-rep 幾何);
|
||||
// 剛體變換仍走網格層(TransformRoot 的 SourceGeos 迴圈空轉、mesh 照樣變換),量測走網格近似不受影響。
|
||||
|
||||
private void ImportStlFile(string path, ImportedNode root)
|
||||
{
|
||||
Shell[] shells = new ImportSTL().Read(path)
|
||||
?? throw new InvalidDataException("無法解析 STL 檔");
|
||||
int i = 1;
|
||||
foreach (Shell sh in shells)
|
||||
{
|
||||
var positions = new Point3DCollection();
|
||||
var indices = new Int32Collection();
|
||||
double precision = PrecisionFor(SafeBounds(() => sh.GetBoundingCube()));
|
||||
foreach (Face f in sh.Faces)
|
||||
{
|
||||
GeoPoint[] pts; int[] ind;
|
||||
try { f.GetTriangulation(precision, out pts, out _, out ind, out _); }
|
||||
catch { continue; }
|
||||
if (pts is null || ind is null) continue;
|
||||
int offset = positions.Count;
|
||||
foreach (GeoPoint p in pts) positions.Add(new Point3D(p.x, p.y, p.z));
|
||||
foreach (int k in ind) indices.Add(offset + k);
|
||||
}
|
||||
if (positions.Count == 0) continue;
|
||||
Progress?.Invoke("解析 STL(直接讀三角形)…");
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
List<StlMeshReader.MeshGroup> groups = StlMeshReader.Read(path);
|
||||
if (groups.Count == 0)
|
||||
throw new InvalidDataException("STL 檔內沒有三角形");
|
||||
|
||||
var mesh = new MeshGeometry3D { Positions = positions, TriangleIndices = indices };
|
||||
foreach (StlMeshReader.MeshGroup g in groups)
|
||||
{
|
||||
if (g.Positions.Count == 0) continue;
|
||||
g.Positions.Freeze();
|
||||
g.Indices.Freeze();
|
||||
var mesh = new MeshGeometry3D { Positions = g.Positions, TriangleIndices = g.Indices };
|
||||
mesh.Freeze();
|
||||
int tris = g.Indices.Count / 3;
|
||||
var leaf = new ImportedNode
|
||||
{
|
||||
Name = shells.Length == 1 ? "網格" : $"網格 {i}",
|
||||
Name = g.Name,
|
||||
HasBrep = false,
|
||||
SolidCount = 1,
|
||||
FaceCount = sh.Faces.Length,
|
||||
TriangleCount = indices.Count / 3,
|
||||
FaceCount = tris, // STL 無 B-rep 面 → 以三角形數代表
|
||||
TriangleCount = tris,
|
||||
Bounds = BoundsOfMesh(mesh),
|
||||
};
|
||||
leaf.SourceGeos.Add(sh);
|
||||
leaf.Faces.Add(new ImportedFace(null, mesh));
|
||||
root.Children.Add(leaf);
|
||||
i++;
|
||||
}
|
||||
Progress?.Invoke($"STL 解析 {sw.ElapsedMilliseconds:N0} ms({root.Children.Count} 個網格)");
|
||||
}
|
||||
|
||||
// ─── DXF(多為線架構;Solid/Face 走同一套,曲線取樣成線段)──
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Media3D;
|
||||
|
||||
namespace STPViewer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 直接解析 STL 三角網格(binary / ASCII)→ WPF mesh,**不經過 CADability**。
|
||||
/// CADability 的 ImportSTL 會為每個三角形建一個 B-rep Face(含 Edge/Vertex 拓樸),
|
||||
/// 大檔(百萬三角形)需數十分鐘且吃光記憶體;STL 本就無 B-rep,直接讀三角形即可。
|
||||
/// 實測 100k 三角形:CADability 約 41,000 ms,直接解析約 11 ms。
|
||||
/// </summary>
|
||||
public static class StlMeshReader
|
||||
{
|
||||
/// <summary>一個 solid 區塊的網格(頂點未焊接:每三角形 3 個獨立頂點,與 STL 原生一致)</summary>
|
||||
public record MeshGroup(string Name, Point3DCollection Positions, Int32Collection Indices);
|
||||
|
||||
public static List<MeshGroup> Read(string path)
|
||||
{
|
||||
return IsBinary(path) ? ReadBinary(path) : ReadAscii(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// binary STL 判定:檔案大小必須剛好等於 84 + 50×count(count = offset 80 的 uint32)。
|
||||
/// 這是最可靠的判別法 — binary 的 80-byte header 常也以 "solid" 開頭,不能只看開頭字串。
|
||||
/// </summary>
|
||||
private static bool IsBinary(string path)
|
||||
{
|
||||
var fi = new FileInfo(path);
|
||||
if (fi.Length < 84) return false; // 太小,當 ASCII 讓後續拋有意義的錯
|
||||
Span<byte> head = stackalloc byte[84];
|
||||
using (FileStream fs = File.OpenRead(path))
|
||||
{
|
||||
int read = 0;
|
||||
while (read < 84)
|
||||
{
|
||||
int n = fs.Read(head[read..]);
|
||||
if (n <= 0) break;
|
||||
read += n;
|
||||
}
|
||||
if (read < 84) return false;
|
||||
}
|
||||
uint count = BitConverter.ToUInt32(head[80..]);
|
||||
return fi.Length == 84L + 50L * count;
|
||||
}
|
||||
|
||||
private static List<MeshGroup> ReadBinary(string path)
|
||||
{
|
||||
byte[] bytes = File.ReadAllBytes(path);
|
||||
uint count = BitConverter.ToUInt32(bytes, 80);
|
||||
var positions = new Point3DCollection((int)Math.Min(count * 3, int.MaxValue));
|
||||
var indices = new Int32Collection((int)Math.Min(count * 3, int.MaxValue));
|
||||
|
||||
var span = bytes.AsSpan();
|
||||
int off = 84, vi = 0;
|
||||
for (uint t = 0; t < count; t++)
|
||||
{
|
||||
if (off + 50 > bytes.Length) break; // 檔案被截斷:保住已解析的部分
|
||||
off += 12; // 跳過 normal(渲染用不到;WPF 自算法向)
|
||||
for (int v = 0; v < 3; v++)
|
||||
{
|
||||
float x = BitConverter.ToSingle(span[off..]);
|
||||
float y = BitConverter.ToSingle(span[(off + 4)..]);
|
||||
float z = BitConverter.ToSingle(span[(off + 8)..]);
|
||||
positions.Add(new Point3D(x, y, z));
|
||||
indices.Add(vi++);
|
||||
off += 12;
|
||||
}
|
||||
off += 2; // attribute byte count
|
||||
}
|
||||
return new List<MeshGroup> { new("網格", positions, indices) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ASCII STL:逐行掃 "vertex x y z",每 3 個湊成一個三角形;"solid <name>" 起新群組。
|
||||
/// ASCII 大檔罕見(同樣三角數約為 binary 的 5 倍體積),故以正確性為主。
|
||||
/// </summary>
|
||||
private static List<MeshGroup> ReadAscii(string path)
|
||||
{
|
||||
var groups = new List<MeshGroup>();
|
||||
Point3DCollection? pos = null;
|
||||
Int32Collection? idx = null;
|
||||
int vi = 0;
|
||||
|
||||
void FlushGroup()
|
||||
{
|
||||
if (pos is { Count: > 0 } && idx is not null)
|
||||
groups.Add(new MeshGroup(groups.Count == 0 ? "網格" : $"網格 {groups.Count + 1}", pos, idx));
|
||||
pos = null; idx = null; vi = 0;
|
||||
}
|
||||
|
||||
char[] sep = { ' ', '\t' };
|
||||
foreach (string raw in File.ReadLines(path))
|
||||
{
|
||||
string line = raw.Trim();
|
||||
if (line.Length == 0) continue;
|
||||
|
||||
if (line.StartsWith("solid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FlushGroup();
|
||||
pos = new Point3DCollection();
|
||||
idx = new Int32Collection();
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("endsolid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FlushGroup();
|
||||
continue;
|
||||
}
|
||||
if (!line.StartsWith("vertex", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
pos ??= new Point3DCollection();
|
||||
idx ??= new Int32Collection();
|
||||
string[] tok = line.Split(sep, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (tok.Length < 4) continue;
|
||||
if (double.TryParse(tok[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double x) &&
|
||||
double.TryParse(tok[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double y) &&
|
||||
double.TryParse(tok[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double z))
|
||||
{
|
||||
pos.Add(new Point3D(x, y, z));
|
||||
idx.Add(vi++);
|
||||
}
|
||||
}
|
||||
FlushGroup();
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SmokeTest;
|
||||
|
||||
/// <summary>產生測試用 binary STL:一片 gridN×gridN 的網格,每格 2 個三角形(波浪起伏,避免全共面)。</summary>
|
||||
internal static class MakeStl
|
||||
{
|
||||
public static void Run(string path, int triCount)
|
||||
{
|
||||
// 每格 2 三角,格數 = triCount/2;邊長 grid = ceil(sqrt(cells))
|
||||
int cells = System.Math.Max(1, triCount / 2);
|
||||
int grid = (int)System.Math.Ceiling(System.Math.Sqrt(cells));
|
||||
int tris = grid * grid * 2;
|
||||
|
||||
using var fs = File.Create(path);
|
||||
using var bw = new BinaryWriter(fs);
|
||||
bw.Write(new byte[80]); // header
|
||||
bw.Write((uint)tris); // triangle count
|
||||
|
||||
static void WriteTri(BinaryWriter w, float ax, float ay, float az,
|
||||
float bx, float by, float bz, float cx, float cy, float cz)
|
||||
{
|
||||
for (int i = 0; i < 3; i++) w.Write(0f); // normal (0,0,0)
|
||||
w.Write(ax); w.Write(ay); w.Write(az);
|
||||
w.Write(bx); w.Write(by); w.Write(bz);
|
||||
w.Write(cx); w.Write(cy); w.Write(cz);
|
||||
w.Write((ushort)0); // attribute byte count
|
||||
}
|
||||
|
||||
float Z(int x, int y) => (float)(System.Math.Sin(x * 0.1) * System.Math.Cos(y * 0.1) * 5.0);
|
||||
for (int y = 0; y < grid; y++)
|
||||
for (int x = 0; x < grid; x++)
|
||||
{
|
||||
float x0 = x, x1 = x + 1, y0 = y, y1 = y + 1;
|
||||
WriteTri(bw, x0, y0, Z(x, y), x1, y0, Z(x + 1, y), x1, y1, Z(x + 1, y + 1));
|
||||
WriteTri(bw, x0, y0, Z(x, y), x1, y1, Z(x + 1, y + 1), x0, y1, Z(x, y + 1));
|
||||
}
|
||||
|
||||
Console.WriteLine($"寫出 {tris:N0} 三角形 → {path} ({new FileInfo(path).Length:N0} bytes)");
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@ if (args[0] == "--make-dxf")
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[0] == "--make-stl")
|
||||
{
|
||||
SmokeTest.MakeStl.Run(args[1], args.Length > 2 ? int.Parse(args[2]) : 1_500_000);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[0] == "--clip-test")
|
||||
return SmokeTest.ClipTest.Run() == 0 ? 0 : 2;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user