occt-gltf-addon 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -2,22 +2,25 @@
2
2
 
3
3
  Node.js N-API 原生扩展:使用 OpenCascade (OCCT) 将 **STEP (.step/.stp)** 转换为 **glTF 2.0 (GLB)**。
4
4
 
5
- #### 安装(从 npm 安装会本地编译)
5
+ #### 安装
6
6
 
7
- 这个包默认会在 `npm install` 时执行本地编译(`cmake-js`)。
7
+ - **Linux x64(Ubuntu 等)**:默认会安装预编译包 `occt-gltf-addon-linux-x64`(包含 OCCT runtime),一般 **不需要系统安装 OCCT**。
8
+ - **macOS / 其他平台**:默认会在 `npm install` 时本地编译(`cmake-js`),需要 OCCT 开发文件(headers + libs)。
8
9
 
9
- 你需要:
10
- - **CMake + C++ 编译器 + Python3**
11
- - **OCCT 安装**(共享库 + headers)
12
-
13
- 通过环境变量指定 OCCT 安装前缀:
10
+ 本地编译时通过环境变量指定 OCCT 安装前缀:
14
11
 
15
12
  ```bash
16
13
  export OCCT_ROOT=/path/to/occt/prefix
17
14
  npm i occt-gltf-addon
18
15
  ```
19
16
 
20
- 可选(启用 Draco 压缩需要开发包):
17
+ 如需在 linux-x64 **强制源码编译**(不推荐,且需要你自己装 OCCT):
18
+
19
+ ```bash
20
+ OCCT_GLTF_ADDON_FORCE_BUILD=1 OCCT_ROOT=/path/to/occt/prefix npm i occt-gltf-addon
21
+ ```
22
+
23
+ 可选(仅在 **源码编译** 时用于启用 Draco 支持,需要开发包):
21
24
  - `brew install draco` / `apt-get install libdraco-dev`
22
25
 
23
26
  如需跳过编译(不推荐,运行时会找不到 `.node`):
