create-ifc-lite 1.14.3 → 1.14.5

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
@@ -21,7 +21,7 @@ npm install
21
21
  npm run parse ./model.ifc
22
22
  ```
23
23
 
24
- **React** - React + Vite project with drag-and-drop viewer:
24
+ **React** - React + Vite project with WebGPU rendering and drag-and-drop loading:
25
25
 
26
26
  ```bash
27
27
  npx create-ifc-lite my-viewer --template react
@@ -34,7 +34,7 @@ npm run dev
34
34
 
35
35
  | Flag | Description |
36
36
  |------|-------------|
37
- | `--template <type>` | Template to use: `basic`, `react` (default: `basic`) |
37
+ | `--template <type>` | Template to use: `basic`, `threejs`, `babylonjs`, `react`, `server`, `server-native` (default: `basic`) |
38
38
  | `--help` | Show help |
39
39
 
40
40
  ## Learn More
package/dist/index.js CHANGED
@@ -7,10 +7,9 @@ import { join } from 'path';
7
7
  import { createBasicTemplate } from './templates/basic.js';
8
8
  import { createThreejsTemplate } from './templates/threejs.js';
9
9
  import { createBabylonjsTemplate } from './templates/babylonjs.js';
10
+ import { createReactTemplate } from './templates/react.js';
10
11
  import { createServerTemplate } from './templates/server.js';
11
12
  import { createServerNativeTemplate } from './templates/server-native.js';
