create-ifc-lite 1.14.13 → 1.15.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/dist/index.js CHANGED
@@ -2,22 +2,53 @@
2
2
  /* This Source Code Form is subject to the terms of the Mozilla Public
3
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
4
4
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
5
- import { existsSync, mkdirSync } from 'fs';
6
- import { join } from 'path';
5
+ import { existsSync, mkdirSync, rmSync, readFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
7
8
  import { createBasicTemplate } from './templates/basic.js';
8
9
  import { createThreejsTemplate } from './templates/threejs.js';
9
10
  import { createBabylonjsTemplate } from './templates/babylonjs.js';
10
11
  import { createReactTemplate } from './templates/react.js';
11
12
  import { createServerTemplate } from './templates/server.js';
12
13
  import { createServerNativeTemplate } from './templates/server-native.js';
14
+ /** Every template, mapped to the function that scaffolds it. */
13
15
  const TEMPLATES = {
14
- basic: 'basic',
15
- threejs: 'threejs',
16
- babylonjs: 'babylonjs',
17
- react: 'react',
18
- server: 'server',
19
- 'server-native': 'server-native',
16
+ basic: createBasicTemplate,
17
+ threejs: createThreejsTemplate,
18
+ babylonjs: createBabylonjsTemplate,
19
+ react: createReactTemplate,
20
+ server: createServerTemplate,
21
+ 'server-native': createServerNativeTemplate,
20
22
  };
23
+ const TEMPLATE_NAMES = Object.keys(TEMPLATES);
24
+ /**
25
+ * `name in TEMPLATES` walks the prototype chain, so `--template toString`
26
+ * passed the guard and then matched no branch, silently scaffolding `basic`.
27
+ */
28
+ function isTemplate(name) {
29
+ return Object.hasOwn(TEMPLATES, name);
30
+ }
31
+ /**
32
+ * Read this package's own version. A failure here is a broken install, not a
33
+ * normal condition, so report it and return a value that reads as unknown
34
+ * rather than a plausible number a bug report would then carry — the same
35
+ * lesson `@ifc-lite/cli`'s `readCliVersion` records.
36
+ */
37
+ function readOwnVersion() {
38
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
39
+ try {
40
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
41
+ const version = pkg.version;
42
+ if (typeof version === 'string' && version.length > 0)
43
+ return version;
44
+ }
45
+ catch (err) {
46
+ console.error(`Warning: could not read ${pkgPath} (${err instanceof Error ? err.message : String(err)}).`);
47
+ return '0.0.0-unknown';
48
+ }
49
+ console.error(`Warning: ${pkgPath} declares no "version".`);
50
+ return '0.0.0-unknown';
51
+ }
21
52
  function printUsage() {
22
53
  console.log(`
23
54
  create-ifc-lite - Create IFC-Lite projects instantly
@@ -26,8 +57,9 @@ function printUsage() {
26
57
  npx create-ifc-lite [project-name] [options]
27
58
 
28
59
  Options:
29
- --template <type> Template to use [default: basic]
30
- --help Show this help message
60
+ -t, --template <type> Template to use [default: basic]
61
+ -v, --version Print the create-ifc-lite version
62
+ -h, --help Show this help message
31
63
 
32
64
  Examples:
33
65
  npx create-ifc-lite my-ifc-app
@@ -52,25 +84,38 @@ async function main() {
52
84
  printUsage();
53
85
  process.exit(0);
54
86
  }
87
+ if (args.includes('--version') || args.includes('-v')) {
88
+ console.log(readOwnVersion());
89
+ process.exit(0);
90
+ }
55
91
  // Parse arguments
56
- let projectName = 'my-ifc-app';
92
+ let projectName;
57
93
  let template = 'basic';
58
94
  for (let i = 0; i < args.length; i++) {
59
95
  const arg = args[i];
60
96
  if (arg === '--template' || arg === '-t') {
61
- const t = args[++i];
62
- if (t && t in TEMPLATES) {
63
- template = t;
64
- }
65
- else {
66
- console.error(`Invalid template: ${t}. Available: basic, threejs, babylonjs, react, server, server-native`);
97
+ const value = args[++i];
98
+ if (value === undefined || !isTemplate(value)) {
99
+ console.error(`Invalid template: ${value ?? '(missing)'}. Available: ${TEMPLATE_NAMES.join(', ')}`);
67
100
  process.exit(1);
68
101
  }
102
+ template = value;
103
+ }
104
+ else if (arg.startsWith('-')) {
105
+ // Previously ignored, which made `--templat react` scaffold a project
106
+ // literally named "react" instead of reporting the typo.
107
+ console.error(`Unknown option: ${arg}\nRun \`npx create-ifc-lite --help\` to see the available options.`);
108
+ process.exit(1);
69
109
  }