@@ -55,6 +58,19 @@ await convertSTEPToGLTF({
55
58
 
56
59
  ---
57
60
 
61
+ #### Smoke Test(安装后快速验证)
62
+
63
+ ```bash
64
+ npm run smoke -- /abs/path/to/model.step /tmp/occt-gltf-out --draco --zup
65
+ ```
66
+
67
+ 参数:
68
+ - `--draco`:启用 Draco 输出(可选)
69
+ - `--zup`:导出 Z-up(用于 three.js Z-up 世界;可选)
70
+ - `--quiet`:静默日志(可选)
71
+
72
+ ---
73
+
58
74
  #### 关键选项说明(简版)
59
75
 
60
76
  - **tessellation.linearDeflection / angularDeflection**:B-Rep 三角化精度
package/index.js CHANGED
@@ -40,9 +40,10 @@ const msg = [
40
40
  '[occt-gltf-addon] Failed to load native addon (.node).',
41
41
  `Tried: ${candidates.map((p) => JSON.stringify(p)).join(', ')}`,
42
42
  '',
43
- 'If you installed from npm, ensure native build succeeded:',
44
- '- CMake + C++ toolchain are installed',
45
- "- OCCT is installed and OCCT_ROOT points to its install prefix (e.g. '/opt/homebrew', '/usr', '/usr/local', or your custom install)",
43
+ 'If you installed from npm:',
44
+ '- On linux-x64: you should get the prebuilt optional dependency `occt-gltf-addon-linux-x64` (no system OCCT required).',
45
+ ' Ensure optional deps are enabled (do NOT use `--no-optional`).',
46
+ '- Otherwise / if building from source: ensure CMake + C++ toolchain are installed, and OCCT_ROOT points to your OCCT install prefix',
46
47
  '',
47
48
  "You can try: 'npm rebuild occt-gltf-addon' (with OCCT_ROOT exported).",
48
49
  ].join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "occt-gltf-addon",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -46,7 +46,8 @@
46
46
  "CMakeLists.txt",
47
47
  "src/**",
48
48
  "scripts/**",
49
- "example.js"
49
+ "example.js",
50
+ "smoke_test.js"
50
51
  ],
51
52
  "scripts": {
52
53
  "install": "node scripts/install.js",
@@ -54,6 +55,7 @@
54
55
  "clean": "rm -rf build",
55
56
  "rebuild": "npm run clean && npm run build",
56
57
  "example": "node example.js",
58
+ "smoke": "node smoke_test.js",
57
59
  "test": "node -e \"console.log('no tests')\"",
58
60
 
59
61
  "docker:build:linux-amd64": "bash docker/build-linux-amd64.sh",
@@ -61,7 +63,7 @@
61
63
  "docker:image:linux-amd64:nocache": "docker buildx build --no-cache --platform linux/amd64 -f docker/Dockerfile.linux-amd64 -t occt-addon-build:linux-amd64 ../.."
62
64
  },
63
65
  "optionalDependencies": {
64
- "occt-gltf-addon-linux-x64": "0.1.0"
66
+ "occt-gltf-addon-linux-x64": "0.1.2"
65
67
  },
66
68
  "dependencies": {
67
69
  "cmake-js": "^7.3.1",
@@ -43,8 +43,19 @@ function main() {
43
43
  note(`Using prebuilt binary from ${prebuiltName}; skipping local build.`);
44
44
  return;
45
45
  } catch (e) {
46
- // Prebuilt missing or not loadable (e.g. GLIBC mismatch). Fall back to local build.
47
- note(`Prebuilt package ${prebuiltName} not usable; falling back to local build.`);
46
+ // Prebuilt missing or not loadable (e.g. GLIBC mismatch).
47
+ // On Linux, the default expectation is to use the prebuilt package (no system OCCT required).
48
+ // Only fall back to local build when the user explicitly opts in.
49
+ const canBuildFromSource =
50
+ process.env.OCCT_GLTF_ADDON_FORCE_BUILD === '1' || !!process.env.OCCT_ROOT;
51
+ if (!canBuildFromSource) {
52
+ note(`Prebuilt package ${prebuiltName} not usable, and OCCT_ROOT is not set.`);
53
+ note('This package is intended to work on Linux via the prebuilt optional dependency.');
54
+ note('Fix: ensure optional dependencies are installed (do NOT use --no-optional).');
55
+ note('Or: set OCCT_ROOT and re-install to build from source (or set OCCT_GLTF_ADDON_FORCE_BUILD=1).');
56
+ process.exit(1);
57
+ }
58
+ note(`Prebuilt package ${prebuiltName} not usable; attempting local build from source...`);
48
59
  }
49
60
  }
50
61
 
@@ -53,15 +64,24 @@ function main() {
53
64
  note("OCCT_ROOT is not set. If build fails, set OCCT_ROOT to your OCCT install prefix (e.g. '/opt/homebrew', '/usr', '/usr/local', or your custom install).");
54
65
  }
55
66
 
56
- // Use the locally installed cmake-js (dependency).
57
- const cmakeJsBin = path.join(pkgRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'cmake-js.cmd' : 'cmake-js');
58
- if (!fs.existsSync(cmakeJsBin)) {
59
- note(`cmake-js not found at ${cmakeJsBin}. Try: 'npm install' again.`);
67
+ note('Building native addon with cmake-js...');
68
+
69
+ // Locate cmake-js CLI in a way that works with npm hoisting.
70
+ let cmakeJsCli = null;
71
+ try {
72
+ // eslint-disable-next-line global-require
73
+ cmakeJsCli = require.resolve('cmake-js/bin/cmake-js');
74
+ } catch (e) {
75
+ cmakeJsCli = null;
76
+ }
77
+
78
+ if (!cmakeJsCli || !fs.existsSync(cmakeJsCli)) {
79
+ note('cmake-js is not available (dependency missing).');
80
+ note("Try: 'npm install' again.");
60
81
  process.exit(1);
61
82
  }
62
83
 
63
- note('Building native addon with cmake-js...');
64
- run(cmakeJsBin, ['compile', '-O', 'build'], { cwd: pkgRoot, env: process.env });
84
+ run(process.execPath, [cmakeJsCli, 'compile', '-O', 'build'], { cwd: pkgRoot, env: process.env });
65
85
  }
66
86
 
67
87
  try {
package/smoke_test.js ADDED
@@ -0,0 +1,152 @@
1
+ /* eslint-disable no-console */
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function usage() {
7
+ console.error('Usage: node smoke_test.js <input.step> [outputDir] [--draco] [--zup] [--quiet]');
8
+ console.error('');
9
+ console.error('Notes:');
10
+ console.error('- Requires OCCT runtime libraries to be available at runtime (LD_LIBRARY_PATH / rpath / system install).');
11
+ console.error('- If you use three.js Z-up world (THREE.Object3D.DEFAULT_UP.set(0,0,1)), pass --zup.');
12
+ }
13
+
14
+ function readU32LE(buf, off) {
15
+ return buf.readUInt32LE(off);
16
+ }
17
+
18
+ function parseGlbHeaderAndJson(glbPath) {
19
+ const fd = fs.openSync(glbPath, 'r');
20
+ try {
21
+ const header = Buffer.alloc(12);
22
+ fs.readSync(fd, header, 0, 12, 0);
23
+ const magic = header.toString('ascii', 0, 4);
24
+ const version = readU32LE(header, 4);
25
+
26
+ if (magic !== 'glTF' || version !== 2) {
27
+ throw new Error(`Not a glTF 2.0 GLB: magic=${magic}, version=${version}`);
28
+ }
29
+
30
+ const chunk0 = Buffer.alloc(8);
31
+ fs.readSync(fd, chunk0, 0, 8, 12);
32
+ const jsonLen = readU32LE(chunk0, 0);
33
+ const jsonType = readU32LE(chunk0, 4);
34
+ if (jsonType !== 0x4e4f534a) {
35
+ throw new Error(`Invalid JSON chunk type: 0x${jsonType.toString(16)}`);
36
+ }
37
+
38
+ const jsonBuf = Buffer.alloc(jsonLen);
39
+ fs.readSync(fd, jsonBuf, 0, jsonLen, 20);
40
+ const jsonStr = jsonBuf.toString('utf8').replace(/\s+$/g, '');
41
+ const gltf = JSON.parse(jsonStr);
42
+ return { gltf };
43
+ } finally {
44
+ fs.closeSync(fd);
45
+ }
46
+ }
47
+
48
+ async function main() {
49
+ const argv = process.argv.slice(2);
50
+ const flags = new Set(argv.filter((a) => a.startsWith('--')));
51
+ const positional = argv.filter((a) => !a.startsWith('--'));
52
+
53
+ const inputPath = positional[0];
54
+ if (!inputPath) {
55
+ usage();
56
+ process.exit(1);
57
+ }
58
+
59
+ const outputDir = positional[1] || fs.mkdtempSync(path.join(os.tmpdir(), 'occt-gltf-addon-'));
60
+ fs.mkdirSync(outputDir, { recursive: true });
61
+
62
+ const enableDraco = flags.has('--draco');
63
+ const zUp = flags.has('--zup');
64
+ const quiet = flags.has('--quiet');
65
+
66
+ const baseName = path.parse(inputPath).name || 'out';
67
+ const outMin = path.join(outputDir, `${baseName}.min.glb`);
68
+ const outNormal = path.join(outputDir, `${baseName}.glb`);
69
+
70
+ // Require the package under test.
71
+ // In a consumer project, this should be: require('occt-gltf-addon')
72
+ // Here we use '.' so it works when running inside the package directory too.
73
+ // eslint-disable-next-line global-require, import/no-dynamic-require
74
+ const { convertSTEPToGLTF } = require('.');
75
+
76
+ const outputOpts = {
77
+ unit: 'm',
78
+ bakeTransforms: true,
79
+ center: zUp ? 'xy' : 'xz',
80
+ zUp,
81
+ draco: enableDraco
82
+ ? { enabled: true, compressionLevel: 7, quantizationBitsPosition: 14, quantizationBitsNormal: 10 }
83
+ : { enabled: false },
84
+ };
85
+
86
+ const variants = [
87
+ {
88
+ outputPath: outMin,
89
+ tessellation: { linearDeflection: 5, angularDeflection: Math.PI / 3 },
90
+ filter: { minBBoxDiagonal: 60 },
91
+ output: outputOpts,
92
+ },
93
+ {
94
+ outputPath: outNormal,
95
+ tessellation: { linearDeflection: 0.2, angularDeflection: 0.6, smoothNormals: true, normalCreaseAngle: Math.PI / 3 },
96
+ output: outputOpts,
97
+ },
98
+ ];
99
+
100
+ console.log('[smoke] input:', inputPath);
101
+ console.log('[smoke] outputDir:', outputDir);
102
+ console.log('[smoke] draco:', enableDraco);
103
+ console.log('[smoke] zUp:', zUp);
104
+
105
+ await convertSTEPToGLTF({
106
+ inputPath,
107
+ logLevel: quiet ? 'quiet' : 'info',
108
+ variants,
109
+ });
110
+
111
+ for (const p of [outMin, outNormal]) {
112
+ if (!fs.existsSync(p)) {
113
+ throw new Error(`Missing output file: ${p}`);
114
+ }
115
+ const st = fs.statSync(p);
116
+ if (st.size < 32) {
117
+ throw new Error(`Output file too small: ${p} (${st.size} bytes)`);
118
+ }
119
+
120
+ const { gltf } = parseGlbHeaderAndJson(p);
121
+ const nodes = Array.isArray(gltf.nodes) ? gltf.nodes.length : 0;
122
+ const meshes = Array.isArray(gltf.meshes) ? gltf.meshes.length : 0;
123
+ const nodesWithMatrix = Array.isArray(gltf.nodes)
124
+ ? gltf.nodes.reduce((n, x) => n + (x && Object.prototype.hasOwnProperty.call(x, 'matrix') ? 1 : 0), 0)
125
+ : 0;
126
+
127
+ if (nodes === 0 || meshes === 0) {
128
+ throw new Error(`Invalid glTF content in ${p}: nodes=${nodes}, meshes=${meshes}`);
129
+ }
130
+ if (nodesWithMatrix !== 0) {
131
+ throw new Error(`Expected baked transforms (no node.matrix). Got nodesWithMatrix=${nodesWithMatrix} in ${p}`);
132
+ }
133
+
134
+ if (enableDraco) {
135
+ const used = gltf.extensionsUsed || [];
136
+ if (!used.includes('KHR_draco_mesh_compression')) {
137
+ throw new Error(`Expected Draco extensionUsed in ${p}`);
138
+ }
139
+ }
140
+
141
+ console.log('[smoke] OK:', p, `(${st.size} bytes)`);
142
+ }
143
+
144
+ console.log('[smoke] PASS');
145
+ }
146
+
147
+ main().catch((e) => {
148
+ console.error('[smoke] FAIL');
149
+ console.error(e && (e.stack || e.message || e));
150
+ process.exit(1);
151
+ });
152
+