occt-gltf-addon 0.1.13 → 0.1.14

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/CMakeLists.txt CHANGED
@@ -127,7 +127,17 @@ add_library(${PROJECT_NAME} SHARED
127
127
  )
128
128
 
129
129
  # --- Optional: Draco (KHR_draco_mesh_compression) ---
130
- option(OCCT_GLTF_ADDON_ENABLE_DRACO "Enable Draco mesh compression support" ON)
130
+ # Default to OFF to avoid linking native Draco (which may require newer glibc).
131
+ option(OCCT_GLTF_ADDON_ENABLE_DRACO "Enable Draco mesh compression support" OFF)
132
+ if(DEFINED ENV{OCCT_GLTF_ADDON_ENABLE_DRACO})
133
+ string(TOLOWER "$ENV{OCCT_GLTF_ADDON_ENABLE_DRACO}" _draco_env)
134
+ if(_draco_env STREQUAL "1" OR _draco_env STREQUAL "on" OR _draco_env STREQUAL "true" OR _draco_env STREQUAL "yes")
135
+ set(OCCT_GLTF_ADDON_ENABLE_DRACO ON CACHE BOOL "Enable Draco mesh compression support" FORCE)
136
+ else()
137
+ set(OCCT_GLTF_ADDON_ENABLE_DRACO OFF CACHE BOOL "Enable Draco mesh compression support" FORCE)
138
+ endif()
139
+ endif()
140
+
131
141
  if(OCCT_GLTF_ADDON_ENABLE_DRACO)
132
142
  if(DEFINED ENV{DRACO_ROOT} AND NOT "$ENV{DRACO_ROOT}" STREQUAL "")
133
143
  set(DRACO_ROOT "$ENV{DRACO_ROOT}" CACHE PATH "Draco install prefix" FORCE)