12
- import { fixViewerTemplate } from './utils/config-fixers.js';
13
- import { downloadViewer } from './utils/download.js';
14
13
  const TEMPLATES = {
15
14
  basic: 'basic',
16
15
  threejs: 'threejs',
@@ -42,7 +41,7 @@ function printUsage() {
42
41
  basic Minimal TypeScript project for parsing IFC files
43
42
  threejs Three.js viewer (WebGL, no WebGPU required)
44
43
  babylonjs Babylon.js viewer (WebGL, no WebGPU required)
45
- react Full-featured React + Vite viewer with WebGPU rendering
44
+ react React + Vite viewer with WebGPU rendering
46
45
  server Docker-based IFC processing server with TypeScript client
47
46
  server-native Native binary server (no Docker required)
48
47
  `);
@@ -87,16 +86,8 @@ async function main() {
87
86
  createBabylonjsTemplate(targetDir, projectName);
88
87
  }
89
88
  else if (template === 'react') {
90
- // Download the actual viewer from GitHub
91
- const success = await downloadViewer(targetDir, projectName);
92
- if (success) {
93
- fixViewerTemplate(targetDir, projectName);
94
- }
95
- else {
96
- console.error(' Failed to download viewer. Creating minimal fallback...');
97
- mkdirSync(targetDir, { recursive: true });
98
- createBasicTemplate(targetDir, projectName);
99
- }
89
+ mkdirSync(targetDir, { recursive: true });
90
+ createReactTemplate(targetDir, projectName);
100
91
  }
101
92
  else if (template === 'server') {
102
93
  mkdirSync(targetDir, { recursive: true });
@@ -133,4 +124,7 @@ async function main() {
133
124
  }
134
125
  console.log();
135
126
  }
136
- main().catch(console.error);
127
+ main().catch((error) => {
128
+ console.error(error instanceof Error ? `\n ${error.message}\n` : error);
129
+ process.exit(1);
130
+ });
@@ -20,6 +20,7 @@ export function createBabylonjsTemplate(targetDir, projectName) {
20
20
  dev: 'vite',
21
21
  build: 'tsc && vite build',
22
22
  preview: 'vite preview',
23
+ postinstall: 'node ./scripts/fix-ifc-lite-geometry-worker.mjs',
23
24
  },
24
25
  dependencies: {
25
26
  '@ifc-lite/geometry': geometryVersion,
@@ -94,6 +95,24 @@ export default defineConfig({
94
95
  `);
95
96
  // src/
96
97
  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
+ `);
97
116
  // src/ifc-to-babylon.ts
98
117
  writeFileSync(join(targetDir, 'src', 'ifc-to-babylon.ts'), `import {
99
118
  Mesh, VertexData, StandardMaterial, Color3,
@@ -3,23 +3,23 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
- import { getLatestVersion } from '../utils/config-fixers.js';
6
+ import { getPackageVersion } from '../utils/config-fixers.js';
7
7
  /**
8
8
  * Scaffold a minimal TypeScript project for parsing IFC files.
9
9
  */
10
10
  export function createBasicTemplate(targetDir, projectName) {
11
- const latestVersion = getLatestVersion();
11
+ const parserVersion = getPackageVersion('@ifc-lite/parser');
12
12
  // package.json
13
13
  writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
14
14
  name: projectName,
15
- version: latestVersion.replace('^', ''),
15
+ version: parserVersion.replace('^', ''),
16
16
  type: 'module',
17
17
  scripts: {
18
18
  parse: 'npx tsx src/index.ts',
19
19
  build: 'tsc',
20
20
  },
21
21
  dependencies: {
22
- '@ifc-lite/parser': latestVersion,
22
+ '@ifc-lite/parser': parserVersion,
23
23
  },
24
24
  devDependencies: {
25
25
  '@types/node': '^22.0.0',
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Scaffold a standalone React + Vite WebGPU viewer.
3
+ */
4
+ export declare function createReactTemplate(targetDir: string, projectName: string): void;
@@ -0,0 +1,580 @@
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 { mkdirSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
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
+ }
15
+ /**
16
+ * Scaffold a standalone React + Vite WebGPU viewer.
17
+ */
18
+ export function createReactTemplate(targetDir, projectName) {
19
+ const geometryVersion = getPackageVersion('@ifc-lite/geometry');
20
+ const rendererVersion = getPackageVersion('@ifc-lite/renderer');
21
+ writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
22
+ name: projectName,
23
+ version: '0.1.0',
24
+ private: true,
25
+ type: 'module',
26
+ scripts: {
27
+ dev: 'vite',
28
+ build: 'tsc && vite build',
29
+ typecheck: 'tsc --noEmit',
30
+ preview: 'vite preview',
31
+ },
32
+ dependencies: {
33
+ '@ifc-lite/geometry': geometryVersion,
34
+ '@ifc-lite/renderer': rendererVersion,
35
+ react: '^18.2.0',
36
+ 'react-dom': '^18.2.0',
37
+ },
38
+ devDependencies: {
39
+ '@types/react': '^18.2.0',
40
+ '@types/react-dom': '^18.2.0',
41
+ '@vitejs/plugin-react': '^4.2.0',
42
+ typescript: '^5.3.0',
43
+ vite: '^5.0.0',
44
+ },
45
+ }, null, 2));
46
+ writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
47
+ compilerOptions: {
48
+ target: 'ES2022',
49
+ useDefineForClassFields: true,
50
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
51
+ module: 'ESNext',
52
+ skipLibCheck: true,
53
+ moduleResolution: 'bundler',
54
+ allowImportingTsExtensions: false,
55
+ resolveJsonModule: true,
56
+ isolatedModules: true,
57
+ noEmit: true,
58
+ jsx: 'react-jsx',
59
+ strict: true,
60
+ esModuleInterop: true,
61
+ },
62
+ include: ['src'],
63
+ }, null, 2));
64
+ writeSourceFile(targetDir, 'vite.config.ts', `import { defineConfig } from 'vite';
65
+ import react from '@vitejs/plugin-react';
66
+
67
+ const isolationHeaders = {
68
+ 'Cross-Origin-Opener-Policy': 'same-origin',
69
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
70
+ };
71
+
72
+ export default defineConfig({
73
+ plugins: [react()],
74
+ optimizeDeps: {
75
+ exclude: ['@ifc-lite/wasm'],
76
+ },
77
+ server: {
78
+ headers: isolationHeaders,
79
+ },
80
+ preview: {
81
+ headers: isolationHeaders,
82
+ },
83
+ });
84
+ `);
85
+ writeFileSync(join(targetDir, 'index.html'), `<!DOCTYPE html>
86
+ <html lang="en">
87
+ <head>
88
+ <meta charset="UTF-8" />
89
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
90
+ <title>${projectName}</title>
91
+ </head>
92
+ <body>
93
+ <div id="root"></div>
94
+ <script type="module" src="/src/main.tsx"></script>
95
+ </body>
96
+ </html>
97
+ `);
98
+ mkdirSync(join(targetDir, 'src'));
99
+ writeSourceFile(targetDir, 'src/main.tsx', `import React from 'react';
100
+ import ReactDOM from 'react-dom/client';
101
+ import App from './App.js';
102
+ import './styles.css';
103
+
104
+ ReactDOM.createRoot(document.getElementById('root')!).render(
105
+ <React.StrictMode>
106
+ <App />
107
+ </React.StrictMode>,
108
+ );
109
+ `);
110
+ writeSourceFile(targetDir, 'src/App.tsx', `import { useCallback, useEffect, useRef, useState, type ChangeEvent, type DragEvent } from 'react';
111
+ import { GeometryProcessor } from '@ifc-lite/geometry';
112
+ import { Renderer } from '@ifc-lite/renderer';
113
+
114
+ type ViewerSession = {
115
+ renderer: Renderer;
116
+ destroy: () => void;
117
+ };
118
+
119
+ function setupCameraControls(canvas: HTMLCanvasElement, renderer: Renderer): () => void {
120
+ const camera = renderer.getCamera();
121
+ let isDragging = false;
122
+ let isPanning = false;
123
+ let lastX = 0;
124
+ let lastY = 0;
125
+
126
+ const onMouseDown = (event: MouseEvent) => {
127
+ isDragging = true;
128
+ isPanning = event.button === 1 || event.button === 2 || event.shiftKey;
129
+ lastX = event.clientX;
130
+ lastY = event.clientY;
131
+ canvas.style.cursor = isPanning ? 'move' : 'grabbing';
132
+ };
133
+
134
+ const onMouseMove = (event: MouseEvent) => {
135
+ if (!isDragging) return;
136
+ const deltaX = event.clientX - lastX;
137
+ const deltaY = event.clientY - lastY;
138
+ lastX = event.clientX;
139
+ lastY = event.clientY;
140
+
141
+ if (isPanning) {
142
+ camera.pan(deltaX, deltaY);
143
+ } else {
144
+ camera.orbit(deltaX, deltaY);
145
+ }
146
+ };
147
+
148
+ const stopDrag = () => {
149
+ isDragging = false;
150
+ isPanning = false;
151
+ canvas.style.cursor = 'grab';
152
+ };
153
+
154
+ const onWheel = (event: WheelEvent) => {
155
+ event.preventDefault();
156
+ camera.zoom(event.deltaY);
157
+ };
158
+
159
+ const onContextMenu = (event: MouseEvent) => {
160
+ event.preventDefault();
161
+ };
162
+
163
+ canvas.addEventListener('mousedown', onMouseDown);
164
+ window.addEventListener('mousemove', onMouseMove);
165
+ window.addEventListener('mouseup', stopDrag);
166
+ canvas.addEventListener('mouseleave', stopDrag);
167
+ canvas.addEventListener('wheel', onWheel, { passive: false });
168
+ canvas.addEventListener('contextmenu', onContextMenu);
169
+ canvas.style.cursor = 'grab';
170
+
171
+ return () => {
172
+ canvas.removeEventListener('mousedown', onMouseDown);
173
+ window.removeEventListener('mousemove', onMouseMove);
174
+ window.removeEventListener('mouseup', stopDrag);
175
+ canvas.removeEventListener('mouseleave', stopDrag);
176
+ canvas.removeEventListener('wheel', onWheel);
177
+ canvas.removeEventListener('contextmenu', onContextMenu);
178
+ };
179
+ }
180
+
181
+ async function createViewer(canvas: HTMLCanvasElement): Promise<ViewerSession> {
182
+ const renderer = new Renderer(canvas);
183
+ await renderer.init();
184
+
185
+ const resize = () => {
186
+ const rect = canvas.getBoundingClientRect();
187
+ const width = Math.max(1, Math.floor(rect.width));
188
+ const height = Math.max(1, Math.floor(rect.height));
189
+ renderer.resize(width, height);
190
+ };
191
+
192
+ resize();
193
+ window.addEventListener('resize', resize);
194
+ const cleanupControls = setupCameraControls(canvas, renderer);
195
+
196
+ let destroyed = false;
197
+ let frameId = 0;
198
+ const loop = () => {
199
+ if (destroyed) return;
200
+ renderer.render();
201
+ frameId = requestAnimationFrame(loop);
202
+ };
203
+ loop();
204
+
205
+ return {
206
+ renderer,
207
+ destroy: () => {
208
+ destroyed = true;
209
+ cancelAnimationFrame(frameId);
210
+ cleanupControls();
211
+ window.removeEventListener('resize', resize);
212
+ renderer.destroy();
213
+ },
214
+ };
215
+ }
216
+
217
+ export default function App() {
218
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
219
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
220
+ const processorRef = useRef<GeometryProcessor | null>(null);
221
+ const viewerRef = useRef<ViewerSession | null>(null);
222
+ const loadIdRef = useRef(0);
223
+ const [status, setStatus] = useState('Initializing WebGPU viewer...');
224
+ const [ready, setReady] = useState(false);
225
+ const [busy, setBusy] = useState(true);
226
+
227
+ useEffect(() => {
228
+ let cancelled = false;
229
+ const initId = ++loadIdRef.current;
230
+
231
+ const init = async () => {
232
+ if (!canvasRef.current) return;
233
+ if (!('gpu' in navigator)) {
234
+ setStatus('WebGPU is not available in this browser. Try a recent Chromium-based browser.');
235
+ setBusy(false);
236
+ return;
237
+ }
238
+
239
+ try {
240
+ const processor = new GeometryProcessor();
241
+ await processor.init();
242
+ if (cancelled || initId !== loadIdRef.current) return;
243
+
244
+ processorRef.current = processor;
245
+ const viewer = await createViewer(canvasRef.current);
246
+ if (cancelled || initId !== loadIdRef.current) {
247
+ viewer.destroy();
248
+ return;
249
+ }
250
+
251
+ viewerRef.current = viewer;
252
+ setReady(true);
253
+ setBusy(false);
254
+ setStatus('Drop an IFC file or choose one to start rendering.');
255
+ } catch (error) {
256
+ console.error(error);
257
+ setBusy(false);
258
+ setStatus(error instanceof Error ? error.message : 'Failed to initialize viewer.');
259
+ }
260
+ };
261
+
262
+ void init();
263
+
264
+ return () => {
265
+ cancelled = true;
266
+ loadIdRef.current += 1;
267
+ viewerRef.current?.destroy();
268
+ viewerRef.current = null;
269
+ };
270
+ }, []);
271
+
272
+ const loadFile = useCallback(async (file: File) => {
273
+ if (!canvasRef.current || !processorRef.current || !ready || busy) return;
274
+ const loadId = ++loadIdRef.current;
275
+
276
+ setBusy(true);
277
+ setStatus('Preparing renderer...');
278
+
279
+ try {
280
+ const previousViewer = viewerRef.current;
281
+ const viewer = await createViewer(canvasRef.current);
282
+ if (loadId !== loadIdRef.current) {
283
+ viewer.destroy();
284
+ return;
285
+ }
286
+
287
+ previousViewer?.destroy();
288
+ viewerRef.current = viewer;
289
+ const renderer = viewer.renderer;
290
+ const processor = processorRef.current;
291
+ if (!processor) {
292
+ throw new Error('Geometry processor is no longer available.');
293
+ }
294
+
295
+ const bytes = new Uint8Array(await file.arrayBuffer());
296
+
297
+ let loadedMeshes = 0;
298
+ setStatus('Streaming geometry...');
299
+
300
+ for await (const event of processor.processStreaming(bytes)) {
301
+ if (loadId !== loadIdRef.current) return;
302
+
303
+ if (event.type === 'batch') {
304
+ renderer.addMeshes(event.meshes, true);
305
+ loadedMeshes = event.totalSoFar;
306
+ setStatus('Loaded ' + loadedMeshes + ' meshes...');
307
+ }
308
+
309
+ if (event.type === 'complete') {
310
+ renderer.fitToView();
311
+ setStatus(file.name + ' loaded with ' + event.totalMeshes + ' meshes.');
312
+ }
313
+ }
314
+ } catch (error) {
315
+ console.error(error);
316
+ if (loadId === loadIdRef.current) {
317
+ setStatus(error instanceof Error ? error.message : 'Failed to load IFC file.');
318
+ }
319
+ } finally {
320
+ if (loadId === loadIdRef.current) {
321
+ setBusy(false);
322
+ }
323
+ }
324
+ }, [busy, ready]);
325
+
326
+ const onFileChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
327
+ const file = event.target.files?.[0];
328
+ if (!file) return;
329
+ await loadFile(file);
330
+ event.target.value = '';
331
+ }, [loadFile]);
332
+
333
+ const onDrop = useCallback(async (event: DragEvent<HTMLElement>) => {
334
+ event.preventDefault();
335
+ if (!ready || busy) return;
336
+ const file = event.dataTransfer.files?.[0];
337
+ if (file) {
338
+ await loadFile(file);
339
+ }
340
+ }, [busy, loadFile, ready]);
341
+
342
+ const openFilePicker = useCallback(() => {
343
+ if (!ready || busy) return;
344
+ fileInputRef.current?.click();
345
+ }, [busy, ready]);
346
+
347
+ return (
348
+ <div className="app">
349
+ <aside className="sidebar">
350
+ <div>
351
+ <p className="eyebrow">IFC-Lite</p>
352
+ <h1>${projectName}</h1>
353
+ <p className="muted">
354
+ Standalone React + Vite viewer powered by IFC-Lite geometry streaming and WebGPU rendering.
355
+ </p>
356
+ </div>
357
+
358
+ <input
359
+ ref={fileInputRef}
360
+ className="visuallyHiddenInput"
361
+ type="file"
362
+ accept=".ifc"
363
+ disabled={!ready || busy}
364
+ aria-label="Choose IFC file"
365
+ onChange={(event) => void onFileChange(event)}
366
+ />
367
+ <button
368
+ type="button"
369
+ className="uploadButton"
370
+ disabled={!ready || busy}
371
+ aria-disabled={!ready || busy}
372
+ onClick={openFilePicker}
373
+ >
374
+ {busy ? 'Working…' : 'Choose IFC File'}
375
+ </button>
376
+
377
+ <div className="panel">
378
+ <p className="label">Status</p>
379
+ <p>{status}</p>
380
+ </div>
381
+
382
+ <div className="panel">
383
+ <p className="label">Controls</p>
384
+ <ul>
385
+ <li>Left drag: orbit</li>
386
+ <li>Shift / middle / right drag: pan</li>
387
+ <li>Wheel: zoom</li>
388
+ <li>Drop an IFC anywhere on the viewport</li>
389
+ </ul>
390
+ </div>
391
+ </aside>
392
+
393
+ <main className="viewportShell" onDragOver={(event) => event.preventDefault()} onDrop={(event) => void onDrop(event)}>
394
+ <canvas ref={canvasRef} className="viewport" />
395
+ </main>
396
+ </div>
397
+ );
398
+ }
399
+ `);
400
+ writeSourceFile(targetDir, 'src/styles.css', `:root {
401
+ color-scheme: dark;
402
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
403
+ background: #0b1220;
404
+ color: #e5eefc;
405
+ }
406
+
407
+ * {
408
+ box-sizing: border-box;
409
+ }
410
+
411
+ html,
412
+ body,
413
+ #root {
414
+ margin: 0;
415
+ min-height: 100%;
416
+ height: 100%;
417
+ }
418
+
419
+ body {
420
+ background:
421
+ radial-gradient(circle at top, rgba(59, 130, 246, 0.18), transparent 30%),
422
+ #0b1220;
423
+ }
424
+
425
+ .app {
426
+ display: grid;
427
+ grid-template-columns: 320px minmax(0, 1fr);
428
+ min-height: 100vh;
429
+ }
430
+
431
+ .sidebar {
432
+ display: flex;
433
+ flex-direction: column;
434
+ gap: 1rem;
435
+ padding: 1.5rem;
436
+ border-right: 1px solid rgba(148, 163, 184, 0.18);
437
+ background: rgba(15, 23, 42, 0.92);
438
+ backdrop-filter: blur(16px);
439
+ }
440
+
441
+ .eyebrow,
442
+ .label {
443
+ margin: 0 0 0.5rem;
444
+ color: #93c5fd;
445
+ font-size: 0.8rem;
446
+ font-weight: 700;
447
+ letter-spacing: 0.08em;
448
+ text-transform: uppercase;
449
+ }
450
+
451
+ .sidebar h1 {
452
+ margin: 0 0 0.75rem;
453
+ font-size: 1.8rem;
454
+ }
455
+
456
+ .muted {
457
+ margin: 0;
458
+ color: #94a3b8;
459
+ line-height: 1.5;
460
+ }
461
+
462
+ .uploadButton {
463
+ display: inline-flex;
464
+ align-items: center;
465
+ justify-content: center;
466
+ min-height: 3rem;
467
+ padding: 0.9rem 1rem;
468
+ border-radius: 0.9rem;
469
+ background: linear-gradient(135deg, #2563eb, #0ea5e9);
470
+ color: white;
471
+ font-weight: 700;
472
+ cursor: pointer;
473
+ transition: transform 0.15s ease, opacity 0.15s ease;
474
+ border: none;
475
+ }
476
+
477
+ .uploadButton:hover:not(:disabled) {
478
+ transform: translateY(-1px);
479
+ }
480
+
481
+ .uploadButton:disabled {
482
+ opacity: 0.6;
483
+ cursor: not-allowed;
484
+ }
485
+
486
+ .visuallyHiddenInput {
487
+ position: absolute;
488
+ width: 1px;
489
+ height: 1px;
490
+ padding: 0;
491
+ margin: -1px;
492
+ overflow: hidden;
493
+ clip: rect(0, 0, 0, 0);
494
+ white-space: nowrap;
495
+ border: 0;
496
+ }
497
+
498
+ .panel {
499
+ padding: 1rem;
500
+ border: 1px solid rgba(148, 163, 184, 0.18);
501
+ border-radius: 1rem;
502
+ background: rgba(15, 23, 42, 0.7);
503
+ }
504
+
505
+ .panel p {
506
+ margin: 0;
507
+ line-height: 1.5;
508
+ }
509
+
510
+ .panel ul {
511
+ margin: 0;
512
+ padding-left: 1.1rem;
513
+ color: #cbd5e1;
514
+ line-height: 1.6;
515
+ }
516
+
517
+ .viewportShell {
518
+ position: relative;
519
+ min-width: 0;
520
+ min-height: 100vh;
521
+ padding: 1rem;
522
+ }
523
+
524
+ .viewport {
525
+ display: block;
526
+ width: 100%;
527
+ height: calc(100vh - 2rem);
528
+ border-radius: 1rem;
529
+ border: 1px solid rgba(148, 163, 184, 0.16);
530
+ background: linear-gradient(180deg, #020617, #0f172a);
531
+ outline: none;
532
+ }
533
+
534
+ @media (max-width: 960px) {
535
+ .app {
536
+ grid-template-columns: 1fr;
537
+ }
538
+
539
+ .sidebar {
540
+ border-right: none;
541
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
542
+ }
543
+
544
+ .viewportShell {
545
+ min-height: 60vh;
546
+ }
547
+
548
+ .viewport {
549
+ height: 60vh;
550
+ }
551
+ }
552
+ `);
553
+ writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
554
+
555
+ React + Vite IFC viewer using [IFC-Lite](https://github.com/louistrue/ifc-lite).
556
+
557
+ ## Quick Start
558
+
559
+ \`\`\`bash
560
+ npm install
561
+ npm run dev
562
+ \`\`\`
563
+
564
+ Open the app in your browser and drop an IFC file onto the viewport.
565
+
566
+ ## Features
567
+
568
+ - React + Vite developer experience
569
+ - IFC-Lite geometry streaming for progressive loading
570
+ - WebGPU rendering via \`@ifc-lite/renderer\`
571
+ - Drag-and-drop or file picker loading
572
+ - Orbit, pan, and zoom camera controls
573
+
574
+ ## Learn More
575
+
576
+ - [IFC-Lite Documentation](https://louistrue.github.io/ifc-lite/)
577
+ - [Rendering Guide](https://louistrue.github.io/ifc-lite/guide/rendering/)
578
+ - [Geometry Guide](https://louistrue.github.io/ifc-lite/guide/geometry/)
579
+ `);
580
+ }
@@ -3,13 +3,14 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
- import { getLatestVersion } from '../utils/config-fixers.js';
6
+ import { getPackageVersion } from '../utils/config-fixers.js';
7
7
  /**
8
8
  * Scaffold a native-binary IFC processing server with TypeScript client examples.
9
9
  * No Docker required -- the server binary is downloaded and run via npm scripts.
10
10
  */
