occt-gltf-addon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,760 @@
1
+ #include "occt_convert.h"
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+ #include <cstdint>
6
+ #include <functional>
7
+ #include <iostream>
8
+ #include <cmath>
9
+ #include <stdexcept>
10
+ #include <string>
11
+ #include <unordered_map>
12
+
13
+ // OCCT tessellation
14
+ #include <BRepMesh_IncrementalMesh.hxx>
15
+ #include <BRepBndLib.hxx>
16
+ #include <BRep_Tool.hxx>
17
+ #include <Bnd_Box.hxx>
18
+ #include <IMeshTools_Parameters.hxx>
19
+ #include <Poly_Triangulation.hxx>
20
+ #include <TopExp_Explorer.hxx>
21
+ #include <TopoDS.hxx>
22
+ #include <TopoDS_Face.hxx>
23
+ #include <TopoDS_Shape.hxx>
24
+ #include <TopLoc_Location.hxx>
25
+ #include <gp_Pnt.hxx>
26
+ #include <gp_Vec.hxx>
27
+ #include <gp_Trsf.hxx>
28
+
29
+ // OCCT XDE (assembly + names)
30
+ #include <STEPCAFControl_Reader.hxx>
31
+ #include <TDocStd_Document.hxx>
32
+ #include <TDataStd_Name.hxx>
33
+ #include <TDF_Label.hxx>
34
+ #include <XCAFDoc_DocumentTool.hxx>
35
+ #include <XCAFDoc_ColorTool.hxx>
36
+ #include <XCAFDoc_ShapeTool.hxx>
37
+ #include <TopTools_ShapeMapHasher.hxx>
38
+ #include <NCollection_DataMap.hxx>
39
+ #include <Quantity_ColorRGBA.hxx>
40
+
41
+ static inline void AppendVec3(std::vector<float>& dst, const gp_Vec& v)
42
+ {
43
+ dst.push_back(static_cast<float>(v.X()));
44
+ dst.push_back(static_cast<float>(v.Y()));
45
+ dst.push_back(static_cast<float>(v.Z()));
46
+ }
47
+
48
+ static inline void AppendPnt3(std::vector<float>& dst, const gp_Pnt& p)
49
+ {
50
+ dst.push_back(static_cast<float>(p.X()));
51
+ dst.push_back(static_cast<float>(p.Y()));
52
+ dst.push_back(static_cast<float>(p.Z()));
53
+ }
54
+
55
+ static std::string ToUtf8(const TCollection_ExtendedString& s)
56
+ {
57
+ const int len = s.LengthOfCString();
58
+ std::vector<char> buf((size_t)len + 1);
59
+ Standard_PCharacter p = buf.data();
60
+ s.ToUTF8CString(p);
61
+ return std::string(buf.data(), (size_t)len);
62
+ }
63
+
64
+ static std::string GetLabelNameUtf8(const TDF_Label& L)
65
+ {
66
+ occ::handle<TDataStd_Name> nameAttr;
67
+ if (L.FindAttribute(TDataStd_Name::GetID(), nameAttr))
68
+ {
69
+ return ToUtf8(nameAttr->Get());
70
+ }
71
+ return std::string();
72
+ }
73
+
74
+ static std::array<float, 16> LocationToGltfMatrix(const TopLoc_Location& loc)
75
+ {
76
+ const gp_Trsf t = loc.Transformation();
77
+ const double m11 = t.Value(1, 1), m12 = t.Value(1, 2), m13 = t.Value(1, 3), m14 = t.Value(1, 4);
78
+ const double m21 = t.Value(2, 1), m22 = t.Value(2, 2), m23 = t.Value(2, 3), m24 = t.Value(2, 4);
79
+ const double m31 = t.Value(3, 1), m32 = t.Value(3, 2), m33 = t.Value(3, 3), m34 = t.Value(3, 4);
80
+
81
+ // glTF expects column-major matrices.
82
+ return {
83
+ (float)m11, (float)m21, (float)m31, 0.0f,
84
+ (float)m12, (float)m22, (float)m32, 0.0f,
85
+ (float)m13, (float)m23, (float)m33, 0.0f,
86
+ (float)m14, (float)m24, (float)m34, 1.0f
87
+ };
88
+ }
89
+
90
+ static MeshData ConvertShapeToMesh(const TopoDS_Shape& shape,
91
+ double linearDeflection,
92
+ double angularDeflection,
93
+ bool smoothNormals,
94
+ double normalCreaseAngle,
95
+ ConvertStats* stats)
96
+ {
97
+ using Clock = std::chrono::steady_clock;
98
+ auto ms = [](Clock::time_point a, Clock::time_point b) -> std::uint64_t {
99
+ return (std::uint64_t)std::chrono::duration_cast<std::chrono::milliseconds>(b - a).count();
100
+ };
101
+
102
+ const Clock::time_point tMesh0 = Clock::now();
103
+ BRepMesh_IncrementalMesh mesher;
104
+ mesher.SetShape(shape);
105
+ IMeshTools_Parameters& params = mesher.ChangeParameters();
106
+ params.Deflection = linearDeflection;
107
+ params.Angle = angularDeflection;
108
+ params.Relative = false;
109
+ params.InParallel = true;
110
+ mesher.Perform();
111
+ const Clock::time_point tMesh1 = Clock::now();
112
+ if (stats) stats->msMesh += ms(tMesh0, tMesh1);
113
+
114
+ MeshData out;
115
+ out.positions.reserve(1024);
116
+ out.normals.reserve(1024);
117
+
118
+ std::uint64_t faceCount = 0;
119
+ for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) { ++faceCount; }
120
+ if (stats) stats->facesTotal += faceCount;
121
+
122
+ const Clock::time_point tExtract0 = Clock::now();
123
+
124
+ std::uint64_t facesWithTri = 0;
125
+ std::uint64_t triCountTotal = 0;
126
+
127
+ struct QuantKey
128
+ {
129
+ std::int64_t x = 0;
130
+ std::int64_t y = 0;
131
+ std::int64_t z = 0;
132
+ bool operator==(const QuantKey& o) const { return x == o.x && y == o.y && z == o.z; }
133
+ };
134
+ struct QuantKeyHash
135
+ {
136
+ std::size_t operator()(const QuantKey& k) const noexcept
137
+ {
138
+ const std::size_t h1 = std::hash<std::int64_t>{}(k.x);
139
+ const std::size_t h2 = std::hash<std::int64_t>{}(k.y);
140
+ const std::size_t h3 = std::hash<std::int64_t>{}(k.z);
141
+ return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2))
142
+ ^ (h3 + 0x9e3779b97f4a7c15ULL + (h2 << 6) + (h2 >> 2));
143
+ }
144
+ };
145
+
146
+ // For smooth normals we need to group triangles by shared vertices.
147
+ // We weld vertices by quantized position (very fine grid to catch numeric noise).
148
+ std::vector<QuantKey> keys;
149
+ std::vector<float> weights; // per-vertex weight (triangle area*2)
150
+ constexpr double kQuantEps = 1e-6; // in STEP units (usually mm)
151
+ auto makeKey = [&](const gp_Pnt& p) -> QuantKey {
152
+ return QuantKey{
153
+ (std::int64_t)std::llround(p.X() / kQuantEps),
154
+ (std::int64_t)std::llround(p.Y() / kQuantEps),
155
+ (std::int64_t)std::llround(p.Z() / kQuantEps)
156
+ };
157
+ };
158
+
159
+ for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next())
160
+ {
161
+ const TopoDS_Face face = TopoDS::Face(exp.Current());
162
+
163
+ TopLoc_Location triLoc;
164
+ Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(face, triLoc);
165
+ if (tri.IsNull())
166
+ {
167
+ continue;
168
+ }
169
+ ++facesWithTri;
170
+
171
+ const gp_Trsf trsf = triLoc.Transformation();
172
+
173
+ for (int i = 1; i <= tri->NbTriangles(); ++i)
174
+ {
175
+ int n1 = 0, n2 = 0, n3 = 0;
176
+ tri->Triangle(i).Get(n1, n2, n3);
177
+
178
+ gp_Pnt p1 = tri->Node(n1).Transformed(trsf);
179
+ gp_Pnt p2 = tri->Node(n2).Transformed(trsf);
180
+ gp_Pnt p3 = tri->Node(n3).Transformed(trsf);
181
+
182
+ const gp_Vec v12(p1, p2);
183
+ const gp_Vec v13(p1, p3);
184
+ gp_Vec n = v12.Crossed(v13); // unnormalized (magnitude ~ area*2)
185
+ const double nLen2 = n.SquareMagnitude();
186
+ const double w = (nLen2 > 0.0) ? std::sqrt(nLen2) : 0.0;
187
+ if (w > 0.0)
188
+ {
189
+ n /= w; // normalize
190
+ }
191
+
192
+ // Respect face orientation (avoid flipped shading).
193
+ if (face.Orientation() == TopAbs_REVERSED)
194
+ {
195
+ n.Reverse();
196
+ }
197
+
198
+ AppendPnt3(out.positions, p1);
199
+ AppendPnt3(out.positions, p2);
200
+ AppendPnt3(out.positions, p3);
201
+
202
+ AppendVec3(out.normals, n);
203
+ AppendVec3(out.normals, n);
204
+ AppendVec3(out.normals, n);
205
+
206
+ if (smoothNormals)
207
+ {
208
+ keys.push_back(makeKey(p1));
209
+ keys.push_back(makeKey(p2));
210
+ keys.push_back(makeKey(p3));
211
+ const float wf = static_cast<float>(w);
212
+ weights.push_back(wf);
213
+ weights.push_back(wf);
214
+ weights.push_back(wf);
215
+ }
216
+ }
217
+
218
+ triCountTotal += static_cast<std::uint64_t>(tri->NbTriangles());
219
+ }
220
+
221
+ const Clock::time_point tExtract1 = Clock::now();
222
+ if (stats)
223
+ {
224
+ stats->facesWithTriangulation += facesWithTri;
225
+ stats->triangles += triCountTotal;
226
+ stats->vertices += static_cast<std::uint64_t>(out.positions.size() / 3);
227
+ stats->msExtract += ms(tExtract0, tExtract1);
228
+ }
229
+
230
+ // Convert flat normals into smooth vertex normals (with crease angle).
231
+ if (smoothNormals && !out.positions.empty())
232
+ {
233
+ const double kPi = 3.1415926535897932384626433832795;
234
+ const double crease = std::max(0.0, std::min(kPi, normalCreaseAngle));
235
+ if (crease > 0.0)
236
+ {
237
+ const float cosThresh = static_cast<float>(std::cos(crease));
238
+ const size_t vCount = out.positions.size() / 3;
239
+ if (keys.size() == vCount && weights.size() == vCount && out.normals.size() == out.positions.size())
240
+ {
241
+ struct Cluster
242
+ {
243
+ float nx = 0.0f, ny = 0.0f, nz = 1.0f; // representative direction (unit)
244
+ double sx = 0.0, sy = 0.0, sz = 0.0; // accumulated weighted normal
245
+ };
246
+
247
+ std::unordered_map<QuantKey, std::vector<Cluster>, QuantKeyHash> clustersByKey;
248
+ clustersByKey.reserve(vCount / 2);
249
+
250
+ auto normalizeDir = [](double& x, double& y, double& z) {
251
+ const double len2 = x * x + y * y + z * z;
252
+ if (len2 > 0.0)
253
+ {
254
+ const double inv = 1.0 / std::sqrt(len2);
255
+ x *= inv; y *= inv; z *= inv;
256
+ }
257
+ };
258
+
259
+ // 1) Build clusters for each welded vertex key.
260
+ for (size_t vi = 0; vi < vCount; ++vi)
261
+ {
262
+ const float fx = out.normals[vi * 3 + 0];
263
+ const float fy = out.normals[vi * 3 + 1];
264
+ const float fz = out.normals[vi * 3 + 2];
265
+ const float w = weights[vi];
266
+ if (w <= 0.0f)
267
+ {
268
+ continue;
269
+ }
270
+
271
+ auto& clusters = clustersByKey[keys[vi]];
272
+ int best = -1;
273
+ float bestDot = -2.0f;
274
+ for (int ci = 0; ci < (int)clusters.size(); ++ci)
275
+ {
276
+ const float dot = fx * clusters[ci].nx + fy * clusters[ci].ny + fz * clusters[ci].nz;
277
+ if (dot > bestDot)
278
+ {
279
+ bestDot = dot;
280
+ best = ci;
281
+ }
282
+ }
283
+
284
+ if (best >= 0 && bestDot >= cosThresh)
285
+ {
286
+ Cluster& c = clusters[(size_t)best];
287
+ c.sx += (double)fx * (double)w;
288
+ c.sy += (double)fy * (double)w;
289
+ c.sz += (double)fz * (double)w;
290
+ double nx = c.sx, ny = c.sy, nz = c.sz;
291
+ normalizeDir(nx, ny, nz);
292
+ c.nx = (float)nx; c.ny = (float)ny; c.nz = (float)nz;
293
+ }
294
+ else
295
+ {
296
+ Cluster c;
297
+ c.sx = (double)fx * (double)w;
298
+ c.sy = (double)fy * (double)w;
299
+ c.sz = (double)fz * (double)w;
300
+ c.nx = fx; c.ny = fy; c.nz = fz;
301
+ clusters.push_back(c);
302
+ }
303
+ }
304
+
305
+ // 2) Assign per-vertex smooth normals: pick the cluster closest to the original face normal.
306
+ for (size_t vi = 0; vi < vCount; ++vi)
307
+ {
308
+ const float fx = out.normals[vi * 3 + 0];
309
+ const float fy = out.normals[vi * 3 + 1];
310
+ const float fz = out.normals[vi * 3 + 2];
311
+
312
+ const auto it = clustersByKey.find(keys[vi]);
313
+ if (it == clustersByKey.end() || it->second.empty())
314
+ {
315
+ continue;
316
+ }
317
+ const auto& clusters = it->second;
318
+
319
+ int best = 0;
320
+ float bestDot = fx * clusters[0].nx + fy * clusters[0].ny + fz * clusters[0].nz;
321
+ for (int ci = 1; ci < (int)clusters.size(); ++ci)
322
+ {
323
+ const float dot = fx * clusters[(size_t)ci].nx + fy * clusters[(size_t)ci].ny + fz * clusters[(size_t)ci].nz;
324
+ if (dot > bestDot)
325
+ {
326
+ bestDot = dot;
327
+ best = ci;
328
+ }
329
+ }
330
+
331
+ const Cluster& c = clusters[(size_t)best];
332
+ double nx = c.sx, ny = c.sy, nz = c.sz;
333
+ normalizeDir(nx, ny, nz);
334
+ out.normals[vi * 3 + 0] = (float)nx;
335
+ out.normals[vi * 3 + 1] = (float)ny;
336
+ out.normals[vi * 3 + 2] = (float)nz;
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ return out;
343
+ }
344
+
345
+ static int AddNode(SceneData& scene, SceneNode node)
346
+ {
347
+ scene.nodes.push_back(std::move(node));
348
+ return static_cast<int>(scene.nodes.size() - 1);
349
+ }
350
+
351
+ static double BBoxDiagonal(const TopoDS_Shape& shape)
352
+ {
353
+ Bnd_Box box;
354
+ box.SetGap(0.0);
355
+ BRepBndLib::Add(shape, box);
356
+ if (box.IsVoid())
357
+ {
358
+ return 0.0;
359
+ }
360
+ double xmin = 0, ymin = 0, zmin = 0, xmax = 0, ymax = 0, zmax = 0;
361
+ box.Get(xmin, ymin, zmin, xmax, ymax, zmax);
362
+ const double dx = static_cast<double>(xmax - xmin);
363
+ const double dy = static_cast<double>(ymax - ymin);
364
+ const double dz = static_cast<double>(zmax - zmin);
365
+ return std::sqrt(dx * dx + dy * dy + dz * dz);
366
+ }
367
+
368
+ struct StepXdeContext
369
+ {
370
+ occ::handle<TDocStd_Document> doc;
371
+ occ::handle<XCAFDoc_ShapeTool> shapeTool;
372
+ occ::handle<XCAFDoc_ColorTool> colorTool;
373
+ NCollection_Sequence<TDF_Label> roots;
374
+ };
375
+
376
+ StepXdeContext* LoadStepXdeContext(const std::string& inputPath,
377
+ const bool readNames,
378
+ const bool readColors,
379
+ ConvertStats* stats,
380
+ const int logLevel)
381
+ {
382
+ using Clock = std::chrono::steady_clock;
383
+ auto ms = [](Clock::time_point a, Clock::time_point b) -> std::uint64_t {
384
+ return (std::uint64_t)std::chrono::duration_cast<std::chrono::milliseconds>(b - a).count();
385
+ };
386
+
387
+ if (logLevel >= 1)
388
+ {
389
+ std::cerr << "[occt-gltf] STEP(XDE) read: " << inputPath << "\n";
390
+ }
391
+
392
+ const Clock::time_point tRead0 = Clock::now();
393
+ STEPCAFControl_Reader reader;
394
+ reader.SetNameMode(readNames); // read names (Unicode)
395
+ reader.SetColorMode(readColors); // read colors (if present)
396
+ reader.SetLayerMode(false);
397
+ reader.SetPropsMode(false);
398
+
399
+ const IFSelect_ReturnStatus status = reader.ReadFile(inputPath.c_str());
400
+ const Clock::time_point tRead1 = Clock::now();
401
+ if (status != IFSelect_RetDone)
402
+ {
403
+ throw std::runtime_error("Failed to read STEP file: " + inputPath);
404
+ }
405
+ if (stats) stats->msRead = ms(tRead0, tRead1);
406
+
407
+ const Clock::time_point tTransfer0 = Clock::now();
408
+ if (logLevel >= 1)
409
+ {
410
+ std::cerr << "[occt-gltf] XDE Transfer...\n";
411
+ }
412
+ occ::handle<TDocStd_Document> doc = new TDocStd_Document("occt-gltf");
413
+ if (!reader.Transfer(doc))
414
+ {
415
+ throw std::runtime_error("STEPCAFControl_Reader.Transfer() failed.");
416
+ }
417
+ const Clock::time_point tTransfer1 = Clock::now();
418
+ if (stats) stats->msTransfer = ms(tTransfer0, tTransfer1);
419
+
420
+ const occ::handle<XCAFDoc_ShapeTool> shapeTool =
421
+ XCAFDoc_DocumentTool::ShapeTool(doc->Main());
422
+ if (shapeTool.IsNull())
423
+ {
424
+ throw std::runtime_error("XCAFDoc_ShapeTool not available.");
425
+ }
426
+
427
+ StepXdeContext* ctx = new StepXdeContext();
428
+ ctx->doc = doc;
429
+ ctx->shapeTool = shapeTool;
430
+ ctx->colorTool = XCAFDoc_DocumentTool::ColorTool(doc->Main());
431
+ shapeTool->GetFreeShapes(ctx->roots);
432
+ if (logLevel >= 1)
433
+ {
434
+ std::cerr << "[occt-gltf] FreeShapes: " << ctx->roots.Length() << "\n";
435
+ }
436
+ return ctx;
437
+ }
438
+
439
+ void FreeStepXdeContext(StepXdeContext* ctx)
440
+ {
441
+ delete ctx;
442
+ }
443
+
444
+ SceneData ConvertStepXdeContextToScene(StepXdeContext* ctx,
445
+ double linearDeflection,
446
+ double angularDeflection,
447
+ bool smoothNormals,
448
+ double normalCreaseAngle,
449
+ double minBBoxDiagonal,
450
+ ConvertStats* stats,
451
+ int logLevel)
452
+ {
453
+ if (ctx == nullptr || ctx->doc.IsNull() || ctx->shapeTool.IsNull())
454
+ {
455
+ throw std::runtime_error("Invalid StepXdeContext.");
456
+ }
457
+
458
+ if (stats)
459
+ {
460
+ stats->linearDeflection = linearDeflection;
461
+ stats->angularDeflection = angularDeflection;
462
+ }
463
+
464
+ const occ::handle<XCAFDoc_ShapeTool> shapeTool = ctx->shapeTool;
465
+
466
+ SceneData scene;
467
+
468
+ const occ::handle<XCAFDoc_ColorTool> colorTool = ctx->colorTool;
469
+ std::unordered_map<std::uint32_t, int> materialMap;
470
+
471
+ struct ColorInfo
472
+ {
473
+ bool hasColor = false;
474
+ bool isInstance = false; // instance color overrides shape colors
475
+ std::uint32_t rgba8 = 0; // packed RGBA8
476
+ };
477
+
478
+ auto clamp01 = [](double v) -> double { return std::min(1.0, std::max(0.0, v)); };
479
+ auto toU8 = [&](double v) -> std::uint8_t {
480
+ const double x = clamp01(v);
481
+ return static_cast<std::uint8_t>(std::lround(x * 255.0));
482
+ };
483
+
484
+ auto packRgba8 = [&](const Quantity_ColorRGBA& c) -> std::uint32_t {
485
+ const auto& rgb = c.GetRGB();
486
+ const std::uint8_t r = toU8(rgb.Red());
487
+ const std::uint8_t g = toU8(rgb.Green());
488
+ const std::uint8_t b = toU8(rgb.Blue());
489
+ const std::uint8_t a = toU8(static_cast<double>(c.Alpha()));
490
+ return (static_cast<std::uint32_t>(r) << 24)
491
+ | (static_cast<std::uint32_t>(g) << 16)
492
+ | (static_cast<std::uint32_t>(b) << 8)
493
+ | static_cast<std::uint32_t>(a);
494
+ };
495
+
496
+ auto getOrCreateMaterial = [&](const std::uint32_t rgba8) -> int {
497
+ const auto it = materialMap.find(rgba8);
498
+ if (it != materialMap.end())
499
+ {
500
+ return it->second;
501
+ }
502
+
503
+ const float r = static_cast<float>((rgba8 >> 24) & 0xFFu) / 255.0f;
504
+ const float g = static_cast<float>((rgba8 >> 16) & 0xFFu) / 255.0f;
505
+ const float b = static_cast<float>((rgba8 >> 8) & 0xFFu) / 255.0f;
506
+ const float a = static_cast<float>((rgba8) & 0xFFu) / 255.0f;
507
+
508
+ MaterialData mat;
509
+ mat.baseColorFactor = {r, g, b, a};
510
+ mat.metallicFactor = 0.0f;
511
+ mat.roughnessFactor = 1.0f;
512
+ mat.doubleSided = true;
513
+
514
+ scene.materials.push_back(mat);
515
+ const int idx = static_cast<int>(scene.materials.size() - 1);
516
+ materialMap.emplace(rgba8, idx);
517
+ return idx;
518
+ };
519
+
520
+ auto tryGetLabelColor = [](const TDF_Label& L, Quantity_ColorRGBA& out) -> bool {
521
+ // Prefer surface color; fall back to generic color.
522
+ if (XCAFDoc_ColorTool::GetColor(L, XCAFDoc_ColorSurf, out)) return true;
523
+ if (XCAFDoc_ColorTool::GetColor(L, XCAFDoc_ColorGen, out)) return true;
524
+ return false;
525
+ };
526
+
527
+ auto tryGetInstanceColor = [&](const TopoDS_Shape& instShape, Quantity_ColorRGBA& out) -> bool {
528
+ if (colorTool.IsNull()) return false;
529
+ if (colorTool->GetInstanceColor(instShape, XCAFDoc_ColorSurf, out)) return true;
530
+ if (colorTool->GetInstanceColor(instShape, XCAFDoc_ColorGen, out)) return true;
531
+ return false;
532
+ };
533
+
534
+ struct MeshVariants
535
+ {
536
+ std::unordered_map<std::uint32_t, int> byColor; // rgba8 -> meshIndex (or -1)
537
+ };
538
+
539
+ // Mesh cache by base shape (location stripped) + color (material).
540
+ NCollection_DataMap<TopoDS_Shape, MeshVariants, TopTools_ShapeMapHasher> meshMap;
541
+
542
+ std::function<int(const TDF_Label&, const TopLoc_Location&, const std::string&, const ColorInfo&)> build;
543
+ build = [&](const TDF_Label& label,
544
+ const TopLoc_Location& loc,
545
+ const std::string& overrideName,
546
+ const ColorInfo& parentColor) -> int {
547
+ const std::string name = [&]() -> std::string {
548
+ std::string n = !overrideName.empty() ? overrideName : GetLabelNameUtf8(label);
549
+ if (n.empty()) n = "Node";
550
+ return n;
551
+ }();
552
+
553
+ ColorInfo curColor = parentColor;
554
+ if (!curColor.isInstance)
555
+ {
556
+ Quantity_ColorRGBA col;
557
+ if (tryGetLabelColor(label, col))
558
+ {
559
+ curColor.hasColor = true;
560
+ curColor.isInstance = false;
561
+ curColor.rgba8 = packRgba8(col);
562
+ }
563
+ }
564
+
565
+ if (XCAFDoc_ShapeTool::IsAssembly(label))
566
+ {
567
+ // Build children first; if all children are discarded, discard this assembly node too.
568
+ std::vector<int> childIndices;
569
+ NCollection_Sequence<TDF_Label> comps;
570
+ XCAFDoc_ShapeTool::GetComponents(label, comps, false);
571
+ for (NCollection_Sequence<TDF_Label>::Iterator it(comps); it.More(); it.Next())
572
+ {
573
+ const TDF_Label compL = it.Value();
574
+ const TopLoc_Location childLoc = XCAFDoc_ShapeTool::GetLocation(compL);
575
+ TDF_Label refL;
576
+ std::string childName = GetLabelNameUtf8(compL);
577
+ if (!XCAFDoc_ShapeTool::GetReferredShape(compL, refL))
578
+ {
579
+ refL = compL;
580
+ }
581
+
582
+ ColorInfo childColor = curColor;
583
+ {
584
+ Quantity_ColorRGBA instCol;
585
+ bool hasInstCol = false;
586
+ TopoDS_Shape instShape = XCAFDoc_ShapeTool::GetShape(compL);
587
+ if (!instShape.IsNull() && tryGetInstanceColor(instShape, instCol))
588
+ {
589
+ hasInstCol = true;
590
+ }
591
+ else if (tryGetLabelColor(compL, instCol))
592
+ {
593
+ hasInstCol = true;
594
+ }
595
+
596
+ if (hasInstCol)
597
+ {
598
+ childColor.hasColor = true;
599
+ childColor.isInstance = true;
600
+ childColor.rgba8 = packRgba8(instCol);
601
+ }
602
+ }
603
+
604
+ const int childIdx = build(refL, childLoc, childName, childColor);
605
+ if (childIdx >= 0)
606
+ {
607
+ childIndices.push_back(childIdx);
608
+ }
609
+ }
610
+
611
+ if (childIndices.empty())
612
+ {
613
+ return -1;
614
+ }
615
+
616
+ SceneNode node;
617
+ node.matrix = LocationToGltfMatrix(loc);
618
+ node.name = name;
619
+ node.children = std::move(childIndices);
620
+ return AddNode(scene, std::move(node));
621
+ }
622
+
623
+ // Leaf / part: generate or reuse mesh.
624
+ TopoDS_Shape s = XCAFDoc_ShapeTool::GetShape(label);
625
+ if (s.IsNull())
626
+ {
627
+ return -1;
628
+ }
629
+ TopoDS_Shape base = s.Located(TopLoc_Location()); // strip location for cache key
630
+
631
+ // Small-part filter (before meshing).
632
+ if (minBBoxDiagonal > 0.0)
633
+ {
634
+ const double diag = BBoxDiagonal(base);
635
+ if (diag > 0.0 && diag < minBBoxDiagonal)
636
+ {
637
+ if (stats) stats->skippedSmallParts += 1;
638
+ return -1;
639
+ }
640
+ }
641
+
642
+ const std::uint32_t matKey = curColor.hasColor ? curColor.rgba8 : 0xFFFFFFFFu;
643
+
644
+ // meshIdx: -2 = not cached yet, -1 = cached as "no mesh", >=0 = mesh index
645
+ int meshIdx = -2;
646
+ MeshVariants* variants = meshMap.ChangeSeek(base);
647
+ if (variants == nullptr)
648
+ {
649
+ MeshVariants v;
650
+ meshMap.Bind(base, std::move(v));
651
+ variants = meshMap.ChangeSeek(base);
652
+ }
653
+ if (variants != nullptr)
654
+ {
655
+ const auto it = variants->byColor.find(matKey);
656
+ if (it != variants->byColor.end())
657
+ {
658
+ meshIdx = it->second;
659
+ }
660
+ }
661
+
662
+ if (meshIdx == -1)
663
+ {
664
+ return -1;
665
+ }
666
+
667
+ if (meshIdx == -2)
668
+ {
669
+ if (logLevel >= 2)
670
+ {
671
+ std::cerr << "[occt-gltf] Meshing leaf: " << name << "\n";
672
+ }
673
+ MeshData mesh = ConvertShapeToMesh(base,
674
+ linearDeflection,
675
+ angularDeflection,
676
+ smoothNormals,
677
+ normalCreaseAngle,
678
+ stats);
679
+ if (mesh.positions.empty())
680
+ {
681
+ if (logLevel >= 2)
682
+ {
683
+ std::cerr << "[occt-gltf] Warning: skip leaf (no triangulation): " << name << "\n";
684
+ }
685
+ if (stats) stats->skippedMeshesNoTriangulation += 1;
686
+ if (variants != nullptr) variants->byColor[matKey] = -1;
687
+ return -1;
688
+ }
689
+
690
+ if (curColor.hasColor)
691
+ {
692
+ mesh.materialIndex = getOrCreateMaterial(matKey);
693
+ }
694
+
695
+ scene.meshes.push_back(std::move(mesh));
696
+ meshIdx = static_cast<int>(scene.meshes.size() - 1);
697
+ if (variants != nullptr) variants->byColor[matKey] = meshIdx;
698
+ }
699
+
700
+ if (meshIdx < 0)
701
+ {
702
+ return -1;
703
+ }
704
+
705
+ SceneNode node;
706
+ node.matrix = LocationToGltfMatrix(loc);
707
+ node.name = name;
708
+ node.meshIndex = meshIdx;
709
+ return AddNode(scene, std::move(node));
710
+ };
711
+
712
+ for (NCollection_Sequence<TDF_Label>::Iterator it(ctx->roots); it.More(); it.Next())
713
+ {
714
+ const TDF_Label rootL = it.Value();
715
+ const ColorInfo noColor;
716
+ const int rootIdx = build(rootL, TopLoc_Location(), std::string(), noColor);
717
+ if (rootIdx >= 0)
718
+ {
719
+ scene.roots.push_back(rootIdx);
720
+ }
721
+ }
722
+
723
+ if (stats)
724
+ {
725
+ stats->nodes = static_cast<std::uint64_t>(scene.nodes.size());
726
+ stats->meshes = static_cast<std::uint64_t>(scene.meshes.size());
727
+ // stats->msTotal is computed by the worker (including read/transfer/write).
728
+ }
729
+
730
+ return scene;
731
+ }
732
+
733
+ SceneData ConvertSTEPToScene(const std::string& inputPath,
734
+ double linearDeflection,
735
+ double angularDeflection,
736
+ double minBBoxDiagonal,
737
+ ConvertStats* stats)
738
+ {
739
+ StepXdeContext* ctx = LoadStepXdeContext(inputPath, true, true, stats, /*logLevel*/ 1);
740
+ try
741
+ {
742
+ SceneData scene =
743
+ ConvertStepXdeContextToScene(ctx,
744
+ linearDeflection,
745
+ angularDeflection,
746
+ /*smoothNormals*/ true,
747
+ /*normalCreaseAngle*/ 1.0471975511965976,
748
+ minBBoxDiagonal,
749
+ stats,
750
+ /*logLevel*/ 1);
751
+ FreeStepXdeContext(ctx);
752
+ return scene;
753
+ }
754
+ catch (...)
755
+ {
756
+ FreeStepXdeContext(ctx);
757
+ throw;
758
+ }
759
+ }
760
+