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.
package/src/addon.cpp ADDED
@@ -0,0 +1,504 @@
1
+ #include <napi.h>
2
+
3
+ #include "convert_worker.h"
4
+
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ static Napi::Value ConvertSTEPToGLTF(const Napi::CallbackInfo& info)
9
+ {
10
+ Napi::Env env = info.Env();
11
+
12
+ if (info.Length() < 1 || !info[0].IsObject())
13
+ {
14
+ Napi::TypeError::New(env, "Expected options object").ThrowAsJavaScriptException();
15
+ return env.Null();
16
+ }
17
+
18
+ Napi::Object options = info[0].As<Napi::Object>();
19
+
20
+ if (!options.Has("inputPath") || !options.Get("inputPath").IsString())
21
+ {
22
+ Napi::TypeError::New(env, "options.inputPath must be a string").ThrowAsJavaScriptException();
23
+ return env.Null();
24
+ }
25
+
26
+ std::string inputPath = options.Get("inputPath").As<Napi::String>().Utf8Value();
27
+
28
+ int logLevel = 1; // 0=quiet, 1=info, 2=debug
29
+ if (options.Has("logLevel"))
30
+ {
31
+ const Napi::Value v = options.Get("logLevel");
32
+ if (v.IsNumber())
33
+ {
34
+ logLevel = v.As<Napi::Number>().Int32Value();
35
+ }
36
+ else if (v.IsString())
37
+ {
38
+ const std::string s = v.As<Napi::String>().Utf8Value();
39
+ if (s == "quiet") logLevel = 0;
40
+ else if (s == "info") logLevel = 1;
41
+ else if (s == "debug") logLevel = 2;
42
+ else
43
+ {
44
+ Napi::RangeError::New(env, "logLevel must be 0|1|2 or 'quiet'|'info'|'debug'")
45
+ .ThrowAsJavaScriptException();
46
+ return env.Null();
47
+ }
48
+ }
49
+ else
50
+ {
51
+ Napi::TypeError::New(env, "logLevel must be a number or string").ThrowAsJavaScriptException();
52
+ return env.Null();
53
+ }
54
+ }
55
+ if (logLevel < 0 || logLevel > 2)
56
+ {
57
+ Napi::RangeError::New(env, "logLevel must be 0, 1, or 2").ThrowAsJavaScriptException();
58
+ return env.Null();
59
+ }
60
+
61
+ ConvertWorker::TessellationOptions tess;
62
+ if (options.Has("tessellation") && options.Get("tessellation").IsObject())
63
+ {
64
+ Napi::Object t = options.Get("tessellation").As<Napi::Object>();
65
+ if (t.Has("linearDeflection") && t.Get("linearDeflection").IsNumber())
66
+ {
67
+ tess.linearDeflection = t.Get("linearDeflection").As<Napi::Number>().DoubleValue();
68
+ }
69
+ if (t.Has("angularDeflection") && t.Get("angularDeflection").IsNumber())
70
+ {
71
+ tess.angularDeflection = t.Get("angularDeflection").As<Napi::Number>().DoubleValue();
72
+ }
73
+ if (t.Has("smoothNormals") && t.Get("smoothNormals").IsBoolean())
74
+ {
75
+ tess.smoothNormals = t.Get("smoothNormals").As<Napi::Boolean>().Value();
76
+ }
77
+ if (t.Has("normalCreaseAngle") && t.Get("normalCreaseAngle").IsNumber())
78
+ {
79
+ tess.normalCreaseAngle = t.Get("normalCreaseAngle").As<Napi::Number>().DoubleValue();
80
+ }
81
+ }
82
+
83
+ if (tess.linearDeflection <= 0.0)
84
+ {
85
+ Napi::RangeError::New(env, "tessellation.linearDeflection must be > 0").ThrowAsJavaScriptException();
86
+ return env.Null();
87
+ }
88
+ if (tess.angularDeflection <= 0.0)
89
+ {
90
+ Napi::RangeError::New(env, "tessellation.angularDeflection must be > 0").ThrowAsJavaScriptException();
91
+ return env.Null();
92
+ }
93
+ if (tess.normalCreaseAngle < 0.0)
94
+ {
95
+ Napi::RangeError::New(env, "tessellation.normalCreaseAngle must be >= 0").ThrowAsJavaScriptException();
96
+ return env.Null();
97
+ }
98
+
99
+ ConvertWorker::FilterOptions filter;
100
+ if (options.Has("filter") && options.Get("filter").IsObject())
101
+ {
102
+ Napi::Object f = options.Get("filter").As<Napi::Object>();
103
+ if (f.Has("minBBoxDiagonal") && f.Get("minBBoxDiagonal").IsNumber())
104
+ {
105
+ filter.minBBoxDiagonal = f.Get("minBBoxDiagonal").As<Napi::Number>().DoubleValue();
106
+ }
107
+ }
108
+ if (filter.minBBoxDiagonal < 0.0)
109
+ {
110
+ Napi::RangeError::New(env, "filter.minBBoxDiagonal must be >= 0").ThrowAsJavaScriptException();
111
+ return env.Null();
112
+ }
113
+
114
+ ConvertWorker::OutputOptions output;
115
+ if (options.Has("output") && options.Get("output").IsObject())
116
+ {
117
+ Napi::Object o = options.Get("output").As<Napi::Object>();
118
+ if (o.Has("unit") && o.Get("unit").IsString())
119
+ {
120
+ const std::string unit = o.Get("unit").As<Napi::String>().Utf8Value();
121
+ if (unit == "m")
122
+ {
123
+ output.unitScale = 0.001; // mm -> m (OCCT default)
124
+ }
125
+ else if (unit == "cm")
126
+ {
127
+ output.unitScale = 0.1; // mm -> cm
128
+ }
129
+ else if (unit == "mm")
130
+ {
131
+ output.unitScale = 1.0; // keep mm
132
+ }
133
+ else
134
+ {
135
+ Napi::RangeError::New(env, "output.unit must be one of: 'm', 'cm', 'mm'")
136
+ .ThrowAsJavaScriptException();
137
+ return env.Null();
138
+ }
139
+ }
140
+ if (o.Has("unitScale") && o.Get("unitScale").IsNumber())
141
+ {
142
+ output.unitScale = o.Get("unitScale").As<Napi::Number>().DoubleValue();
143
+ }
144
+
145
+ if (o.Has("bakeTransforms") && o.Get("bakeTransforms").IsBoolean())
146
+ {
147
+ output.bakeTransforms = o.Get("bakeTransforms").As<Napi::Boolean>().Value();
148
+ }
149
+
150
+ if (o.Has("centerXY") && o.Get("centerXY").IsBoolean())
151
+ {
152
+ const bool v = o.Get("centerXY").As<Napi::Boolean>().Value();
153
+ output.center = v ? ConvertWorker::OutputOptions::CenterMode::XY
154
+ : ConvertWorker::OutputOptions::CenterMode::None;
155
+ }
156
+ if (o.Has("centerXZ") && o.Get("centerXZ").IsBoolean())
157
+ {
158
+ const bool v = o.Get("centerXZ").As<Napi::Boolean>().Value();
159
+ output.center = v ? ConvertWorker::OutputOptions::CenterMode::XZ
160
+ : ConvertWorker::OutputOptions::CenterMode::None;
161
+ }
162
+ if (o.Has("centerYZ") && o.Get("centerYZ").IsBoolean())
163
+ {
164
+ const bool v = o.Get("centerYZ").As<Napi::Boolean>().Value();
165
+ output.center = v ? ConvertWorker::OutputOptions::CenterMode::YZ
166
+ : ConvertWorker::OutputOptions::CenterMode::None;
167
+ }
168
+
169
+ if (o.Has("center") && o.Get("center").IsString())
170
+ {
171
+ const std::string center = o.Get("center").As<Napi::String>().Utf8Value();
172
+ if (center == "none")
173
+ {
174
+ output.center = ConvertWorker::OutputOptions::CenterMode::None;
175
+ }
176
+ else if (center == "xy")
177
+ {
178
+ output.center = ConvertWorker::OutputOptions::CenterMode::XY;
179
+ }
180
+ else if (center == "xz")
181
+ {
182
+ output.center = ConvertWorker::OutputOptions::CenterMode::XZ;
183
+ }
184
+ else if (center == "yz")
185
+ {
186
+ output.center = ConvertWorker::OutputOptions::CenterMode::YZ;
187
+ }
188
+ else
189
+ {
190
+ Napi::RangeError::New(env, "output.center must be one of: 'none', 'xy', 'xz', 'yz'")
191
+ .ThrowAsJavaScriptException();
192
+ return env.Null();
193
+ }
194
+ }
195
+
196
+ if (o.Has("zUp") && o.Get("zUp").IsBoolean())
197
+ {
198
+ output.zUp = o.Get("zUp").As<Napi::Boolean>().Value();
199
+ }
200
+ if (o.Has("zup") && o.Get("zup").IsBoolean())
201
+ {
202
+ output.zUp = o.Get("zup").As<Napi::Boolean>().Value();
203
+ }
204
+
205
+ if (o.Has("draco"))
206
+ {
207
+ const Napi::Value dv = o.Get("draco");
208
+ if (dv.IsBoolean())
209
+ {
210
+ output.dracoEnabled = dv.As<Napi::Boolean>().Value();
211
+ }
212
+ else if (dv.IsObject())
213
+ {
214
+ Napi::Object d = dv.As<Napi::Object>();
215
+ if (d.Has("enabled") && d.Get("enabled").IsBoolean())
216
+ {
217
+ output.dracoEnabled = d.Get("enabled").As<Napi::Boolean>().Value();
218
+ }
219
+ else
220
+ {
221
+ output.dracoEnabled = true;
222
+ }
223
+ if (d.Has("compressionLevel") && d.Get("compressionLevel").IsNumber())
224
+ {
225
+ output.dracoCompressionLevel = d.Get("compressionLevel").As<Napi::Number>().Int32Value();
226
+ }
227
+ if (d.Has("quantizationBitsPosition") && d.Get("quantizationBitsPosition").IsNumber())
228
+ {
229
+ output.dracoQuantBitsPosition = d.Get("quantizationBitsPosition").As<Napi::Number>().Int32Value();
230
+ }
231
+ if (d.Has("quantizationBitsNormal") && d.Get("quantizationBitsNormal").IsNumber())
232
+ {
233
+ output.dracoQuantBitsNormal = d.Get("quantizationBitsNormal").As<Napi::Number>().Int32Value();
234
+ }
235
+ }
236
+ else
237
+ {
238
+ Napi::TypeError::New(env, "output.draco must be boolean or object").ThrowAsJavaScriptException();
239
+ return env.Null();
240
+ }
241
+ }
242
+ }
243
+ if (output.unitScale <= 0.0)
244
+ {
245
+ Napi::RangeError::New(env, "output.unitScale must be > 0").ThrowAsJavaScriptException();
246
+ return env.Null();
247
+ }
248
+ if (output.dracoCompressionLevel < 0 || output.dracoCompressionLevel > 10)
249
+ {
250
+ Napi::RangeError::New(env, "output.draco.compressionLevel must be in [0..10]").ThrowAsJavaScriptException();
251
+ return env.Null();
252
+ }
253
+ if (output.dracoQuantBitsPosition < 1 || output.dracoQuantBitsPosition > 30)
254
+ {
255
+ Napi::RangeError::New(env, "output.draco.quantizationBitsPosition must be in [1..30]")
256
+ .ThrowAsJavaScriptException();
257
+ return env.Null();
258
+ }
259
+ if (output.dracoQuantBitsNormal < 1 || output.dracoQuantBitsNormal > 30)
260
+ {
261
+ Napi::RangeError::New(env, "output.draco.quantizationBitsNormal must be in [1..30]")
262
+ .ThrowAsJavaScriptException();
263
+ return env.Null();
264
+ }
265
+
266
+ // Build variants list (multi-output) or fallback to single outputPath.
267
+ std::vector<ConvertWorker::VariantOptions> variants;
268
+ if (options.Has("variants"))
269
+ {
270
+ if (!options.Get("variants").IsArray())
271
+ {
272
+ Napi::TypeError::New(env, "options.variants must be an array").ThrowAsJavaScriptException();
273
+ return env.Null();
274
+ }
275
+ Napi::Array arr = options.Get("variants").As<Napi::Array>();
276
+ const uint32_t n = arr.Length();
277
+ if (n == 0)
278
+ {
279
+ Napi::RangeError::New(env, "options.variants must be non-empty").ThrowAsJavaScriptException();
280
+ return env.Null();
281
+ }
282
+ variants.reserve(n);
283
+ for (uint32_t i = 0; i < n; ++i)
284
+ {
285
+ if (!arr.Get(i).IsObject())
286
+ {
287
+ Napi::TypeError::New(env, "variants[i] must be an object").ThrowAsJavaScriptException();
288
+ return env.Null();
289
+ }
290
+ Napi::Object v = arr.Get(i).As<Napi::Object>();
291
+ if (!v.Has("outputPath") || !v.Get("outputPath").IsString())
292
+ {
293
+ Napi::TypeError::New(env, "variants[i].outputPath must be a string").ThrowAsJavaScriptException();
294
+ return env.Null();
295
+ }
296
+
297
+ ConvertWorker::VariantOptions vo;
298
+ vo.outputPath = v.Get("outputPath").As<Napi::String>().Utf8Value();
299
+ vo.tess = tess;
300
+ vo.filter = filter;
301
+ vo.output = output;
302
+
303
+ if (v.Has("tessellation") && v.Get("tessellation").IsObject())
304
+ {
305
+ Napi::Object t = v.Get("tessellation").As<Napi::Object>();
306
+ if (t.Has("linearDeflection") && t.Get("linearDeflection").IsNumber())
307
+ {
308
+ vo.tess.linearDeflection = t.Get("linearDeflection").As<Napi::Number>().DoubleValue();
309
+ }
310
+ if (t.Has("angularDeflection") && t.Get("angularDeflection").IsNumber())
311
+ {
312
+ vo.tess.angularDeflection = t.Get("angularDeflection").As<Napi::Number>().DoubleValue();
313
+ }
314
+ if (t.Has("smoothNormals") && t.Get("smoothNormals").IsBoolean())
315
+ {
316
+ vo.tess.smoothNormals = t.Get("smoothNormals").As<Napi::Boolean>().Value();
317
+ }
318
+ if (t.Has("normalCreaseAngle") && t.Get("normalCreaseAngle").IsNumber())
319
+ {
320
+ vo.tess.normalCreaseAngle = t.Get("normalCreaseAngle").As<Napi::Number>().DoubleValue();
321
+ }
322
+ if (vo.tess.linearDeflection <= 0.0)
323
+ {
324
+ Napi::RangeError::New(env, "variants[i].tessellation.linearDeflection must be > 0")
325
+ .ThrowAsJavaScriptException();
326
+ return env.Null();
327
+ }
328
+ if (vo.tess.angularDeflection <= 0.0)
329
+ {
330
+ Napi::RangeError::New(env, "variants[i].tessellation.angularDeflection must be > 0")
331
+ .ThrowAsJavaScriptException();
332
+ return env.Null();
333
+ }
334
+ if (vo.tess.normalCreaseAngle < 0.0)
335
+ {
336
+ Napi::RangeError::New(env, "variants[i].tessellation.normalCreaseAngle must be >= 0")
337
+ .ThrowAsJavaScriptException();
338
+ return env.Null();
339
+ }
340
+ }
341
+
342
+ if (v.Has("filter") && v.Get("filter").IsObject())
343
+ {
344
+ Napi::Object f = v.Get("filter").As<Napi::Object>();
345
+ if (f.Has("minBBoxDiagonal") && f.Get("minBBoxDiagonal").IsNumber())
346
+ {
347
+ vo.filter.minBBoxDiagonal = f.Get("minBBoxDiagonal").As<Napi::Number>().DoubleValue();
348
+ }
349
+ if (vo.filter.minBBoxDiagonal < 0.0)
350
+ {
351
+ Napi::RangeError::New(env, "variants[i].filter.minBBoxDiagonal must be >= 0")
352
+ .ThrowAsJavaScriptException();
353
+ return env.Null();
354
+ }
355
+ }
356
+
357
+ if (v.Has("output") && v.Get("output").IsObject())
358
+ {
359
+ Napi::Object o = v.Get("output").As<Napi::Object>();
360
+ // Reuse the same parsing logic by applying overrides in the same order.
361
+ if (o.Has("unit") && o.Get("unit").IsString())
362
+ {
363
+ const std::string unit = o.Get("unit").As<Napi::String>().Utf8Value();
364
+ if (unit == "m") vo.output.unitScale = 0.001;
365
+ else if (unit == "cm") vo.output.unitScale = 0.1;
366
+ else if (unit == "mm") vo.output.unitScale = 1.0;
367
+ else
368
+ {
369
+ Napi::RangeError::New(env, "variants[i].output.unit must be one of: 'm', 'cm', 'mm'")
370
+ .ThrowAsJavaScriptException();
371
+ return env.Null();
372
+ }
373
+ }
374
+ if (o.Has("unitScale") && o.Get("unitScale").IsNumber())
375
+ {
376
+ vo.output.unitScale = o.Get("unitScale").As<Napi::Number>().DoubleValue();
377
+ }
378
+ if (vo.output.unitScale <= 0.0)
379
+ {
380
+ Napi::RangeError::New(env, "variants[i].output.unitScale must be > 0").ThrowAsJavaScriptException();
381
+ return env.Null();
382
+ }
383
+
384
+ if (o.Has("bakeTransforms") && o.Get("bakeTransforms").IsBoolean())
385
+ {
386
+ vo.output.bakeTransforms = o.Get("bakeTransforms").As<Napi::Boolean>().Value();
387
+ }
388
+
389
+ if (o.Has("center") && o.Get("center").IsString())
390
+ {
391
+ const std::string center = o.Get("center").As<Napi::String>().Utf8Value();
392
+ if (center == "none") vo.output.center = ConvertWorker::OutputOptions::CenterMode::None;
393
+ else if (center == "xy") vo.output.center = ConvertWorker::OutputOptions::CenterMode::XY;
394
+ else if (center == "xz") vo.output.center = ConvertWorker::OutputOptions::CenterMode::XZ;
395
+ else if (center == "yz") vo.output.center = ConvertWorker::OutputOptions::CenterMode::YZ;
396
+ else
397
+ {
398
+ Napi::RangeError::New(env, "variants[i].output.center must be one of: 'none', 'xy', 'xz', 'yz'")
399
+ .ThrowAsJavaScriptException();
400
+ return env.Null();
401
+ }
402
+ }
403
+
404
+ if (o.Has("zUp") && o.Get("zUp").IsBoolean())
405
+ {
406
+ vo.output.zUp = o.Get("zUp").As<Napi::Boolean>().Value();
407
+ }
408
+ if (o.Has("zup") && o.Get("zup").IsBoolean())
409
+ {
410
+ vo.output.zUp = o.Get("zup").As<Napi::Boolean>().Value();
411
+ }
412
+
413
+ if (o.Has("draco"))
414
+ {
415
+ const Napi::Value dv = o.Get("draco");
416
+ if (dv.IsBoolean())
417
+ {
418
+ vo.output.dracoEnabled = dv.As<Napi::Boolean>().Value();
419
+ }
420
+ else if (dv.IsObject())
421
+ {
422
+ Napi::Object d = dv.As<Napi::Object>();
423
+ if (d.Has("enabled") && d.Get("enabled").IsBoolean())
424
+ {
425
+ vo.output.dracoEnabled = d.Get("enabled").As<Napi::Boolean>().Value();
426
+ }
427
+ else
428
+ {
429
+ vo.output.dracoEnabled = true;
430
+ }
431
+ if (d.Has("compressionLevel") && d.Get("compressionLevel").IsNumber())
432
+ {
433
+ vo.output.dracoCompressionLevel = d.Get("compressionLevel").As<Napi::Number>().Int32Value();
434
+ }
435
+ if (d.Has("quantizationBitsPosition") && d.Get("quantizationBitsPosition").IsNumber())
436
+ {
437
+ vo.output.dracoQuantBitsPosition = d.Get("quantizationBitsPosition").As<Napi::Number>().Int32Value();
438
+ }
439
+ if (d.Has("quantizationBitsNormal") && d.Get("quantizationBitsNormal").IsNumber())
440
+ {
441
+ vo.output.dracoQuantBitsNormal = d.Get("quantizationBitsNormal").As<Napi::Number>().Int32Value();
442
+ }
443
+ }
444
+ else
445
+ {
446
+ Napi::TypeError::New(env, "variants[i].output.draco must be boolean or object")
447
+ .ThrowAsJavaScriptException();
448
+ return env.Null();
449
+ }
450
+ }
451
+
452
+ if (vo.output.dracoCompressionLevel < 0 || vo.output.dracoCompressionLevel > 10)
453
+ {
454
+ Napi::RangeError::New(env, "variants[i].output.draco.compressionLevel must be in [0..10]")
455
+ .ThrowAsJavaScriptException();
456
+ return env.Null();
457
+ }
458
+ if (vo.output.dracoQuantBitsPosition < 1 || vo.output.dracoQuantBitsPosition > 30)
459
+ {
460
+ Napi::RangeError::New(env, "variants[i].output.draco.quantizationBitsPosition must be in [1..30]")
461
+ .ThrowAsJavaScriptException();
462
+ return env.Null();
463
+ }
464
+ if (vo.output.dracoQuantBitsNormal < 1 || vo.output.dracoQuantBitsNormal > 30)
465
+ {
466
+ Napi::RangeError::New(env, "variants[i].output.draco.quantizationBitsNormal must be in [1..30]")
467
+ .ThrowAsJavaScriptException();
468
+ return env.Null();
469
+ }
470
+ }
471
+
472
+ variants.push_back(std::move(vo));
473
+ }
474
+ }
475
+ else
476
+ {
477
+ if (!options.Has("outputPath") || !options.Get("outputPath").IsString())
478
+ {
479
+ Napi::TypeError::New(env, "options.outputPath must be a string").ThrowAsJavaScriptException();
480
+ return env.Null();
481
+ }
482
+ ConvertWorker::VariantOptions vo;
483
+ vo.outputPath = options.Get("outputPath").As<Napi::String>().Utf8Value();
484
+ vo.tess = tess;
485
+ vo.filter = filter;
486
+ vo.output = output;
487
+ variants.push_back(std::move(vo));
488
+ }
489
+
490
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
491
+ auto* worker = new ConvertWorker(env, deferred, std::move(inputPath), logLevel, std::move(variants));
492
+ worker->Queue();
493
+
494
+ return deferred.Promise();
495
+ }
496
+
497
+ static Napi::Object Init(Napi::Env env, Napi::Object exports)
498
+ {
499
+ exports.Set("convertSTEPToGLTF", Napi::Function::New(env, ConvertSTEPToGLTF));
500
+ return exports;
501
+ }
502
+
503
+ NODE_API_MODULE(occt_gltf_addon, Init)
504
+