11
11
  export function createServerNativeTemplate(targetDir, projectName) {
12
- const latestVersion = getLatestVersion();
12
+ const serverBinVersion = getPackageVersion('@ifc-lite/server-bin');
13
+ const serverClientVersion = getPackageVersion('@ifc-lite/server-client');
13
14
  // package.json
14
15
  writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
15
16
  name: projectName,
@@ -26,8 +27,8 @@ export function createServerNativeTemplate(targetDir, projectName) {
26
27
  'typecheck': 'tsc --noEmit',
27
28
  },
28
29
  dependencies: {
29
- '@ifc-lite/server-bin': latestVersion,
30
- '@ifc-lite/server-client': latestVersion,
30
+ '@ifc-lite/server-bin': serverBinVersion,
31
+ '@ifc-lite/server-client': serverClientVersion,
31
32
  },
32
33
  devDependencies: {
33
34
  'typescript': '^5.3.0',
@@ -184,9 +185,13 @@ Example:
184
185
  process.exit(1);
185
186
  }
186
187
 
187
- const buffer = readFileSync(ifcPath);
188
+ const nodeBuffer = readFileSync(ifcPath);
189
+ const buffer = nodeBuffer.buffer.slice(
190
+ nodeBuffer.byteOffset,
191
+ nodeBuffer.byteOffset + nodeBuffer.byteLength,
192
+ ) as ArrayBuffer;
188
193
  console.log(\`\\nParsing: \${ifcPath}\`);
189
- console.log(\`File size: \${(buffer.length / 1024 / 1024).toFixed(2)} MB\`);
194
+ console.log(\`File size: \${(nodeBuffer.length / 1024 / 1024).toFixed(2)} MB\`);
190
195
 
191
196
  const startTime = performance.now();
192
197
 
@@ -264,8 +269,12 @@ async function main() {
264
269
  process.exit(1);
265
270
  }
266
271
 
267
- const buffer = readFileSync(ifcPath);
268
- console.log(\`\\nStreaming: \${ifcPath} (\${(buffer.length / 1024 / 1024).toFixed(1)} MB)\`);
272
+ const nodeBuffer = readFileSync(ifcPath);
273
+ const buffer = nodeBuffer.buffer.slice(
274
+ nodeBuffer.byteOffset,
275
+ nodeBuffer.byteOffset + nodeBuffer.byteLength,
276
+ ) as ArrayBuffer;
277
+ console.log(\`\\nStreaming: \${ifcPath} (\${(nodeBuffer.length / 1024 / 1024).toFixed(1)} MB)\`);
269
278
 
270
279
  const startTime = performance.now();
271
280
  let totalMeshes = 0;
@@ -3,12 +3,12 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  import { mkdirSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
- import { getLatestVersion } from '../utils/config-fixers.js';
6
+ import { getPackageVersion } from '../utils/config-fixers.js';
7
7
  /**
8
8
  * Scaffold a Docker-based IFC processing server with TypeScript client examples.
9
9
  */
10
10
  export function createServerTemplate(targetDir, projectName) {
11
- const latestVersion = getLatestVersion();
11
+ const serverClientVersion = getPackageVersion('@ifc-lite/server-client');
12
12
  // docker-compose.yml - Production configuration
13
13
  writeFileSync(join(targetDir, 'docker-compose.yml'), `# IFC-Lite Server - Production Configuration
14
14
  # Start with: docker compose up -d
@@ -180,7 +180,7 @@ cache
180
180
  'typecheck': 'tsc --noEmit',
181
181
  },
182
182
  dependencies: {
183
- '@ifc-lite/server-client': latestVersion,
183
+ '@ifc-lite/server-client': serverClientVersion,
184
184
  },
185
185
  devDependencies: {
186
186
  'typescript': '^5.3.0',
@@ -273,8 +273,12 @@ The server will:
273
273
 
274
274
  // Read IFC file
275
275
  console.log(\`\\nParsing: \${ifcPath}\`);
276
- const buffer = readFileSync(ifcPath);
277
- console.log(\`File size: \${(buffer.length / 1024 / 1024).toFixed(2)} MB\`);
276
+ const nodeBuffer = readFileSync(ifcPath);
277
+ const buffer = nodeBuffer.buffer.slice(
278
+ nodeBuffer.byteOffset,
279
+ nodeBuffer.byteOffset + nodeBuffer.byteLength,
280
+ ) as ArrayBuffer;
281
+ console.log(\`File size: \${(nodeBuffer.length / 1024 / 1024).toFixed(2)} MB\`);
278
282
 
279
283
  // Parse with Parquet format (most efficient)
280
284
  console.log('\\nSending to server...');
@@ -373,8 +377,12 @@ async function main() {
373
377
  process.exit(1);
374
378
  }
375
379
 
376
- const buffer = readFileSync(ifcPath);
377
- console.log(\`\\nStreaming: \${ifcPath} (\${(buffer.length / 1024 / 1024).toFixed(1)} MB)\`);
380
+ const nodeBuffer = readFileSync(ifcPath);
381
+ const buffer = nodeBuffer.buffer.slice(
382
+ nodeBuffer.byteOffset,
383
+ nodeBuffer.byteOffset + nodeBuffer.byteLength,
384
+ ) as ArrayBuffer;
385
+ console.log(\`\\nStreaming: \${ifcPath} (\${(nodeBuffer.length / 1024 / 1024).toFixed(1)} MB)\`);
378
386
 
379
387
  // Check Parquet support
380
388
  const parquetAvailable = await client.isParquetSupported();
@@ -438,6 +446,8 @@ main().catch(console.error);
438
446
  * See example.ts and example-stream.ts for usage examples.
439
447
  */
440
448
 
449
+ import { IfcServerClient } from '@ifc-lite/server-client';
450
+
441
451
  export { IfcServerClient } from '@ifc-lite/server-client';
442
452
  export type {
443
453
  ServerConfig,
@@ -455,7 +465,6 @@ export const DEFAULT_SERVER_URL = 'http://localhost:3001';
455
465
  * Create a pre-configured client for the local Docker server.
456
466
  */
457
467
  export function createLocalClient(options?: { timeout?: number }) {
458
- const { IfcServerClient } = require('@ifc-lite/server-client');
459
468
  return new IfcServerClient({
460
469
  baseUrl: process.env.SERVER_URL || DEFAULT_SERVER_URL,
461
470
  timeout: options?.timeout ?? 300000,
@@ -20,6 +20,7 @@ export function createThreejsTemplate(targetDir, projectName) {
20
20
  dev: 'vite',
21
21
  build: 'tsc && vite build',
22
22
  preview: 'vite preview',
23
+ postinstall: 'node ./scripts/fix-ifc-lite-geometry-worker.mjs',
23
24
  },
24
25
  dependencies: {
25
26
  '@ifc-lite/geometry': geometryVersion,
@@ -95,6 +96,24 @@ export default defineConfig({
95
96
  `);
96
97
  // src/
97
98
  mkdirSync(join(targetDir, 'src'));
99
+ mkdirSync(join(targetDir, 'scripts'));
100
+ // scripts/fix-ifc-lite-geometry-worker.mjs
101
+ writeFileSync(join(targetDir, 'scripts', 'fix-ifc-lite-geometry-worker.mjs'), `import { existsSync, readFileSync, writeFileSync } from 'node:fs';
102
+ import path from 'node:path';
103
+
104
+ const entryPath = path.join(process.cwd(), 'node_modules', '@ifc-lite', 'geometry', 'dist', 'index.js');
105
+
106
+ if (!existsSync(entryPath)) {
107
+ process.exit(0);
108
+ }
109
+
110
+ const source = readFileSync(entryPath, 'utf8');
111
+ const patched = source.replace(/geometry\\.worker\\.ts/g, 'geometry.worker.js');
112
+
113
+ if (patched !== source) {
114
+ writeFileSync(entryPath, patched);
115
+ }
116
+ `);
98
117
  // src/ifc-to-threejs.ts
99
118
  writeFileSync(join(targetDir, 'src', 'ifc-to-threejs.ts'), `import * as THREE from 'three';
100
119
  import type { MeshData } from '@ifc-lite/geometry';
@@ -1,17 +1,10 @@
1
1
  /**
2
- * Fetch the latest published version of a specific npm package.
2
+ * Fetch the latest installable published version of a specific npm package.
3
3
  * Results are cached so repeated calls for the same package are free.
4
- * Falls back to '^1.0.0' when the registry is unreachable.
4
+ * Throws when the registry is unreachable so scaffolds never emit broken
5
+ * placeholder versions.
5
6
  */
6
7
  export declare function getPackageVersion(packageName: string): string;
7
- /**
8
- * Fetch the latest published version of @ifc-lite/parser from npm.
9
- * Falls back to '^1.0.0' when the registry is unreachable.
10
- *
11
- * @deprecated Use getPackageVersion(packageName) to query each package
12
- * individually and avoid version mismatches between packages.
13
- */
14
- export declare function getLatestVersion(): string;
15
8
  /**
16
9
  * Rewrite the viewer's package.json so it works as a standalone project:
17
10
  * - Set the project name
@@ -3,40 +3,84 @@
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 { execSync } from 'child_process';
6
+ import { execFileSync } from 'child_process';
7
7
  /** Cache of npm-resolved versions to avoid redundant registry queries. */
8
8
  const versionCache = new Map();
9
+ const publishedVersionsCache = new Map();
10
+ const VALID_PACKAGE_NAME = /^(?:@[\w.-]+\/)?[\w.-]+$/;
11
+ const NPM_TIMEOUT_MS = 30000;
12
+ const MAX_VERSION_CANDIDATES = 10;
13
+ function readJsonFromNpm(args) {
14
+ const result = execFileSync('npm', args, {
15
+ stdio: 'pipe',
16
+ timeout: NPM_TIMEOUT_MS,
17
+ }).toString().trim();
18
+ return result ? JSON.parse(result) : {};
19
+ }
20
+ function getPublishedVersions(packageName) {
21
+ if (!VALID_PACKAGE_NAME.test(packageName)) {
22
+ throw new Error(`Invalid package name: ${packageName}`);
23
+ }
24
+ if (publishedVersionsCache.has(packageName)) {
25
+ return publishedVersionsCache.get(packageName);
26
+ }
27
+ const json = readJsonFromNpm(['view', packageName, 'versions', '--json']);
28
+ const versions = Array.isArray(json) ? json : [json];
29
+ const set = new Set(versions.filter((value) => typeof value === 'string'));
30
+ publishedVersionsCache.set(packageName, set);
31
+ return set;
32
+ }
33
+ function extractPinnedVersion(range) {
34
+ const match = range.match(/\d+\.\d+\.\d+/);
35
+ return match ? match[0] : null;
36
+ }
37
+ function getVersionDependencies(packageName, version) {
38
+ const json = readJsonFromNpm(['view', `${packageName}@${version}`, 'dependencies', '--json']);
39
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
40
+ return {};
41
+ }
42
+ return json;
43
+ }
44
+ function isInstallablePublishedVersion(packageName, version) {
45
+ const dependencies = getVersionDependencies(packageName, version);
46
+ for (const [dependencyName, dependencyRange] of Object.entries(dependencies)) {
47
+ if (!dependencyName.startsWith('@ifc-lite/'))
48
+ continue;
49
+ const pinnedVersion = extractPinnedVersion(dependencyRange);
50
+ if (!pinnedVersion)
51
+ continue;
52
+ if (!getPublishedVersions(dependencyName).has(pinnedVersion)) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
9
58
  /**
10
- * Fetch the latest published version of a specific npm package.
59
+ * Fetch the latest installable published version of a specific npm package.
11
60
  * Results are cached so repeated calls for the same package are free.
12
- * Falls back to '^1.0.0' when the registry is unreachable.
61
+ * Throws when the registry is unreachable so scaffolds never emit broken
62
+ * placeholder versions.
13
63
  */
14
64
  export function getPackageVersion(packageName) {
15
65
  if (versionCache.has(packageName)) {
16
66
  return versionCache.get(packageName);
17
67
  }
18
68
  try {
19
- const result = execSync(`npm view ${packageName} version`, { stdio: 'pipe' });
20
- const version = `^${result.toString().trim()}`;
69
+ const versions = [...getPublishedVersions(packageName)];
70
+ const recentVersions = versions.slice(-MAX_VERSION_CANDIDATES).reverse();
71
+ const selectedVersion = recentVersions.find((version) => isInstallablePublishedVersion(packageName, version));
72
+ if (!selectedVersion) {
73
+ throw new Error(`No installable published version found for ${packageName}.`);
74
+ }
75
+ const version = `^${selectedVersion}`;
21
76
  versionCache.set(packageName, version);
22
77
  return version;
23
78
  }
24
- catch {
25
- const fallback = '^1.0.0';
26
- versionCache.set(packageName, fallback);
27
- return fallback;
79
+ catch (cause) {
80
+ throw new Error(`Failed to resolve the latest published version of ${packageName}. ` +
81
+ 'Check your npm registry access and try again.', { cause });
28
82
  }
29
83
  }
30
- /**
31
- * Fetch the latest published version of @ifc-lite/parser from npm.
32
- * Falls back to '^1.0.0' when the registry is unreachable.
33
- *
34
- * @deprecated Use getPackageVersion(packageName) to query each package
35
- * individually and avoid version mismatches between packages.
36
- */
37
- export function getLatestVersion() {
38
- return getPackageVersion('@ifc-lite/parser');
39
- }
40
84
  /**
41
85
  * Rewrite the viewer's package.json so it works as a standalone project:
42
86
  * - Set the project name
@@ -106,6 +150,8 @@ export function fixViteConfig(targetDir) {
106
150
  // Write standalone vite config with WASM support
107
151
  const viteConfig = `import { defineConfig } from 'vite';
108
152
  import react from '@vitejs/plugin-react';
153
+ import wasm from 'vite-plugin-wasm';
154
+ import topLevelAwait from 'vite-plugin-top-level-await';
109
155
  import path from 'path';
110
156
  import { readFileSync } from 'fs';
111
157
 
@@ -114,22 +160,14 @@ const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'));
114
160
  export default defineConfig({
115
161
  plugins: [
116
162
  react(),
117
- {
118
- name: 'wasm-mime-type',
119
- configureServer(server) {
120
- server.middlewares.use((req, res, next) => {
121
- if (req.url?.endsWith('.wasm')) {
122
- res.setHeader('Content-Type', 'application/wasm');
123
- }
124
- next();
125
- });
126
- },
127
- },
163
+ wasm(),
164
+ topLevelAwait(),
128
165
  ],
129
166
  define: {
130
167
  __APP_VERSION__: JSON.stringify(pkg.version),
131
168
  __BUILD_DATE__: JSON.stringify(new Date().toISOString()),
132
169
  __RELEASE_HISTORY__: JSON.stringify([]),
170
+ __PACKAGE_VERSIONS__: JSON.stringify([]),
133
171
  },
134
172
  resolve: {
135
173
  alias: {
@@ -139,6 +177,10 @@ export default defineConfig({
139
177
  server: {
140
178
  port: 3000,
141
179
  open: true,
180
+ headers: {
181
+ 'Cross-Origin-Opener-Policy': 'same-origin',
182
+ 'Cross-Origin-Embedder-Policy': 'credentialless',
183
+ },
142
184
  fs: {
143
185
  allow: ['..'],
144
186
  },
@@ -147,7 +189,18 @@ export default defineConfig({
147
189
  target: 'esnext',
148
190
  },
149
191
  optimizeDeps: {
150
- exclude: ['@duckdb/duckdb-wasm', '@ifc-lite/wasm'],
192
+ exclude: [
193
+ '@duckdb/duckdb-wasm',
194
+ '@ifc-lite/wasm',
195
+ 'parquet-wasm',
196
+ 'quickjs-emscripten',
197
+ '@jitl/quickjs-wasmfile-release-asyncify',
198
+ 'esbuild-wasm',
199
+ ],
200
+ },
201
+ worker: {
202
+ format: 'es',
203
+ plugins: () => [wasm(), topLevelAwait()],
151
204
  },
152
205
  assetsInclude: ['**/*.wasm'],
153
206
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ifc-lite",
3
- "version": "1.14.3",
3
+ "version": "1.14.5",
4
4
  "description": "Create IFC-Lite projects with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,8 +8,7 @@
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "files": [
11
- "dist",
12
- "templates"
11
+ "dist"
13
12
  ],
14
13
  "keywords": [
15
14
  "ifc",
@@ -32,7 +31,7 @@
32
31
  "node": ">=18.0.0"
33
32
  },
34
33
  "scripts": {
35
- "build": "tsc",
36
- "dev": "tsc --watch"
34
+ "build": "pnpm exec tsc",
35
+ "dev": "pnpm exec tsc --watch"
37
36
  }
38
37
  }