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,474 @@
1
+ #include "convert_worker.h"
2
+
3
+ #include "gltf_writer.h"
4
+ #include "mesh_types.h"
5
+ #include "occt_convert.h"
6
+
7
+ #include <algorithm>
8
+ #include <chrono>
9
+ #include <cmath>
10
+ #include <exception>
11
+ #include <iomanip>
12
+ #include <iostream>
13
+ #include <limits>
14
+ #include <sstream>
15
+ #include <utility>
16
+
17
+ static std::string FormatHMSms(const std::uint64_t ms)
18
+ {
19
+ const std::uint64_t milli = ms % 1000;
20
+ const std::uint64_t totalSec = ms / 1000;
21
+ const std::uint64_t sec = totalSec % 60;
22
+ const std::uint64_t totalMin = totalSec / 60;
23
+ const std::uint64_t min = totalMin % 60;
24
+ const std::uint64_t hour = totalMin / 60;
25
+
26
+ std::ostringstream oss;
27
+ oss << std::setfill('0')
28
+ << std::setw(2) << hour << ":"
29
+ << std::setw(2) << min << ":"
30
+ << std::setw(2) << sec << "."
31
+ << std::setw(3) << milli;
32
+ return oss.str();
33
+ }
34
+
35
+ static std::string FormatSeconds(const std::uint64_t ms)
36
+ {
37
+ std::ostringstream oss;
38
+ oss << std::fixed << std::setprecision(3) << (static_cast<double>(ms) / 1000.0) << "s";
39
+ return oss.str();
40
+ }
41
+
42
+ static void ApplyUnitScale(SceneData& scene, const double s)
43
+ {
44
+ if (s == 1.0)
45
+ {
46
+ return;
47
+ }
48
+ // Apply uniform scale on root nodes so it propagates to the whole hierarchy.
49
+ for (int rootIdx : scene.roots)
50
+ {
51
+ if (rootIdx < 0 || static_cast<size_t>(rootIdx) >= scene.nodes.size())
52
+ {
53
+ continue;
54
+ }
55
+ auto& m = scene.nodes[static_cast<size_t>(rootIdx)].matrix;
56
+ // Column-major 4x4: scale the basis vectors and translation.
57
+ m[0] *= static_cast<float>(s); m[1] *= static_cast<float>(s); m[2] *= static_cast<float>(s);
58
+ m[4] *= static_cast<float>(s); m[5] *= static_cast<float>(s); m[6] *= static_cast<float>(s);
59
+ m[8] *= static_cast<float>(s); m[9] *= static_cast<float>(s); m[10] *= static_cast<float>(s);
60
+ m[12] *= static_cast<float>(s); m[13] *= static_cast<float>(s); m[14] *= static_cast<float>(s);
61
+ }
62
+ }
63
+
64
+ using Mat4 = std::array<float, 16>;
65
+
66
+ static Mat4 MulMat4(const Mat4& A, const Mat4& B);
67
+
68
+ static Mat4 YUpToZUpMat4()
69
+ {
70
+ // Rotate +90 degrees about X:
71
+ // (x, y, z) -> (x, -z, y)
72
+ // Column-major 4x4.
73
+ return Mat4{
74
+ 1, 0, 0, 0,
75
+ 0, 0, 1, 0,
76
+ 0, -1, 0, 0,
77
+ 0, 0, 0, 1
78
+ };
79
+ }
80
+
81
+ static void ApplyYUpToZUp(SceneData& scene)
82
+ {
83
+ const Mat4 R = YUpToZUpMat4();
84
+ for (int rootIdx : scene.roots)
85
+ {
86
+ if (rootIdx < 0 || static_cast<size_t>(rootIdx) >= scene.nodes.size())
87
+ {
88
+ continue;
89
+ }
90
+ scene.nodes[(size_t)rootIdx].matrix = MulMat4(R, scene.nodes[(size_t)rootIdx].matrix);
91
+ }
92
+ }
93
+
94
+ using CenterMode = ConvertWorker::OutputOptions::CenterMode;
95
+
96
+ static Mat4 IdentityMat4()
97
+ {
98
+ return Mat4{
99
+ 1,0,0,0,
100
+ 0,1,0,0,
101
+ 0,0,1,0,
102
+ 0,0,0,1
103
+ };
104
+ }
105
+
106
+ static Mat4 MulMat4(const Mat4& A, const Mat4& B)
107
+ {
108
+ Mat4 C{};
109
+ for (int col = 0; col < 4; ++col)
110
+ {
111
+ for (int row = 0; row < 4; ++row)
112
+ {
113
+ float sum = 0.0f;
114
+ for (int k = 0; k < 4; ++k)
115
+ {
116
+ sum += A[k * 4 + row] * B[col * 4 + k];
117
+ }
118
+ C[col * 4 + row] = sum;
119
+ }
120
+ }
121
+ return C;
122
+ }
123
+
124
+ static inline void TransformPoint(const Mat4& M, float& x, float& y, float& z)
125
+ {
126
+ const float tx = M[0] * x + M[4] * y + M[8] * z + M[12];
127
+ const float ty = M[1] * x + M[5] * y + M[9] * z + M[13];
128
+ const float tz = M[2] * x + M[6] * y + M[10] * z + M[14];
129
+ x = tx; y = ty; z = tz;
130
+ }
131
+
132
+ static inline void TransformVector3x3(const Mat4& M, float& x, float& y, float& z)
133
+ {
134
+ const float tx = M[0] * x + M[4] * y + M[8] * z;
135
+ const float ty = M[1] * x + M[5] * y + M[9] * z;
136
+ const float tz = M[2] * x + M[6] * y + M[10] * z;
137
+ x = tx; y = ty; z = tz;
138
+ }
139
+
140
+ static inline void Normalize3(float& x, float& y, float& z)
141
+ {
142
+ const float len2 = x * x + y * y + z * z;
143
+ if (len2 > 0.0f)
144
+ {
145
+ const float inv = 1.0f / std::sqrt(len2);
146
+ x *= inv; y *= inv; z *= inv;
147
+ }
148
+ }
149
+
150
+ struct BBox3D
151
+ {
152
+ float minX = std::numeric_limits<float>::infinity();
153
+ float maxX = -std::numeric_limits<float>::infinity();
154
+ float minY = std::numeric_limits<float>::infinity();
155
+ float maxY = -std::numeric_limits<float>::infinity();
156
+ float minZ = std::numeric_limits<float>::infinity();
157
+ float maxZ = -std::numeric_limits<float>::infinity();
158
+ bool valid = false;
159
+ };
160
+
161
+ static void BakeTransformsToMeshes(SceneData& scene, BBox3D* bbox)
162
+ {
163
+ if (scene.nodes.empty())
164
+ {
165
+ return;
166
+ }
167
+
168
+ std::vector<Mat4> world(scene.nodes.size(), IdentityMat4());
169
+ const Mat4 I = IdentityMat4();
170
+
171
+ std::function<void(int, const Mat4&)> dfs = [&](int idx, const Mat4& parent) {
172
+ if (idx < 0 || static_cast<size_t>(idx) >= scene.nodes.size())
173
+ {
174
+ return;
175
+ }
176
+ const Mat4 w = MulMat4(parent, scene.nodes[(size_t)idx].matrix);
177
+ world[(size_t)idx] = w;
178
+ for (int c : scene.nodes[(size_t)idx].children)
179
+ {
180
+ dfs(c, w);
181
+ }
182
+ };
183
+
184
+ for (int root : scene.roots)
185
+ {
186
+ dfs(root, I);
187
+ }
188
+
189
+ std::vector<MeshData> baked;
190
+ baked.reserve(scene.nodes.size());
191
+
192
+ for (size_t ni = 0; ni < scene.nodes.size(); ++ni)
193
+ {
194
+ SceneNode& node = scene.nodes[ni];
195
+ if (node.meshIndex < 0)
196
+ {
197
+ continue;
198
+ }
199
+ const int srcIdx = node.meshIndex;
200
+ if (static_cast<size_t>(srcIdx) >= scene.meshes.size())
201
+ {
202
+ continue;
203
+ }
204
+
205
+ const MeshData& src = scene.meshes[(size_t)srcIdx];
206
+ MeshData dst;
207
+ dst.materialIndex = src.materialIndex;
208
+ dst.positions = src.positions;
209
+ dst.normals = src.normals;
210
+
211
+ const Mat4& M = world[ni];
212
+
213
+ // Transform positions
214
+ for (size_t i = 0; i + 2 < dst.positions.size(); i += 3)
215
+ {
216
+ float x = dst.positions[i + 0];
217
+ float y = dst.positions[i + 1];
218
+ float z = dst.positions[i + 2];
219
+ TransformPoint(M, x, y, z);
220
+ dst.positions[i + 0] = x;
221
+ dst.positions[i + 1] = y;
222
+ dst.positions[i + 2] = z;
223
+
224
+ if (bbox)
225
+ {
226
+ bbox->valid = true;
227
+ bbox->minX = std::min(bbox->minX, x);
228
+ bbox->maxX = std::max(bbox->maxX, x);
229
+ bbox->minY = std::min(bbox->minY, y);
230
+ bbox->maxY = std::max(bbox->maxY, y);
231
+ bbox->minZ = std::min(bbox->minZ, z);
232
+ bbox->maxZ = std::max(bbox->maxZ, z);
233
+ }
234
+ }
235
+
236
+ // Transform normals (3x3) + normalize
237
+ for (size_t i = 0; i + 2 < dst.normals.size(); i += 3)
238
+ {
239
+ float x = dst.normals[i + 0];
240
+ float y = dst.normals[i + 1];
241
+ float z = dst.normals[i + 2];
242
+ TransformVector3x3(M, x, y, z);
243
+ Normalize3(x, y, z);
244
+ dst.normals[i + 0] = x;
245
+ dst.normals[i + 1] = y;
246
+ dst.normals[i + 2] = z;
247
+ }
248
+
249
+ baked.push_back(std::move(dst));
250
+ node.meshIndex = static_cast<int>(baked.size() - 1);
251
+ }
252
+
253
+ scene.meshes = std::move(baked);
254
+
255
+ // Reset all node transforms to identity (apply transforms).
256
+ for (auto& node : scene.nodes)
257
+ {
258
+ node.matrix = IdentityMat4();
259
+ }
260
+ }
261
+
262
+ static void ShiftMeshes(SceneData& scene, const float dx, const float dy, const float dz)
263
+ {
264
+ for (auto& mesh : scene.meshes)
265
+ {
266
+ for (size_t i = 0; i + 2 < mesh.positions.size(); i += 3)
267
+ {
268
+ mesh.positions[i + 0] -= dx;
269
+ mesh.positions[i + 1] -= dy;
270
+ mesh.positions[i + 2] -= dz;
271
+ }
272
+ }
273
+ }
274
+
275
+ ConvertWorker::ConvertWorker(const Napi::Env& env,
276
+ Napi::Promise::Deferred deferred,
277
+ std::string inputPath,
278
+ const int logLevel,
279
+ std::vector<VariantOptions> variants)
280
+ : Napi::AsyncWorker(env),
281
+ deferred_(std::move(deferred)),
282
+ inputPath_(std::move(inputPath)),
283
+ logLevel_(logLevel),
284
+ variants_(std::move(variants))
285
+ {
286
+ }
287
+
288
+ void ConvertWorker::Execute()
289
+ {
290
+ try
291
+ {
292
+ if (logLevel_ >= 1)
293
+ {
294
+ std::cerr << "[occt-gltf] ===== Convert START =====\n";
295
+ }
296
+
297
+ if (variants_.empty())
298
+ {
299
+ throw std::runtime_error("No output variants specified.");
300
+ }
301
+
302
+ // Load STEP/XDE once (dominant cost), then run meshing+export for each variant.
303
+ ConvertStats shared;
304
+ StepXdeContext* ctx = LoadStepXdeContext(inputPath_, /*readNames*/ true, /*readColors*/ true, &shared, logLevel_);
305
+ try
306
+ {
307
+ // Process coarsest tessellation first to avoid reusing a finer triangulation for a coarse variant.
308
+ std::vector<size_t> order(variants_.size());
309
+ for (size_t i = 0; i < variants_.size(); ++i) order[i] = i;
310
+ std::stable_sort(order.begin(), order.end(), [&](size_t a, size_t b) {
311
+ const auto& A = variants_[a].tess;
312
+ const auto& B = variants_[b].tess;
313
+ if (A.linearDeflection != B.linearDeflection) return A.linearDeflection > B.linearDeflection;
314
+ return A.angularDeflection > B.angularDeflection;
315
+ });
316
+
317
+ for (size_t oi = 0; oi < order.size(); ++oi)
318
+ {
319
+ const VariantOptions& v = variants_[order[oi]];
320
+
321
+ if (logLevel_ >= 1)
322
+ {
323
+ std::cerr << "[occt-gltf] Variant " << (oi + 1) << "/" << order.size()
324
+ << " -> " << v.outputPath
325
+ << " (lin=" << v.tess.linearDeflection
326
+ << ", ang=" << v.tess.angularDeflection
327
+ << ", normals=" << (v.tess.smoothNormals ? "smooth" : "flat");
328
+ if (v.tess.smoothNormals)
329
+ {
330
+ std::cerr << ", crease=" << v.tess.normalCreaseAngle;
331
+ }
332
+ std::cerr
333
+ << ", minBBoxDiag=" << v.filter.minBBoxDiagonal
334
+ << ", unitScale=" << v.output.unitScale
335
+ << ", bake=" << (v.output.bakeTransforms ? "true" : "false")
336
+ << ", center=";
337
+ switch (v.output.center)
338
+ {
339
+ case CenterMode::XY: std::cerr << "xy"; break;
340
+ case CenterMode::XZ: std::cerr << "xz"; break;
341
+ case CenterMode::YZ: std::cerr << "yz"; break;
342
+ case CenterMode::None: default: std::cerr << "none"; break;
343
+ }
344
+ std::cerr << ")\n";
345
+ }
346
+
347
+ ConvertStats st;
348
+ st.msRead = shared.msRead;
349
+ st.msTransfer = shared.msTransfer;
350
+
351
+ SceneData scene = ConvertStepXdeContextToScene(ctx,
352
+ v.tess.linearDeflection,
353
+ v.tess.angularDeflection,
354
+ v.tess.smoothNormals,
355
+ v.tess.normalCreaseAngle,
356
+ v.filter.minBBoxDiagonal,
357
+ &st,
358
+ logLevel_);
359
+
360
+ ApplyUnitScale(scene, v.output.unitScale);
361
+ if (v.output.zUp)
362
+ {
363
+ ApplyYUpToZUp(scene);
364
+ }
365
+
366
+ if (v.output.bakeTransforms)
367
+ {
368
+ const bool needCenter = (v.output.center != CenterMode::None);
369
+ BBox3D bbox;
370
+ BakeTransformsToMeshes(scene, needCenter ? &bbox : nullptr);
371
+ if (needCenter && bbox.valid)
372
+ {
373
+ const float cx = (bbox.minX + bbox.maxX) * 0.5f;
374
+ const float cy = (bbox.minY + bbox.maxY) * 0.5f;
375
+ const float cz = (bbox.minZ + bbox.maxZ) * 0.5f;
376
+
377
+ float dx = 0.0f, dy = 0.0f, dz = 0.0f;
378
+ switch (v.output.center)
379
+ {
380
+ case CenterMode::XY:
381
+ dx = cx;
382
+ dy = cy;
383
+ if (v.output.zUp)
384
+ {
385
+ dz = bbox.minZ; // ground for Z-up: make minZ = 0
386
+ }
387
+ break;
388
+ case CenterMode::XZ:
389
+ dx = cx;
390
+ dz = cz;
391
+ if (!v.output.zUp)
392
+ {
393
+ dy = bbox.minY; // ground for Y-up: make minY = 0
394
+ }
395
+ break;
396
+ case CenterMode::YZ: dy = cy; dz = cz; break;
397
+ case CenterMode::None: default: break;
398
+ }
399
+ ShiftMeshes(scene, dx, dy, dz);
400
+ }
401
+ }
402
+ else if (v.output.center != CenterMode::None)
403
+ {
404
+ if (logLevel_ >= 1)
405
+ {
406
+ std::cerr << "[occt-gltf] Warning: output.center requested but output.bakeTransforms=false; ignored.\n";
407
+ }
408
+ }
409
+
410
+ const auto tWrite0 = std::chrono::steady_clock::now();
411
+ GltfWriteOptions wopt;
412
+ wopt.dracoEnabled = v.output.dracoEnabled;
413
+ wopt.dracoCompressionLevel = v.output.dracoCompressionLevel;
414
+ wopt.dracoQuantBitsPosition = v.output.dracoQuantBitsPosition;
415
+ wopt.dracoQuantBitsNormal = v.output.dracoQuantBitsNormal;
416
+ st.glbBytes = WriteGLB(v.outputPath, scene, wopt);
417
+ const auto tWrite1 = std::chrono::steady_clock::now();
418
+ st.msWrite =
419
+ (std::uint64_t)std::chrono::duration_cast<std::chrono::milliseconds>(tWrite1 - tWrite0).count();
420
+
421
+ st.msTotal = st.msRead + st.msTransfer + st.msMesh + st.msExtract + st.msWrite;
422
+
423
+ if (logLevel_ >= 1)
424
+ {
425
+ std::cerr << "[occt-gltf] DONE. faces=" << st.facesWithTriangulation << "/" << st.facesTotal
426
+ << ", triangles=" << st.triangles
427
+ << ", vertices=" << st.vertices
428
+ << ", glbBytes=" << st.glbBytes
429
+ << ", total=" << FormatHMSms(st.msTotal) << "\n";
430
+ std::cerr << "[occt-gltf] Timing. read=" << FormatHMSms(st.msRead)
431
+ << ", transfer=" << FormatHMSms(st.msTransfer)
432
+ << ", mesh=" << FormatHMSms(st.msMesh)
433
+ << ", extract=" << FormatHMSms(st.msExtract)
434
+ << ", write=" << FormatHMSms(st.msWrite) << "\n";
435
+ std::cerr << "[occt-gltf] Scene. nodes=" << scene.nodes.size()
436
+ << ", meshes=" << scene.meshes.size()
437
+ << ", skippedNoTri=" << st.skippedMeshesNoTriangulation
438
+ << ", skippedSmall=" << st.skippedSmallParts << "\n";
439
+ }
440
+ }
441
+
442
+ FreeStepXdeContext(ctx);
443
+ }
444
+ catch (...)
445
+ {
446
+ FreeStepXdeContext(ctx);
447
+ throw;
448
+ }
449
+
450
+ if (logLevel_ >= 1)
451
+ {
452
+ std::cerr << "[occt-gltf] ===== Convert END =====\n";
453
+ }
454
+ }
455
+ catch (const std::exception& ex)
456
+ {
457
+ SetError(ex.what());
458
+ }
459
+ catch (...)
460
+ {
461
+ SetError("Unknown error");
462
+ }
463
+ }
464
+
465
+ void ConvertWorker::OnOK()
466
+ {
467
+ deferred_.Resolve(Env().Undefined());
468
+ }
469
+
470
+ void ConvertWorker::OnError(const Napi::Error& e)
471
+ {
472
+ deferred_.Reject(e.Value());
473
+ }
474
+
@@ -0,0 +1,94 @@
1
+ #pragma once
2
+
3
+ #include <napi.h>
4
+
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ class ConvertWorker final : public Napi::AsyncWorker
9
+ {
10
+ public:
11
+ struct TessellationOptions
12
+ {
13
+ double linearDeflection = 0.1;
14
+ double angularDeflection = 0.5; // radians
15
+
16
+ // Generate smooth vertex normals by averaging adjacent triangle normals.
17
+ // If false, flat normals are generated (one normal per triangle).
18
+ bool smoothNormals = true;
19
+
20
+ // Crease angle (radians) used when smoothNormals=true. Faces meeting at angles
21
+ // larger than this are treated as sharp edges (their normals won't be averaged).
22
+ // Default 60 degrees.
23
+ double normalCreaseAngle = 1.0471975511965976;
24
+ };
25
+
26
+ struct FilterOptions
27
+ {
28
+ // Discard leaf parts with bounding-box diagonal smaller than this value (<= 0 disables).
29
+ // Units are the STEP model units as imported by OCCT (usually mm).
30
+ double minBBoxDiagonal = 0.0;
31
+ };
32
+
33
+ struct OutputOptions
34
+ {
35
+ // Unit scale applied to the exported scene (via root node transforms).
36
+ // OCCT XSTEP defaults to millimeters, while glTF convention is meters.
37
+ // Therefore default is 0.001 (mm -> m).
38
+ double unitScale = 0.001;
39
+
40
+ // If true, bake node transforms into mesh vertex data and reset all node transforms to identity
41
+ // (similar to Blender "Apply transforms").
42
+ bool bakeTransforms = false;
43
+
44
+ // Recenter the exported model so that the selected plane center of its bounding box is at (0,0).
45
+ // - Requires bakeTransforms=true (we shift vertex positions).
46
+ enum class CenterMode
47
+ {
48
+ None = 0,
49
+ XY, // center X/Y to (0,0)
50
+ XZ, // center X/Z to (0,0) and shift Y so bbox.minY becomes 0 (ground)
51
+ YZ
52
+ };
53
+ CenterMode center = CenterMode::None;
54
+
55
+ // If true, treat the source coordinates as Z-up (common in CAD/Blender) and rotate
56
+ // the exported scene into Z-up orientation (useful when your runtime (e.g. three.js)
57
+ // uses Z-up world: THREE.Object3D.DEFAULT_UP.set(0,0,1)).
58
+ //
59
+ // Note: glTF's standard coordinate system is Y-up. Enabling zUp makes the exported
60
+ // data Z-up (non-standard glTF), so Y-up viewers may show it rotated.
61
+ bool zUp = false;
62
+
63
+ // Draco mesh compression (KHR_draco_mesh_compression)
64
+ bool dracoEnabled = false;
65
+ int dracoCompressionLevel = 7; // 0..10 (higher = smaller/slower)
66
+ int dracoQuantBitsPosition = 14;
67
+ int dracoQuantBitsNormal = 10;
68
+ };
69
+
70
+ struct VariantOptions
71
+ {
72
+ std::string outputPath;
73
+ TessellationOptions tess;
74
+ FilterOptions filter;
75
+ OutputOptions output;
76
+ };
77
+
78
+ ConvertWorker(const Napi::Env& env,
79
+ Napi::Promise::Deferred deferred,
80
+ std::string inputPath,
81
+ int logLevel,
82
+ std::vector<VariantOptions> variants);
83
+
84
+ void Execute() override;
85
+ void OnOK() override;
86
+ void OnError(const Napi::Error& e) override;
87
+
88
+ private:
89
+ Napi::Promise::Deferred deferred_;
90
+ std::string inputPath_;
91
+ int logLevel_ = 1; // 0=quiet,1=info,2=debug
92
+ std::vector<VariantOptions> variants_;
93
+ };
94
+