package/README.md CHANGED
@@ -45,6 +45,12 @@ await convertSTEPToGLTF({
45
45
  export OCCT_GLTF_ADDON_PREBUILT_BASE_URL=https://cdn2-1304552240.cos.ap-shanghai.myqcloud.com/libs/occt/
46
46
  ```
47
47
 
48
+ 运行时自检(兼容安装脚本被禁用的场景):
49
+ - 首次调用时若本地没有对应 `prebuilt/<platform>-<arch>/`,会尝试在运行时下载预编译包并缓存(优先写入包内 `node_modules/occt-gltf-addon/prebuilt/`,不可写时回退到 `/tmp/occt-gltf-addon/prebuilt`)。
50
+ - 可设置 `OCCT_GLTF_ADDON_RUNTIME_DOWNLOAD=0` 关闭运行时下载。
51
+ - 可设置 `OCCT_GLTF_ADDON_PREBUILT_CACHE_DIR=/tmp/occt-gltf-addon/prebuilt` 自定义缓存目录。
52
+ - 可设置 `OCCT_GLTF_ADDON_PREBUILT_ROOT` 或 `OCCT_GLTF_ADDON_PREBUILT_DIR` 指定本地预编译包位置。
53
+
48
54
  本地编译时通过环境变量指定 OCCT 安装前缀:
49
55
 
50
56
  ```bash
@@ -70,8 +76,9 @@ OCCT_GLTF_ADDON_SKIP_BUILD=1 npm i occt-gltf-addon
70
76
  OCCT_GLTF_ADDON_SKIP_DOWNLOAD=1 npm i occt-gltf-addon
71
77
  ```
72
78
 
73
- Draco(仅源码编译时启用,需要开发包):
74
- - `brew install draco` / `apt-get install libdraco-dev`
79
+ Draco(Node.js 层压缩,避免原生依赖):
80
+ - 默认不链接原生 Draco;当设置 `output.draco` 时,会在 JS 层使用 `gltf-pipeline` + `draco3d` 压缩。
81
+ - 如需恢复原生 Draco(可能引入 glibc 兼容问题),构建时设置:`OCCT_GLTF_ADDON_ENABLE_DRACO=1`
75
82
 
76
83
  ---
77
84
 
@@ -104,7 +111,7 @@ Draco(仅源码编译时启用,需要开发包):
104
111
  - `bakeTransforms`:烘焙节点变换到顶点
105
112
  - `center`:`'none'|'xy'|'xz'|'yz'`(配合 bake 使用)
106
113
  - `zUp`:导出为 Z-up 坐标(非标准 glTF)
107
- - `draco`:`boolean | { enabled, compressionLevel, quantizationBitsPosition, quantizationBitsNormal }`
114
+ - `draco`:`boolean | { enabled, compressionLevel, quantizationBitsPosition, quantizationBitsNormal }`(Node.js 层 Draco 压缩)
108
115
 
109
116
  ---
110
117
 
package/example.js CHANGED
@@ -40,8 +40,8 @@ async function main() {
40
40
  // If your runtime uses Z-up world (e.g. three.js: THREE.Object3D.DEFAULT_UP.set(0,0,1)),
41
41
  // enable this to export Z-up coordinates (note: non-standard glTF; Y-up viewers may show it rotated).
42
42
  zUp: true,
43
- // Draco (KHR_draco_mesh_compression). NOTE: some importers (e.g. Blender) may not support Draco.
44
- // draco: { enabled: true, compressionLevel: 7, quantizationBitsPosition: 14, quantizationBitsNormal: 10 },
43
+ // Draco (KHR_draco_mesh_compression) is applied in JS after conversion to avoid native deps.
44
+ // NOTE: some importers (e.g. Blender) may not support Draco.
45
45
  draco: {
46
46
  enabled: true,
47
47
  compressionLevel: 7, // 0..10 (越大越小/越慢)
package/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // CommonJS entrypoint. cmake-js outputs: build/<Config>/<target>.node
2
2
  const fs = require('fs');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const os = require('os');
3
6
  const path = require('path');
7
+ const tar = require('tar');
4
8
 
5
9
  function prependEnvPath(key, dir) {
6
10
  if (!dir) return;
@@ -41,12 +45,100 @@ function setOcctResourceEnv(resRoot) {
41
45
  setIfEmpty('CSF_MIGRATION_TYPES', path.join(R, 'StdResource', 'MigrationSheet.txt'));
42
46
  }
43
47
 
48
+ function normalizeBaseUrl(base) {
49
+ if (!base) return '';
50
+ return base.endsWith('/') ? base : `${base}/`;
51
+ }
52
+
53
+ function getPrebuiltUrl(tag, version) {
54
+ const base =
55
+ process.env.OCCT_GLTF_ADDON_PREBUILT_BASE_URL ||
56
+ 'https://cdn2-1304552240.cos.ap-shanghai.myqcloud.com/libs/occt/';
57
+ const normalized = normalizeBaseUrl(base);
58
+ return `${normalized}occt-gltf-addon-${version}-${tag}.tgz`;
59
+ }
60
+
61
+ function getPackageVersion() {
62
+ try {
63
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
64
+ return pkg.version || '0.0.0';
65
+ } catch (e) {
66
+ return '0.0.0';
67
+ }
68
+ }
69
+
70
+ function isWritableDir(dir) {
71
+ try {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ fs.accessSync(dir, fs.constants.W_OK);
74
+ return true;
75
+ } catch (e) {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ function resolvePrebuiltDirCandidates(baseDir, tag) {
81
+ if (!baseDir) return [];
82
+ const candidates = [baseDir];
83
+ if (path.basename(baseDir) !== tag) {
84
+ candidates.push(path.join(baseDir, tag));
85
+ }
86
+ return candidates;
87
+ }
88
+
89
+ function getPrebuiltSearchDirs(tag) {
90
+ const dirs = [];
91
+ const fromDir = process.env.OCCT_GLTF_ADDON_PREBUILT_DIR;
92
+ const fromRoot = process.env.OCCT_GLTF_ADDON_PREBUILT_ROOT;
93
+ const cacheRoot =
94
+ process.env.OCCT_GLTF_ADDON_PREBUILT_CACHE_DIR ||
95
+ path.join(os.tmpdir(), 'occt-gltf-addon', 'prebuilt');
96
+
97
+ resolvePrebuiltDirCandidates(fromDir, tag).forEach((d) => dirs.push(d));
98
+ if (fromRoot) dirs.push(path.join(fromRoot, tag));
99
+ dirs.push(path.join(cacheRoot, tag));
100
+ dirs.push(path.join(__dirname, 'prebuilt', tag));
101
+
102
+ return [...new Set(dirs)];
103
+ }
104
+
105
+ function resolveDownloadPrebuiltDir(tag) {
106
+ const candidates = [];
107
+ const fromDir = process.env.OCCT_GLTF_ADDON_PREBUILT_DIR;
108
+ const fromRoot = process.env.OCCT_GLTF_ADDON_PREBUILT_ROOT;
109
+ const cacheRoot =
110
+ process.env.OCCT_GLTF_ADDON_PREBUILT_CACHE_DIR ||
111
+ path.join(os.tmpdir(), 'occt-gltf-addon', 'prebuilt');
112
+
113
+ resolvePrebuiltDirCandidates(fromDir, tag).forEach((d) => candidates.push(d));
114
+ if (fromRoot) candidates.push(path.join(fromRoot, tag));
115
+ candidates.push(path.join(__dirname, 'prebuilt', tag));
116
+ candidates.push(path.join(cacheRoot, tag));
117
+
118
+ for (const dir of candidates) {
119
+ if (isWritableDir(dir)) return dir;
120
+ }
121
+
122
+ return path.join(cacheRoot, tag);
123
+ }
124
+
125
+ function findPrebuiltNode(tag) {
126
+ const candidates = getPrebuiltSearchDirs(tag);
127
+ for (const prebuiltDir of candidates) {
128
+ const prebuiltNode = path.join(prebuiltDir, 'occt_gltf_addon.node');
129
+ if (fs.existsSync(prebuiltNode)) {
130
+ return { prebuiltDir, prebuiltNode };
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
44
136
  function tryLoadPrebuilt() {
45
137
  if (process.env.OCCT_GLTF_ADDON_BINDING_PATH) return null;
46
138
  const tag = `${process.platform}-${process.arch}`;
47
- const prebuiltDir = path.join(__dirname, 'prebuilt', tag);
48
- const prebuiltNode = path.join(prebuiltDir, 'occt_gltf_addon.node');
49
- if (!fs.existsSync(prebuiltNode)) return null;
139
+ const found = findPrebuiltNode(tag);
140
+ if (!found) return null;
141
+ const { prebuiltDir, prebuiltNode } = found;
50
142
 
51
143
  const libDir = path.join(prebuiltDir, 'lib');
52
144
  const resDir = path.join(prebuiltDir, 'resources');
@@ -76,44 +168,224 @@ function tryLoadPrebuilt() {
76
168
  return require(prebuiltNode);
77
169
  }
78
170
 
79
- const prebuilt = tryLoadPrebuilt();
80
- if (prebuilt) {
81
- module.exports = prebuilt;
82
- return;
171
+ function downloadFile(url, dest) {
172
+ return new Promise((resolve, reject) => {
173
+ const client = url.startsWith('https:') ? https : http;
174
+ const req = client.get(url, (res) => {
175
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
176
+ downloadFile(res.headers.location, dest).then(resolve).catch(reject);
177
+ return;
178
+ }
179
+ if (res.statusCode !== 200) {
180
+ reject(new Error(`Download failed with status ${res.statusCode}`));
181
+ res.resume();
182
+ return;
183
+ }
184
+ const file = fs.createWriteStream(dest);
185
+ res.pipe(file);
186
+ file.on('finish', () => file.close(resolve));
187
+ file.on('error', reject);
188
+ });
189
+ req.on('error', reject);
190
+ });
83
191
  }
84
192
 
85
- const overridePath = process.env.OCCT_GLTF_ADDON_BINDING_PATH;
86
- const candidates = overridePath
87
- ? [overridePath]
88
- : [
89
- path.join(__dirname, 'build', 'Release', 'occt_gltf_addon.node'),
90
- path.join(__dirname, 'build', 'RelWithDebInfo', 'occt_gltf_addon.node'),
91
- path.join(__dirname, 'build', 'Debug', 'occt_gltf_addon.node'),
92
- ];
193
+ async function ensurePrebuiltDownloaded(tag) {
194
+ if (process.env.OCCT_GLTF_ADDON_SKIP_DOWNLOAD === '1') return null;
195
+ if (process.env.OCCT_GLTF_ADDON_RUNTIME_DOWNLOAD === '0') return null;
196
+
197
+ const existing = findPrebuiltNode(tag);
198
+ if (existing) return existing.prebuiltDir;
93
199
 
94
- let lastErr = null;
95
- for (const p of candidates) {
200
+ const prebuiltDir = resolveDownloadPrebuiltDir(tag);
201
+ const parentDir = path.dirname(prebuiltDir);
202
+ const prebuiltNode = path.join(prebuiltDir, 'occt_gltf_addon.node');
203
+
204
+ if (fs.existsSync(prebuiltNode)) return prebuiltDir;
205
+
206
+ const version = getPackageVersion();
207
+ const url = getPrebuiltUrl(tag, version);
208
+ const tgzPath = path.join(parentDir, `prebuilt-${tag}.tgz`);
209
+
210
+ fs.mkdirSync(prebuiltDir, { recursive: true });
96
211
  try {
97
- if (!fs.existsSync(p)) continue;
98
- // eslint-disable-next-line import/no-dynamic-require, global-require
99
- module.exports = require(p);
100
- return;
212
+ await downloadFile(url, tgzPath);
213
+ await tar.x({ file: tgzPath, cwd: prebuiltDir });
214
+ fs.rmSync(tgzPath, { force: true });
215
+ return prebuiltDir;
101
216
  } catch (e) {
102
- lastErr = e;
217
+ fs.rmSync(tgzPath, { force: true });
218
+ return null;
103
219
  }
104
220
  }
105
221
 
106
- const msg = [
107
- '[occt-gltf-addon] Failed to load native addon (.node).',
108
- `Tried: ${candidates.map((p) => JSON.stringify(p)).join(', ')}`,
109
- '',
110
- 'If you installed from npm:',
111
- '- This package bundles prebuilt binaries under `prebuilt/<platform>-<arch>/` for supported platforms.',
112
- '- If prebuilt is missing for your platform, build from source with OCCT_ROOT set.',
113
- '',
114
- "You can try: 'npm rebuild occt-gltf-addon' (with OCCT_ROOT exported).",
115
- ].join('\n');
222
+ function loadNativeAddon() {
223
+ const prebuilt = tryLoadPrebuilt();
224
+ if (prebuilt) return prebuilt;
225
+
226
+ const overridePath = process.env.OCCT_GLTF_ADDON_BINDING_PATH;
227
+ const candidates = overridePath
228
+ ? [overridePath]
229
+ : [
230
+ path.join(__dirname, 'build', 'Release', 'occt_gltf_addon.node'),
231
+ path.join(__dirname, 'build', 'RelWithDebInfo', 'occt_gltf_addon.node'),
232
+ path.join(__dirname, 'build', 'Debug', 'occt_gltf_addon.node'),
233
+ ];
234
+
235
+ let lastErr = null;
236
+ for (const p of candidates) {
237
+ try {
238
+ if (!fs.existsSync(p)) continue;
239
+ // eslint-disable-next-line import/no-dynamic-require, global-require
240
+ return require(p);
241
+ } catch (e) {
242
+ lastErr = e;
243
+ }
244
+ }
245
+
246
+ const msg = [
247
+ '[occt-gltf-addon] Failed to load native addon (.node).',
248
+ `Tried: ${candidates.map((p) => JSON.stringify(p)).join(', ')}`,
249
+ '',
250
+ 'If you installed from npm:',
251
+ '- This package bundles prebuilt binaries under `prebuilt/<platform>-<arch>/` for supported platforms.',
252
+ '- If prebuilt is missing for your platform, build from source with OCCT_ROOT set.',
253
+ '',
254
+ "You can try: 'npm rebuild occt-gltf-addon' (with OCCT_ROOT exported).",
255
+ ].join('\n');
256
+
257
+ // Preserve original error for debugging.
258
+ throw new Error(`${msg}\n\n${String(lastErr && (lastErr.stack || lastErr.message || lastErr))}`);
259
+ }
260
+
261
+ function normalizeDracoOptions(draco) {
262
+ if (draco == null) return null;
263
+ if (typeof draco === 'boolean') return draco ? {} : null;
264
+ if (typeof draco !== 'object') return null;
265
+ if (draco.enabled === false) return null;
266
+ return { ...draco, enabled: true };
267
+ }
268
+
269
+ function stripDraco(output) {
270
+ if (!output || typeof output !== 'object') return output;
271
+ const { draco, ...rest } = output;
272
+ return rest;
273
+ }
274
+
275
+ function splitDracoOptions(options) {
276
+ const dracoJobs = [];
277
+ if (!options || typeof options !== 'object') {
278
+ return { nativeOptions: options, dracoJobs };
279
+ }
280
+
281
+ if (Array.isArray(options.variants) && options.variants.length > 0) {
282
+ const variants = options.variants.map((variant) => {
283
+ if (!variant || typeof variant !== 'object') return variant;
284
+ const draco = normalizeDracoOptions(variant.output && variant.output.draco);
285
+ if (!draco) return variant;
286
+ if (!variant.outputPath) {
287
+ throw new Error('[occt-gltf-addon] outputPath is required for Draco compression (variant).');
288
+ }
289
+ dracoJobs.push({ outputPath: variant.outputPath, draco });
290
+ return { ...variant, output: stripDraco(variant.output) };
291
+ });
292
+ return { nativeOptions: { ...options, variants }, dracoJobs };
293
+ }
294
+
295
+ const draco = normalizeDracoOptions(options.output && options.output.draco);
296
+ if (!draco) {
297
+ return { nativeOptions: options, dracoJobs };
298
+ }
299
+ if (!options.outputPath) {
300
+ throw new Error('[occt-gltf-addon] outputPath is required for Draco compression.');
301
+ }
302
+ dracoJobs.push({ outputPath: options.outputPath, draco });
303
+ return { nativeOptions: { ...options, output: stripDraco(options.output) }, dracoJobs };
304
+ }
305
+
306
+ function toGltfPipelineDracoOptions(draco) {
307
+ const options = {};
308
+ if (!draco || typeof draco !== 'object') return options;
309
+ if (typeof draco.compressionLevel === 'number') {
310
+ options.compressionLevel = draco.compressionLevel;
311
+ }
312
+ if (typeof draco.quantizationBitsPosition === 'number') {
313
+ options.quantizePositionBits = draco.quantizationBitsPosition;
314
+ }
315
+ if (typeof draco.quantizationBitsNormal === 'number') {
316
+ options.quantizeNormalBits = draco.quantizationBitsNormal;
317
+ }
318
+ return options;
319
+ }
320
+
321
+ async function compressGlbWithDraco({ outputPath, draco }) {
322
+ if (!outputPath) {
323
+ throw new Error('[occt-gltf-addon] Draco compression requires outputPath.');
324
+ }
325
+
326
+ let gltfPipeline;
327
+ try {
328
+ // eslint-disable-next-line global-require
329
+ gltfPipeline = require('gltf-pipeline');
330
+ } catch (e) {
331
+ throw new Error(
332
+ `[occt-gltf-addon] Draco compression requested but "gltf-pipeline" is not installed.\n` +
333
+ "Install it with: npm i gltf-pipeline",
334
+ );
335
+ }
336
+
337
+ const input = await fs.promises.readFile(outputPath);
338
+ const result = await gltfPipeline.processGlb(input, {
339
+ dracoOptions: toGltfPipelineDracoOptions(draco),
340
+ });
341
+
342
+ if (!result || !result.glb) {
343
+ throw new Error('[occt-gltf-addon] Draco compression failed: empty output.');
344
+ }
345
+
346
+ await fs.promises.writeFile(outputPath, result.glb);
347
+ }
348
+
349
+ async function runNodeDracoCompression(jobs) {
350
+ for (const job of jobs) {
351
+ // eslint-disable-next-line no-await-in-loop
352
+ await compressGlbWithDraco(job);
353
+ }
354
+ }
355
+
356
+ function wrapNative(native) {
357
+ if (!native || typeof native.convertSTEPToGLTF !== 'function') return native;
358
+ const convert = native.convertSTEPToGLTF.bind(native);
359
+ return {
360
+ ...native,
361
+ convertSTEPToGLTF: async (options) => {
362
+ const { nativeOptions, dracoJobs } = splitDracoOptions(options || {});
363
+ await convert(nativeOptions);
364
+ if (dracoJobs.length > 0) {
365
+ await runNodeDracoCompression(dracoJobs);
366
+ }
367
+ },
368
+ };
369
+ }
370
+
371
+ let wrappedNativePromise = null;
372
+
373
+ async function getWrappedNative() {
374
+ if (!wrappedNativePromise) {
375
+ wrappedNativePromise = (async () => {
376
+ const tag = `${process.platform}-${process.arch}`;
377
+ await ensurePrebuiltDownloaded(tag);
378
+ const native = loadNativeAddon();
379
+ return wrapNative(native);
380
+ })();
381
+ }
382
+ return wrappedNativePromise;
383
+ }
116
384
 
117
- // Preserve original error for debugging.
118
- throw new Error(`${msg}\n\n${String(lastErr && (lastErr.stack || lastErr.message || lastErr))}`);
385
+ module.exports = {
386
+ convertSTEPToGLTF: async (options) => {
387
+ const native = await getWrappedNative();
388
+ return native.convertSTEPToGLTF(options);
389
+ },
390
+ };
119
391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "occt-gltf-addon",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "STEP (.step/.stp) to glTF (GLB) converter using OpenCascade (OCCT) via a Node.js N-API addon",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -70,6 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "cmake-js": "^7.3.1",
73
+ "gltf-pipeline": "^4.3.0",
73
74
  "node-addon-api": "^7.1.0",
74
75
  "tar": "^7.4.0"
75
76
  },