70
- else if (!arg.startsWith('-')) {
110
+ else if (projectName === undefined) {
71
111
  projectName = arg;
72
112
  }
113
+ else {
114
+ console.error(`Unexpected argument: ${arg}. Only one project name is accepted.`);
115
+ process.exit(1);
116
+ }
73
117
  }
118
+ projectName ??= 'my-ifc-app';
74
119
  // Reject path separators, '..', and names that would yield an invalid npm
75
120
  // `name`, so join(process.cwd(), projectName) stays under cwd and the
76
121
  // generated package.json is valid. Mirrors config-fixers.ts VALID_PACKAGE_NAME.
@@ -89,35 +134,22 @@ async function main() {
89
134
  process.exit(1);
90
135
  }
91
136
  console.log(`\n Creating IFC-Lite project in ${targetDir}...\n`);
92
- if (template === 'threejs') {
93
- mkdirSync(targetDir, { recursive: true });
94
- createThreejsTemplate(targetDir, projectName);
137
+ mkdirSync(targetDir, { recursive: true });
138
+ try {
139
+ TEMPLATES[template](targetDir, projectName);
95
140
  }
96
- else if (template === 'babylonjs') {
97
- mkdirSync(targetDir, { recursive: true });
98
- createBabylonjsTemplate(targetDir, projectName);
99
- }
100
- else if (template === 'react') {
101
- mkdirSync(targetDir, { recursive: true });
102
- createReactTemplate(targetDir, projectName);
103
- }
104
- else if (template === 'server') {
105
- mkdirSync(targetDir, { recursive: true });
106
- createServerTemplate(targetDir, projectName);
107
- }
108
- else if (template === 'server-native') {
109
- mkdirSync(targetDir, { recursive: true });
110
- createServerNativeTemplate(targetDir, projectName);
111
- }
112
- else {
113
- mkdirSync(targetDir, { recursive: true });
114
- createBasicTemplate(targetDir, projectName);
141
+ catch (error) {
142
+ // Templates resolve their dependency versions from the npm registry as
143
+ // their first act, so an offline run fails AFTER the directory exists.
144
+ // Leaving it behind made the obvious retry fail with "already exists".
145
+ rmSync(targetDir, { recursive: true, force: true });
146
+ throw error;
115
147
  }
116
148
  console.log(` Done! Next steps:\n`);
117
149
  console.log(` cd ${projectName}`);
118
150
  if (template === 'server') {
119
151
  console.log(` docker compose up -d`);
120
- console.log(` npm install && npm run example`);
152
+ console.log(` npm install && npm run example ./your-model.ifc`);
121
153
  console.log(`\n Server will be available at http://localhost:3001`);
122
154
  }
123
155
  else if (template === 'server-native') {
@@ -137,6 +169,15 @@ async function main() {
137
169
  console.log();
138
170
  }
139
171
  main().catch((error) => {
140
- console.error(error instanceof Error ? `\n ${error.message}\n` : error);
172
+ if (error instanceof Error) {
173
+ console.error(`\n ${error.message}`);
174
+ const cause = error.cause;
175
+ if (cause instanceof Error)
176
+ console.error(` Caused by: ${cause.message}`);
177
+ console.error();
178
+ }
179
+ else {
180
+ console.error(error);
181
+ }
141
182
  process.exit(1);
142
183
  });
@@ -4,6 +4,7 @@
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { getPackageVersion } from '../utils/config-fixers.js';
7
+ import { writeGitignore } from '../utils/gitignore.js';
7
8
  /**
8
9
  * Scaffold a Babylon.js IFC viewer project using @ifc-lite/geometry.
9
10
  * No WebGPU required — renders via Babylon.js WebGL Engine.
@@ -20,7 +21,7 @@ export function createBabylonjsTemplate(targetDir, projectName) {
20
21
  dev: 'vite',
21
22
  build: 'tsc && vite build',
22
23
  preview: 'vite preview',
23
- postinstall: 'node ./scripts/fix-ifc-lite-geometry-worker.mjs',
24
+ typecheck: 'tsc --noEmit',
24
25
  },
25
26
  dependencies: {
26
27
  '@ifc-lite/geometry': geometryVersion,
@@ -40,7 +41,7 @@ export function createBabylonjsTemplate(targetDir, projectName) {
40
41
  strict: true,
41
42
  esModuleInterop: true,
42
43
  skipLibCheck: true,
43
- outDir: 'dist',
44
+ noEmit: true,
44
45
  },
45
46
  include: ['src'],
46
47
  }, null, 2));
@@ -83,7 +84,7 @@ export default defineConfig({
83
84
  <header>
84
85
  <h1>${projectName}</h1>
85
86
  <input type="file" id="file-input" accept=".ifc" />
86
- <span id="status">Drop an IFC file to view</span>
87
+ <span id="status">Choose an IFC file to view</span>
87
88
  </header>
88
89
  <div id="canvas-container">
89
90
  <canvas id="viewer"></canvas>
@@ -95,24 +96,6 @@ export default defineConfig({
95
96
  `);
96
97
  // src/
97
98
  mkdirSync(join(targetDir, 'src'));
98
- mkdirSync(join(targetDir, 'scripts'));
99
- // scripts/fix-ifc-lite-geometry-worker.mjs
100
- writeFileSync(join(targetDir, 'scripts', 'fix-ifc-lite-geometry-worker.mjs'), `import { existsSync, readFileSync, writeFileSync } from 'node:fs';
101
- import path from 'node:path';
102
-
103
- const entryPath = path.join(process.cwd(), 'node_modules', '@ifc-lite', 'geometry', 'dist', 'index.js');
104
-
105
- if (!existsSync(entryPath)) {
106
- process.exit(0);
107
- }
108
-
109
- const source = readFileSync(entryPath, 'utf8');
110
- const patched = source.replace(/geometry\\.worker\\.ts/g, 'geometry.worker.js');
111
-
112
- if (patched !== source) {
113
- writeFileSync(entryPath, patched);
114
- }
115
- `);
116
99
  // src/ifc-to-babylon.ts
117
100
  writeFileSync(join(targetDir, 'src', 'ifc-to-babylon.ts'), `import {
118
101
  Mesh, VertexData, StandardMaterial, Color3,
@@ -132,13 +115,23 @@ export function meshDataToBabylon(meshData: MeshData, scene: Scene): Mesh {
132
115
  vertexData.indices = meshData.indices;
133
116
  vertexData.applyToMesh(mesh);
134
117
 
118
+ // Fold the per-element local frame: \`positions\` are relative to \`origin\`,
119
+ // so the world position of vertex i is origin + positions[3i..3i+3]. Most
120
+ // elements carry one (it is what keeps building-scale coordinates inside f32
121
+ // precision); dropping it scatters every element to its local offset.
122
+ if (meshData.origin) {
123
+ mesh.position.set(meshData.origin[0], meshData.origin[1], meshData.origin[2]);
124
+ }
125
+
135
126
  const [r, g, b, a] = meshData.color;
136
127
  const material = new StandardMaterial('mat-' + meshData.expressId, scene);
137
128
  material.diffuseColor = new Color3(r, g, b);
138
129
  material.specularColor = new Color3(0.15, 0.15, 0.15);
130
+ // Double-sided even when opaque: IFC triangle winding isn't reliably outward,
131
+ // so culling back faces punches holes in walls and slabs.
132
+ material.backFaceCulling = false;
139
133
  if (a < 1) {
140
134
  material.alpha = a;
141
- material.backFaceCulling = false;
142
135
  }
143
136
 
144
137
  mesh.material = material;
@@ -241,12 +234,13 @@ fileInput.addEventListener('change', async () => {
241
234
  status.textContent = file.name + ' — ' + event.totalMeshes + ' meshes';
242
235
  }
243
236
  }
244
- } catch (err: any) {
245
- status.textContent = 'Error: ' + err.message;
237
+ } catch (err) {
238
+ status.textContent = 'Error: ' + (err instanceof Error ? err.message : String(err));
246
239
  console.error(err);
247
240
  }
248
241
  });
249
242
  `);
243
+ writeGitignore(targetDir);
250
244
  // README
251
245
  writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
252
246
 
@@ -259,10 +253,13 @@ npm install
259
253
  npm run dev
260
254
  \`\`\`
261
255
 
262
- Open http://localhost:3000 and drop an IFC file.
256
+ Open the URL Vite prints (http://localhost:5173 by default) and pick an IFC file.
257
+
258
+ No IFC file handy? [AC20-FZK-Haus.ifc](https://github.com/LTplus-AG/ifc-lite/releases/download/fixtures-v1/ea6f04eaf92fac4d7ad0038bc3d2dfea4c094dd3f516ecc33c50bf1835ca108d) is a small public test model.
263
259
 
264
260
  ## Learn More
265
261
 
266
- - [IFC-Lite Documentation](https://ifclite.dev/docs/)
262
+ - [IFClite Documentation](https://ifclite.dev/docs/)
263
+ - [Babylon.js Integration Guide](https://ifclite.dev/docs/tutorials/babylonjs-integration/)
267
264
  `);
268
265
  }
@@ -4,21 +4,28 @@
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { getPackageVersion } from '../utils/config-fixers.js';
7
+ import { writeGitignore } from '../utils/gitignore.js';
7
8
  /**
8
9
  * Scaffold a minimal TypeScript project for parsing IFC files.
9
10
  */
10
11
  export function createBasicTemplate(targetDir, projectName) {
11
12
  const parserVersion = getPackageVersion('@ifc-lite/parser');
13
+ // `store.entityIndex.byType` is keyed by the raw STEP spelling, and the
14
+ // PascalCase lookup table for it lives in @ifc-lite/data.
15
+ const dataVersion = getPackageVersion('@ifc-lite/data');
12
16
  // package.json
13
17
  writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
14
18
  name: projectName,
15
- version: parserVersion.replace('^', ''),
19
+ version: '0.1.0',
20
+ private: true,
16
21
  type: 'module',
17
22
  scripts: {
18
- parse: 'npx tsx src/index.ts',
23
+ parse: 'tsx src/index.ts',
19
24
  build: 'tsc',
25
+ typecheck: 'tsc --noEmit',
20
26
  },
21
27
  dependencies: {
28
+ '@ifc-lite/data': dataVersion,
22
29
  '@ifc-lite/parser': parserVersion,
23
30
  },
24
31
  devDependencies: {
@@ -44,7 +51,8 @@ export function createBasicTemplate(targetDir, projectName) {
44
51
  // src/index.ts
45
52
  mkdirSync(join(targetDir, 'src'));
46
53
  writeFileSync(join(targetDir, 'src', 'index.ts'), `import { IfcParser } from '@ifc-lite/parser';
47
- import { readFileSync } from 'fs';
54
+ import { IFC_ENTITY_NAMES } from '@ifc-lite/data';
55
+ import { existsSync, readFileSync } from 'fs';
48
56
 
49
57
  // Example: Parse an IFC file
50
58
  const ifcPath = process.argv[2];
@@ -57,7 +65,13 @@ if (!ifcPath) {
57
65
  process.exit(1);
58
66
  }
59
67
 
60
- // readFileSync returns a Node Buffer (Uint8Array subclass); extract the underlying ArrayBuffer
68
+ if (!existsSync(ifcPath)) {
69
+ console.error(\`File not found: \${ifcPath}\`);
70
+ process.exit(1);
71
+ }
72
+
73
+ // readFileSync returns a Node Buffer (Uint8Array subclass) that may be a view
74
+ // into a larger pooled allocation, so slice out just this file's bytes.
61
75
  const nodeBuffer = readFileSync(ifcPath);
62
76
  const buffer = nodeBuffer.buffer.slice(
63
77
  nodeBuffer.byteOffset,
@@ -67,14 +81,27 @@ const buffer = nodeBuffer.buffer.slice(
67
81
  const parser = new IfcParser();
68
82
 
69
83
  console.log('Parsing IFC file...');
70
- parser.parseColumnar(buffer).then(store => {
84
+
85
+ try {
86
+ const store = await parser.parseColumnar(buffer);
87
+
88
+ // parseColumnar tolerates garbage input rather than throwing, so an empty
89
+ // store is how "that wasn't an IFC file" actually arrives.
90
+ if (store.entityCount === 0) {
91
+ throw new Error(\`No IFC entities found in \${ifcPath} — is it a STEP/IFC file?\`);
92
+ }
93
+
71
94
  console.log('\\nFile parsed successfully!');
72
95
  console.log(\` Entities: \${store.entityCount}\`);
73
96
  console.log(\` Schema: \${store.schemaVersion}\`);
74
97
 
75
- // Count by type
98
+ // Count by type. \`entityIndex.byType\` is keyed by the raw STEP spelling
99
+ // (IFCWALLSTANDARDCASE); IFC_ENTITY_NAMES maps that to the canonical IFC
100
+ // EXPRESS name (IfcWallStandardCase), which is what belongs in front of a
101
+ // user. Geometry resource entities are not in \`store.entities\`, so the
102
+ // per-entity \`getTypeName\` would answer 'Unknown' for most of this list.
76
103
  const typeCounts = [...store.entityIndex.byType.entries()]
77
- .map(([type, ids]) => [type, ids.length] as const)
104
+ .map(([stepName, ids]) => [IFC_ENTITY_NAMES[stepName] ?? stepName, ids.length] as const)
78
105
  .sort((a, b) => b[1] - a[1])
79
106
  .slice(0, 10);
80
107
 
@@ -82,8 +109,12 @@ parser.parseColumnar(buffer).then(store => {
82
109
  for (const [type, count] of typeCounts) {
83
110
  console.log(\` \${type}: \${count}\`);
84
111
  }
85
- });
112
+ } catch (error) {
113
+ console.error(\`\\nFailed to parse \${ifcPath}: \${error instanceof Error ? error.message : String(error)}\`);
114
+ process.exitCode = 1;
115
+ }
86
116
  `);
117
+ writeGitignore(targetDir);
87
118
  // README
88
119
  writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
89
120
 
@@ -99,6 +130,6 @@ npm run parse ./your-model.ifc
99
130
  ## Learn More
100
131
 
101
132
  - [IFC-Lite Documentation](https://ifclite.dev/docs/)
102
- - [API Reference](https://ifclite.dev/docs/api/)
133
+ - [TypeScript API Reference](https://ifclite.dev/docs/api/typescript/)
103
134
  `);
104
135
  }
@@ -4,14 +4,7 @@
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { getPackageVersion } from '../utils/config-fixers.js';
7
- const LICENSE_HEADER = `/* This Source Code Form is subject to the terms of the Mozilla Public
8
- * License, v. 2.0. If a copy of the MPL was not distributed with this
9
- * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
10
-
11
- `;
12
- function writeSourceFile(targetDir, relativePath, content) {
13
- writeFileSync(join(targetDir, relativePath), `${LICENSE_HEADER}${content}`);
14
- }
7
+ import { writeGitignore } from '../utils/gitignore.js';
15
8
  /**
16
9
  * Scaffold a standalone React + Vite WebGPU viewer.
17
10
  */
@@ -38,9 +31,9 @@ export function createReactTemplate(targetDir, projectName) {
38
31
  devDependencies: {
39
32
  '@types/react': '^18.2.0',
40
33
  '@types/react-dom': '^18.2.0',
41
- '@vitejs/plugin-react': '^4.2.0',
34
+ '@vitejs/plugin-react': '^5.0.0',
42
35
  typescript: '^5.3.0',
43
- vite: '^5.0.0',
36
+ vite: '^7.0.0',
44
37
  },
45
38
  }, null, 2));
46
39
  writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
@@ -61,7 +54,7 @@ export function createReactTemplate(targetDir, projectName) {
61
54
  },
62
55
  include: ['src'],
63
56
  }, null, 2));
64
- writeSourceFile(targetDir, 'vite.config.ts', `import { defineConfig } from 'vite';
57
+ writeFileSync(join(targetDir, 'vite.config.ts'), `import { defineConfig } from 'vite';
65
58
  import react from '@vitejs/plugin-react';
66
59
 
67
60
  const isolationHeaders = {
@@ -96,7 +89,7 @@ export default defineConfig({
96
89
  </html>
97
90
  `);
98
91
  mkdirSync(join(targetDir, 'src'));
99
- writeSourceFile(targetDir, 'src/main.tsx', `import React from 'react';
92
+ writeFileSync(join(targetDir, 'src/main.tsx'), `import React from 'react';
100
93
  import ReactDOM from 'react-dom/client';
101
94
  import App from './App.js';
102
95
  import './styles.css';
@@ -107,7 +100,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
107
100
  </React.StrictMode>,
108
101
  );
109
102
  `);
110
- writeSourceFile(targetDir, 'src/App.tsx', `import { useCallback, useEffect, useRef, useState, type ChangeEvent, type DragEvent } from 'react';
103
+ writeFileSync(join(targetDir, 'src/App.tsx'), `import { useCallback, useEffect, useRef, useState, type ChangeEvent, type DragEvent } from 'react';
111
104
  import { GeometryProcessor } from '@ifc-lite/geometry';
112
105
  import { Renderer } from '@ifc-lite/renderer';
113
106
 
@@ -402,7 +395,7 @@ export default function App() {
402
395
  );
403
396
  }
404
397
  `);
405
- writeSourceFile(targetDir, 'src/styles.css', `:root {
398
+ writeFileSync(join(targetDir, 'src/styles.css'), `:root {
406
399
  color-scheme: dark;
407
400
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
408
401
  background: #0b1220;
@@ -555,6 +548,7 @@ body {
555
548
  }
556
549
  }
557
550
  `);
551
+ writeGitignore(targetDir);
558
552
  writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
559
553
 
560
554
  React + Vite IFC viewer using [IFC-Lite](https://github.com/LTplus-AG/ifc-lite).
@@ -15,6 +15,7 @@ export function createServerNativeTemplate(targetDir, projectName) {
15
15
  writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
16
16
  name: projectName,
17
17
  version: '0.1.0',
18
+ private: true,
18
19
  type: 'module',
19
20
  description: 'IFC processing server (native binary) with TypeScript client',
20
21
  scripts: {
@@ -15,7 +15,7 @@ export function createServerTemplate(targetDir, projectName) {
15
15
 
16
16
  services:
17
17
  ifc-server:
18
- image: ghcr.io/LTplus-AG/ifc-lite-server:latest
18
+ image: ghcr.io/ltplus-ag/ifc-lite-server:latest
19
19
  container_name: ${projectName}-server
20
20
  ports:
21
21
  - "\${PORT:-3001}:8080"
@@ -47,7 +47,7 @@ volumes:
47
47
 
48
48
  services:
49
49
  ifc-server:
50
- image: ghcr.io/LTplus-AG/ifc-lite-server:latest
50
+ image: ghcr.io/ltplus-ag/ifc-lite-server:latest
51
51
  container_name: ${projectName}-server-dev
52
52
  ports:
53
53
  - "\${PORT:-3001}:8080"
@@ -167,6 +167,7 @@ cache
167
167
  writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
168
168
  name: projectName,
169
169
  version: '0.1.0',
170
+ private: true,
170
171
  type: 'module',
171
172
  description: 'IFC processing server with TypeScript client',
172
173
  scripts: {
@@ -585,8 +586,8 @@ The Docker image works on any container platform:
585
586
 
586
587
  \`\`\`bash
587
588
  # Pull and run
588
- docker pull ghcr.io/LTplus-AG/ifc-lite-server:latest
589
- docker run -p 8080:8080 -v ifc-cache:/app/cache ghcr.io/LTplus-AG/ifc-lite-server
589
+ docker pull ghcr.io/ltplus-ag/ifc-lite-server:latest
590
+ docker run -p 8080:8080 -v ifc-cache:/app/cache ghcr.io/ltplus-ag/ifc-lite-server
590
591
  \`\`\`
591
592
 
592
593
  ### Environment Variables
@@ -604,7 +605,7 @@ RUST_LOG=info
604
605
  ## Learn More
605
606
 
606
607
  - [IFC-Lite Documentation](https://ifclite.dev/docs/)
607
- - [Server API Reference](https://ifclite.dev/docs/api/server/)
608
+ - [Server Guide](https://ifclite.dev/docs/guide/server/)
608
609
  - [GitHub Repository](https://github.com/LTplus-AG/ifc-lite)
609
610
  `);
610
611
  console.log(' Created docker-compose.yml');
@@ -4,6 +4,7 @@
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { getPackageVersion } from '../utils/config-fixers.js';
7
+ import { writeGitignore } from '../utils/gitignore.js';
7
8
  /**
8
9
  * Scaffold a Three.js IFC viewer project using @ifc-lite/geometry.
9
10
  * No WebGPU required — renders via Three.js WebGLRenderer.
@@ -20,7 +21,7 @@ export function createThreejsTemplate(targetDir, projectName) {
20
21
  dev: 'vite',
21
22
  build: 'tsc && vite build',
22
23
  preview: 'vite preview',
23
- postinstall: 'node ./scripts/fix-ifc-lite-geometry-worker.mjs',
24
+ typecheck: 'tsc --noEmit',
24
25
  },
25
26
  dependencies: {
26
27
  '@ifc-lite/geometry': geometryVersion,
@@ -41,7 +42,7 @@ export function createThreejsTemplate(targetDir, projectName) {
41
42
  strict: true,
42
43
  esModuleInterop: true,
43
44
  skipLibCheck: true,
44
- outDir: 'dist',
45
+ noEmit: true,
45
46
  },
46
47
  include: ['src'],
47
48
  }, null, 2));
@@ -89,7 +90,7 @@ export default defineConfig({
89
90
  <header>
90
91
  <h1>${projectName}</h1>
91
92
  <input type="file" id="file-input" accept=".ifc" />
92
- <span id="status">Drop an IFC file to view</span>
93
+ <span id="status">Choose an IFC file to view</span>
93
94
  </header>
94
95
  <div id="canvas-container">
95
96
  <canvas id="viewer"></canvas>
@@ -101,24 +102,6 @@ export default defineConfig({
101
102
  `);
102
103
  // src/
103
104
  mkdirSync(join(targetDir, 'src'));
104
- mkdirSync(join(targetDir, 'scripts'));
105
- // scripts/fix-ifc-lite-geometry-worker.mjs
106
- writeFileSync(join(targetDir, 'scripts', 'fix-ifc-lite-geometry-worker.mjs'), `import { existsSync, readFileSync, writeFileSync } from 'node:fs';
107
- import path from 'node:path';
108
-
109
- const entryPath = path.join(process.cwd(), 'node_modules', '@ifc-lite', 'geometry', 'dist', 'index.js');
110
-
111
- if (!existsSync(entryPath)) {
112
- process.exit(0);
113
- }
114
-
115
- const source = readFileSync(entryPath, 'utf8');
116
- const patched = source.replace(/geometry\\.worker\\.ts/g, 'geometry.worker.js');
117
-
118
- if (patched !== source) {
119
- writeFileSync(entryPath, patched);
120
- }
121
- `);
122
105
  // src/ifc-to-threejs.ts
123
106
  writeFileSync(join(targetDir, 'src', 'ifc-to-threejs.ts'), `import * as THREE from 'three';
124
107
  import type { MeshData } from '@ifc-lite/geometry';
@@ -145,6 +128,11 @@ export function meshDataToThree(mesh: MeshData): THREE.Mesh {
145
128
  });
146
129
 
147
130
  const threeMesh = new THREE.Mesh(geometry, material);
131
+ // Fold the per-element local frame: \`positions\` are relative to \`origin\`,
132
+ // so the world position of vertex i is origin + positions[3i..3i+3]. Most
133
+ // elements carry one (it is what keeps building-scale coordinates inside f32
134
+ // precision); dropping it scatters every element to its local offset.
135
+ if (mesh.origin) threeMesh.position.fromArray(mesh.origin);
148
136
  threeMesh.userData.expressId = mesh.expressId;
149
137
  return threeMesh;
150
138
  }
@@ -260,12 +248,13 @@ fileInput.addEventListener('change', async () => {
260
248
  status.textContent = file.name + ' — ' + event.totalMeshes + ' meshes';
261
249
  }
262
250
  }
263
- } catch (err: any) {
264
- status.textContent = 'Error: ' + err.message;
251
+ } catch (err) {
252
+ status.textContent = 'Error: ' + (err instanceof Error ? err.message : String(err));
265
253
  console.error(err);
266
254
  }
267
255
  });
268
256
  `);
257
+ writeGitignore(targetDir);
269
258
  // README
270
259
  writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
271
260
 
@@ -278,7 +267,9 @@ npm install
278
267
  npm run dev
279
268
  \`\`\`
280
269
 
281
- Open http://localhost:3000 and drop an IFC file.
270
+ Open the URL Vite prints (http://localhost:5173 by default) and pick an IFC file.
271
+
272
+ No IFC file handy? [AC20-FZK-Haus.ifc](https://github.com/LTplus-AG/ifc-lite/releases/download/fixtures-v1/ea6f04eaf92fac4d7ad0038bc3d2dfea4c094dd3f516ecc33c50bf1835ca108d) is a small public test model.
282
273
 
283
274
  ## Learn More
284
275
 
@@ -3,13 +3,15 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
5
5
  import { join } from 'path';
6
- import { execFileSync } from 'child_process';
6
+ import { execFileSync, spawnSync } from 'child_process';
7
7
  /** Cache of npm-resolved versions to avoid redundant registry queries. */
8
8
  const versionCache = new Map();
9
9
  const publishedVersionsCache = new Map();
10
10
  const VALID_PACKAGE_NAME = /^(?:@[\w.-]+\/)?[\w.-]+$/;
11
11
  const NPM_TIMEOUT_MS = 30000;
12
12
  const MAX_VERSION_CANDIDATES = 10;
13
+ /** npm's own codes for "the registry does not have that artifact". */
14
+ const MISSING_ARTIFACT_CODE = /\bE(404|410)\b/;
13
15
  function readJsonFromNpm(args) {
14
16
  const command = process.platform === 'win32' ? process.env.ComSpec ?? 'cmd.exe' : 'npm';
15
17
  const commandArgs = process.platform === 'win32' ? ['/d', '/s', '/c', 'npm', ...args] : args;
@@ -43,6 +45,31 @@ function getVersionDependencies(packageName, version) {
43
45
  }
44
46
  return json;
45
47
  }
48
+ /**
49
+ * Is the version's own tarball actually downloadable?
50
+ *
51
+ * A version can be `dist-tags.latest`, be listed in the packument, and
52
+ * advertise a `dist.tarball` that 404s -- a publish that registered the
53
+ * version without storing the artifact. `@ifc-lite/server-client@3.1.0` was
54
+ * exactly that, so every `--template server` scaffold died at `npm install`
55
+ * with a 404 on a URL npm itself had handed it (#5570).
56
+ *
57
+ * `npm pack --dry-run` is the probe rather than a raw HTTP request because it
58
+ * resolves through npm's own registry, proxy and auth config, and writes
59
+ * nothing. Only an explicit "not there" answer rejects a candidate: a network
60
+ * failure or an auth wall must not silently push the scaffold back onto an
61
+ * older version, so anything else is treated as "cannot tell, assume fine".
62
+ */
63
+ function isTarballFetchable(packageName, version) {
64
+ const args = ['pack', `${packageName}@${version}`, '--dry-run', '--json'];
65
+ const command = process.platform === 'win32' ? process.env.ComSpec ?? 'cmd.exe' : 'npm';
66
+ const commandArgs = process.platform === 'win32' ? ['/d', '/s', '/c', 'npm', ...args] : args;
67
+ const result = spawnSync(command, commandArgs, { encoding: 'utf-8', timeout: NPM_TIMEOUT_MS });
68
+ if (result.status === 0)
69
+ return true;
70
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
71
+ return !MISSING_ARTIFACT_CODE.test(output);
72
+ }
46
73
  function isInstallablePublishedVersion(packageName, version) {
47
74
  const dependencies = getVersionDependencies(packageName, version);
48
75
  for (const [dependencyName, dependencyRange] of Object.entries(dependencies)) {
@@ -55,7 +82,7 @@ function isInstallablePublishedVersion(packageName, version) {
55
82
  return false;
56
83
  }
57
84
  }
58
- return true;
85
+ return isTarballFetchable(packageName, version);
59
86
  }
60
87
  /**
61
88
  * Fetch the latest installable published version of a specific npm package.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Write the .gitignore every scaffolded project needs. Without one, the first
3
+ * `git add .` in a fresh project commits `node_modules/` and `dist/` — and the
4
+ * viewer templates' `dist/` carries an 8 MB wasm bundle.
5
+ */
6
+ export declare function writeGitignore(targetDir: string): void;
@@ -0,0 +1,37 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ import { writeFileSync } from 'fs';
5
+ import { join } from 'path';
6
+ /**
7
+ * Write the .gitignore every scaffolded project needs. Without one, the first
8
+ * `git add .` in a fresh project commits `node_modules/` and `dist/` — and the
9
+ * viewer templates' `dist/` carries an 8 MB wasm bundle.
10
+ */
11
+ export function writeGitignore(targetDir) {
12
+ writeFileSync(join(targetDir, '.gitignore'), `# Dependencies
13
+ node_modules/
14
+
15
+ # Build output
16
+ dist/
17
+
18
+ # Environment files
19
+ .env
20
+ .env.local
21
+ .env.*.local
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+
29
+ # OS
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # Logs
34
+ *.log
35
+ npm-debug.log*
36
+ `);
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ifc-lite",
3
- "version": "1.14.13",
3
+ "version": "1.15.0",
4
4
  "description": "Create IFC-Lite projects with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,9 +31,9 @@
31
31
  "node": ">=18.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^26.4.0",
34
+ "@types/node": "^26.6.1",
35
35
  "typescript": "^5.3.0",
36
- "vitest": "^4.1.11"
36
+ "vitest": "^5.0.1"
37
37
  },
38
38
  "homepage": "https://ifclite.dev/docs/",
39
39
  "bugs": "https://github.com/LTplus-AG/ifc-lite/issues",