create-ifc-lite 1.11.0 → 1.11.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/dist/index.js +11 -3
- package/dist/templates/threejs.d.ts +5 -0
- package/dist/templates/threejs.js +258 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import { existsSync, mkdirSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { createBasicTemplate } from './templates/basic.js';
|
|
8
|
+
import { createThreejsTemplate } from './templates/threejs.js';
|
|
8
9
|
import { createServerTemplate } from './templates/server.js';
|
|
9
10
|
import { createServerNativeTemplate } from './templates/server-native.js';
|
|
10
11
|
import { fixViewerTemplate } from './utils/config-fixers.js';
|
|
11
12
|
import { downloadViewer } from './utils/download.js';
|
|
12
13
|
const TEMPLATES = {
|
|
13
14
|
basic: 'basic',
|
|
15
|
+
threejs: 'threejs',
|
|
14
16
|
react: 'react',
|
|
15
17
|
server: 'server',
|
|
16
18
|
'server-native': 'server-native',
|
|
@@ -28,12 +30,14 @@ function printUsage() {
|
|
|
28
30
|
|
|
29
31
|
Examples:
|
|
30
32
|
npx create-ifc-lite my-ifc-app
|
|
33
|
+
npx create-ifc-lite my-viewer --template threejs
|
|
31
34
|
npx create-ifc-lite my-viewer --template react
|
|
32
35
|
npx create-ifc-lite my-backend --template server
|
|
33
36
|
npx create-ifc-lite my-backend --template server-native
|
|
34
37
|
|
|
35
38
|
Templates:
|
|
36
39
|
basic Minimal TypeScript project for parsing IFC files
|
|
40
|
+
threejs Three.js viewer (WebGL, no WebGPU required)
|
|
37
41
|
react Full-featured React + Vite viewer with WebGPU rendering
|
|
38
42
|
server Docker-based IFC processing server with TypeScript client
|
|
39
43
|
server-native Native binary server (no Docker required)
|
|
@@ -56,7 +60,7 @@ async function main() {
|
|
|
56
60
|
template = t;
|
|
57
61
|
}
|
|
58
62
|
else {
|
|
59
|
-
console.error(`Invalid template: ${t}. Available: basic, react, server, server-native`);
|
|
63
|
+
console.error(`Invalid template: ${t}. Available: basic, threejs, react, server, server-native`);
|
|
60
64
|
process.exit(1);
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -70,7 +74,11 @@ async function main() {
|
|
|
70
74
|
process.exit(1);
|
|
71
75
|
}
|
|
72
76
|
console.log(`\n Creating IFC-Lite project in ${targetDir}...\n`);
|
|
73
|
-
if (template === '
|
|
77
|
+
if (template === 'threejs') {
|
|
78
|
+
mkdirSync(targetDir, { recursive: true });
|
|
79
|
+
createThreejsTemplate(targetDir, projectName);
|
|
80
|
+
}
|
|
81
|
+
else if (template === 'react') {
|
|
74
82
|
// Download the actual viewer from GitHub
|
|
75
83
|
const success = await downloadViewer(targetDir, projectName);
|
|
76
84
|
if (success) {
|
|
@@ -108,7 +116,7 @@ async function main() {
|
|
|
108
116
|
}
|
|
109
117
|
else {
|
|
110
118
|
console.log(` npm install`);
|
|
111
|
-
if (template === 'react') {
|
|
119
|
+
if (template === 'react' || template === 'threejs') {
|
|
112
120
|
console.log(` npm run dev`);
|
|
113
121
|
}
|
|
114
122
|
else {
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Scaffold a Three.js IFC viewer project using @ifc-lite/geometry.
|
|
9
|
+
* No WebGPU required — renders via Three.js WebGLRenderer.
|
|
10
|
+
*/
|
|
11
|
+
export function createThreejsTemplate(targetDir, projectName) {
|
|
12
|
+
const geometryVersion = getPackageVersion('@ifc-lite/geometry');
|
|
13
|
+
// package.json
|
|
14
|
+
writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
|
|
15
|
+
name: projectName,
|
|
16
|
+
version: '0.1.0',
|
|
17
|
+
private: true,
|
|
18
|
+
type: 'module',
|
|
19
|
+
scripts: {
|
|
20
|
+
dev: 'vite',
|
|
21
|
+
build: 'tsc && vite build',
|
|
22
|
+
preview: 'vite preview',
|
|
23
|
+
},
|
|
24
|
+
dependencies: {
|
|
25
|
+
'@ifc-lite/geometry': geometryVersion,
|
|
26
|
+
three: '^0.183.0',
|
|
27
|
+
},
|
|
28
|
+
devDependencies: {
|
|
29
|
+
'@types/three': '^0.183.0',
|
|
30
|
+
typescript: '^5.3.0',
|
|
31
|
+
vite: '^7.0.0',
|
|
32
|
+
},
|
|
33
|
+
}, null, 2));
|
|
34
|
+
// tsconfig.json
|
|
35
|
+
writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
|
|
36
|
+
compilerOptions: {
|
|
37
|
+
target: 'ES2022',
|
|
38
|
+
module: 'ESNext',
|
|
39
|
+
moduleResolution: 'bundler',
|
|
40
|
+
strict: true,
|
|
41
|
+
esModuleInterop: true,
|
|
42
|
+
skipLibCheck: true,
|
|
43
|
+
outDir: 'dist',
|
|
44
|
+
},
|
|
45
|
+
include: ['src'],
|
|
46
|
+
}, null, 2));
|
|
47
|
+
// vite.config.ts
|
|
48
|
+
writeFileSync(join(targetDir, 'vite.config.ts'), `import { defineConfig } from 'vite';
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
optimizeDeps: {
|
|
52
|
+
exclude: ['@ifc-lite/wasm'],
|
|
53
|
+
},
|
|
54
|
+
server: {
|
|
55
|
+
headers: {
|
|
56
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
57
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
`);
|
|
62
|
+
// index.html
|
|
63
|
+
writeFileSync(join(targetDir, 'index.html'), `<!DOCTYPE html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="UTF-8" />
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
68
|
+
<title>${projectName}</title>
|
|
69
|
+
<style>
|
|
70
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
71
|
+
body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #fff; overflow: hidden; }
|
|
72
|
+
#app { display: flex; flex-direction: column; height: 100vh; }
|
|
73
|
+
header { padding: 0.75rem 1rem; background: #16213e; display: flex; gap: 1rem; align-items: center; }
|
|
74
|
+
header h1 { font-size: 1rem; font-weight: 600; }
|
|
75
|
+
#file-input { padding: 0.4rem 0.8rem; background: #0f3460; border: 1px solid #533483; border-radius: 4px; color: #fff; cursor: pointer; }
|
|
76
|
+
#status { color: #888; font-size: 0.85rem; }
|
|
77
|
+
#canvas-container { flex: 1; position: relative; }
|
|
78
|
+
canvas { display: block; width: 100%; height: 100%; }
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<div id="app">
|
|
83
|
+
<header>
|
|
84
|
+
<h1>${projectName}</h1>
|
|
85
|
+
<input type="file" id="file-input" accept=".ifc" />
|
|
86
|
+
<span id="status">Drop an IFC file to view</span>
|
|
87
|
+
</header>
|
|
88
|
+
<div id="canvas-container">
|
|
89
|
+
<canvas id="viewer"></canvas>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<script type="module" src="/src/main.ts"></script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
95
|
+
`);
|
|
96
|
+
// src/
|
|
97
|
+
mkdirSync(join(targetDir, 'src'));
|
|
98
|
+
// src/ifc-to-threejs.ts
|
|
99
|
+
writeFileSync(join(targetDir, 'src', 'ifc-to-threejs.ts'), `import * as THREE from 'three';
|
|
100
|
+
import type { MeshData } from '@ifc-lite/geometry';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert a single MeshData into a Three.js Mesh.
|
|
104
|
+
*/
|
|
105
|
+
export function meshDataToThree(mesh: MeshData): THREE.Mesh {
|
|
106
|
+
const geometry = new THREE.BufferGeometry();
|
|
107
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(mesh.positions, 3));
|
|
108
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(mesh.normals, 3));
|
|
109
|
+
geometry.setIndex(new THREE.BufferAttribute(mesh.indices, 1));
|
|
110
|
+
|
|
111
|
+
const [r, g, b, a] = mesh.color;
|
|
112
|
+
const material = new THREE.MeshStandardMaterial({
|
|
113
|
+
color: new THREE.Color(r, g, b),
|
|
114
|
+
transparent: a < 1,
|
|
115
|
+
opacity: a,
|
|
116
|
+
side: a < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
117
|
+
depthWrite: a >= 1,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const threeMesh = new THREE.Mesh(geometry, material);
|
|
121
|
+
threeMesh.userData.expressId = mesh.expressId;
|
|
122
|
+
return threeMesh;
|
|
123
|
+
}
|
|
124
|
+
`);
|
|
125
|
+
// src/main.ts
|
|
126
|
+
writeFileSync(join(targetDir, 'src', 'main.ts'), `import * as THREE from 'three';
|
|
127
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
128
|
+
import { GeometryProcessor } from '@ifc-lite/geometry';
|
|
129
|
+
import { meshDataToThree } from './ifc-to-threejs.js';
|
|
130
|
+
|
|
131
|
+
const canvas = document.getElementById('viewer') as HTMLCanvasElement | null;
|
|
132
|
+
const fileInput = document.getElementById('file-input') as HTMLInputElement | null;
|
|
133
|
+
const status = document.getElementById('status');
|
|
134
|
+
if (!canvas || !fileInput || !status) {
|
|
135
|
+
throw new Error('Required DOM elements not found: viewer, file-input, or status');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Three.js setup
|
|
139
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
140
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
141
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
142
|
+
|
|
143
|
+
const scene = new THREE.Scene();
|
|
144
|
+
scene.background = new THREE.Color(0x1a1a2e);
|
|
145
|
+
|
|
146
|
+
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 10000);
|
|
147
|
+
camera.position.set(20, 15, 20);
|
|
148
|
+
|
|
149
|
+
const controls = new OrbitControls(camera, canvas);
|
|
150
|
+
controls.enableDamping = false;
|
|
151
|
+
|
|
152
|
+
// Lighting
|
|
153
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
|
154
|
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
155
|
+
dirLight.position.set(50, 80, 50);
|
|
156
|
+
scene.add(dirLight);
|
|
157
|
+
|
|
158
|
+
// IFC-Lite geometry processor
|
|
159
|
+
const geometry = new GeometryProcessor();
|
|
160
|
+
|
|
161
|
+
// Resize
|
|
162
|
+
function resize() {
|
|
163
|
+
const container = canvas.parentElement ?? document.body;
|
|
164
|
+
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
165
|
+
camera.aspect = container.clientWidth / container.clientHeight;
|
|
166
|
+
camera.updateProjectionMatrix();
|
|
167
|
+
}
|
|
168
|
+
window.addEventListener('resize', resize);
|
|
169
|
+
resize();
|
|
170
|
+
|
|
171
|
+
// Render loop
|
|
172
|
+
(function animate() {
|
|
173
|
+
requestAnimationFrame(animate);
|
|
174
|
+
controls.update();
|
|
175
|
+
renderer.render(scene, camera);
|
|
176
|
+
})();
|
|
177
|
+
|
|
178
|
+
// File loading
|
|
179
|
+
fileInput.addEventListener('change', async () => {
|
|
180
|
+
const file = fileInput.files?.[0];
|
|
181
|
+
if (!file) return;
|
|
182
|
+
|
|
183
|
+
status.textContent = 'Loading ' + file.name + '...';
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await geometry.init();
|
|
187
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
188
|
+
|
|
189
|
+
// Clear previous model and release GPU resources
|
|
190
|
+
const toRemove = scene.children.filter(
|
|
191
|
+
(c) => c instanceof THREE.Mesh || c instanceof THREE.Group
|
|
192
|
+
);
|
|
193
|
+
for (const obj of toRemove) {
|
|
194
|
+
scene.remove(obj);
|
|
195
|
+
obj.traverse((child) => {
|
|
196
|
+
if (child instanceof THREE.Mesh) {
|
|
197
|
+
child.geometry.dispose();
|
|
198
|
+
if (Array.isArray(child.material)) {
|
|
199
|
+
child.material.forEach((m) => m.dispose());
|
|
200
|
+
} else {
|
|
201
|
+
child.material.dispose();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Stream geometry
|
|
208
|
+
let count = 0;
|
|
209
|
+
for await (const event of geometry.processStreaming(buffer)) {
|
|
210
|
+
if (event.type === 'batch') {
|
|
211
|
+
for (const mesh of event.meshes) {
|
|
212
|
+
scene.add(meshDataToThree(mesh));
|
|
213
|
+
}
|
|
214
|
+
count += event.meshes.length;
|
|
215
|
+
status.textContent = 'Loaded ' + count + ' meshes...';
|
|
216
|
+
}
|
|
217
|
+
if (event.type === 'complete') {
|
|
218
|
+
// Fit camera
|
|
219
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
220
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
221
|
+
const size = box.getSize(new THREE.Vector3());
|
|
222
|
+
const d = Math.max(size.x, size.y, size.z) * 1.5;
|
|
223
|
+
camera.position.set(center.x + d * 0.5, center.y + d * 0.5, center.z + d * 0.5);
|
|
224
|
+
controls.target.copy(center);
|
|
225
|
+
controls.update();
|
|
226
|
+
camera.near = Math.max(size.x, size.y, size.z) * 0.001;
|
|
227
|
+
camera.far = Math.max(size.x, size.y, size.z) * 100;
|
|
228
|
+
camera.updateProjectionMatrix();
|
|
229
|
+
|
|
230
|
+
status.textContent = file.name + ' — ' + event.totalMeshes + ' meshes';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
status.textContent = 'Error: ' + err.message;
|
|
235
|
+
console.error(err);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
`);
|
|
239
|
+
// README
|
|
240
|
+
writeFileSync(join(targetDir, 'README.md'), `# ${projectName}
|
|
241
|
+
|
|
242
|
+
Three.js IFC viewer using [IFC-Lite](https://github.com/louistrue/ifc-lite).
|
|
243
|
+
|
|
244
|
+
## Quick Start
|
|
245
|
+
|
|
246
|
+
\`\`\`bash
|
|
247
|
+
npm install
|
|
248
|
+
npm run dev
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
Open http://localhost:5173 and drop an IFC file.
|
|
252
|
+
|
|
253
|
+
## Learn More
|
|
254
|
+
|
|
255
|
+
- [Three.js Integration Guide](https://louistrue.github.io/ifc-lite/tutorials/threejs-integration/)
|
|
256
|
+
- [IFC-Lite Documentation](https://louistrue.github.io/ifc-lite/)
|
|
257
|
+
`);
|
|
258
|
+
}
|