@sequent-org/ifc-viewer 1.2.4-ci.45.0 → 1.2.4-ci.47.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.
@@ -0,0 +1,102 @@
1
+ import { Mesh, MeshStandardMaterial } from "three";
2
+ import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
3
+
4
+ /**
5
+ * STL loader (ASCII/Binary).
6
+ *
7
+ * Notes:
8
+ * - STL usually has no material/color information -> we apply a default material.
9
+ * - If normals are missing, we compute vertex normals.
10
+ */
11
+ export class StlModelLoader {
12
+ /**
13
+ * @param {{ rotateXNeg90?: boolean }} [options]
14
+ * rotateXNeg90:
15
+ * - Some STL assets are authored Z-up; viewer is Y-up. This rotates into Y-up.
16
+ */
17
+ constructor(options = {}) {
18
+ this.id = 'stl';
19
+ this.extensions = ['.stl'];
20
+ this._loader = new STLLoader();
21
+ this._rotateXNeg90 = options.rotateXNeg90 !== false; // default true
22
+ }
23
+
24
+ /**
25
+ * @param {File} file
26
+ * @param {any} ctx
27
+ */
28
+ async loadFile(file, ctx) {
29
+ const logger = ctx?.logger || console;
30
+ const name = file?.name || '';
31
+ logger?.log?.('[StlModelLoader] loadFile', { name, size: file?.size });
32
+
33
+ const buf = await file.arrayBuffer();
34
+ const geom = this._loader.parse(buf);
35
+ const mesh = this._makeMesh(geom);
36
+ // Axis fix BEFORE Viewer.replaceWithModel(): ensures bbox/shadowReceiver computed correctly.
37
+ if (this._rotateXNeg90) {
38
+ try {
39
+ mesh.rotation.x = -Math.PI / 2;
40
+ mesh.updateMatrixWorld?.(true);
41
+ } catch (_) {}
42
+ }
43
+
44
+ try {
45
+ logger?.log?.('[StlModelLoader] parsed', {
46
+ name,
47
+ triangles: (geom?.index ? (geom.index.count / 3) : (geom?.attributes?.position ? (geom.attributes.position.count / 3) : undefined)),
48
+ hasNormals: !!geom?.attributes?.normal,
49
+ });
50
+ } catch (_) {}
51
+
52
+ return {
53
+ object3D: mesh,
54
+ format: this.id,
55
+ name,
56
+ replacedInViewer: false,
57
+ capabilities: { kind: 'generic' },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * @param {string} url
63
+ * @param {any} ctx
64
+ */
65
+ async loadUrl(url, ctx) {
66
+ const logger = ctx?.logger || console;
67
+ logger?.log?.('[StlModelLoader] loadUrl', { url });
68
+ const geom = await this._loader.loadAsync(url);
69
+ const mesh = this._makeMesh(geom);
70
+ if (this._rotateXNeg90) {
71
+ try {
72
+ mesh.rotation.x = -Math.PI / 2;
73
+ mesh.updateMatrixWorld?.(true);
74
+ } catch (_) {}
75
+ }
76
+ return {
77
+ object3D: mesh,
78
+ format: this.id,
79
+ name: String(url || ''),
80
+ replacedInViewer: false,
81
+ capabilities: { kind: 'generic' },
82
+ };
83
+ }
84
+
85
+ _makeMesh(geometry) {
86
+ const geom = geometry;
87
+ try {
88
+ if (geom && !geom.attributes?.normal) geom.computeVertexNormals?.();
89
+ geom.computeBoundingBox?.();
90
+ } catch (_) {}
91
+
92
+ const mat = new MeshStandardMaterial({
93
+ color: 0xb0b0b0,
94
+ roughness: 0.85,
95
+ metalness: 0.0,
96
+ });
97
+ const mesh = new Mesh(geom, mat);
98
+ mesh.name = 'stl-mesh';
99
+ return mesh;
100
+ }
101
+ }
102
+
@@ -0,0 +1,205 @@
1
+ import { LoadingManager } from "three";
2
+ import { TDSLoader } from "three/examples/jsm/loaders/TDSLoader.js";
3
+
4
+ /**
5
+ * 3DS loader (TDSLoader) with optional textures via multi-file selection.
6
+ *
7
+ * Multi-file behavior:
8
+ * - User may select:
9
+ * - only .3ds -> load geometry/materials from file, textures may be missing
10
+ * - .3ds + textures -> resolve textures by basename via LoadingManager URL modifier
11
+ */
12
+ export class TdsModelLoader {
13
+ /**
14
+ * @param {{ rotateXNeg90?: boolean }} [options]
15
+ * rotateXNeg90:
16
+ * - Many 3DS assets are effectively Z-up; this rotates them into Y-up (viewer default).
17
+ */
18
+ constructor(options = {}) {
19
+ this.id = '3ds';
20
+ this.extensions = ['.3ds'];
21
+ // For file picker convenience (accept=): allow selecting common texture formats.
22
+ this.associatedExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif', '.tga'];
23
+ this._rotateXNeg90 = options.rotateXNeg90 !== false; // default true
24
+ }
25
+
26
+ async loadFile(file, ctx) {
27
+ return await this.loadFiles([file], ctx);
28
+ }
29
+
30
+ /**
31
+ * @param {File[]|FileList} files
32
+ * @param {any} ctx
33
+ */
34
+ async loadFiles(files, ctx) {
35
+ const logger = ctx?.logger || console;
36
+ const arr = Array.from(files || []).filter(Boolean);
37
+ if (!arr.length) throw new Error('TdsModelLoader: no files');
38
+
39
+ const mainFile = arr.find((f) => this._isExt(f?.name, '.3ds'));
40
+ if (!mainFile) {
41
+ throw new Error(`TdsModelLoader: .3ds not found in selection: ${arr.map((f) => f?.name).filter(Boolean).join(', ')}`);
42
+ }
43
+
44
+ const fileMap = this._buildFileMap(arr);
45
+ const urlMap = new Map();
46
+ const revokeAll = () => {
47
+ try { for (const u of urlMap.values()) URL.revokeObjectURL(u); } catch (_) {}
48
+ };
49
+ const getBlobUrl = (key) => {
50
+ const k = String(key || '').toLowerCase();
51
+ if (!k) return null;
52
+ if (urlMap.has(k)) return urlMap.get(k);
53
+ const f = fileMap.get(k);
54
+ if (!f) return null;
55
+ const u = URL.createObjectURL(f);
56
+ urlMap.set(k, u);
57
+ return u;
58
+ };
59
+
60
+ const manager = new LoadingManager();
61
+ /** @type {Set<string>} */
62
+ const failedUrls = new Set();
63
+ /** @type {Set<string>} */
64
+ const requestedBasenames = new Set();
65
+
66
+ manager.onError = (url) => {
67
+ try {
68
+ const raw = String(url || '');
69
+ const clean = raw.split('#')[0].split('?')[0];
70
+ const parts = clean.replace(/\\/g, '/').split('/');
71
+ const last = (parts[parts.length - 1] || '').trim();
72
+ if (last) failedUrls.add(last);
73
+ } catch (_) {}
74
+ };
75
+
76
+ manager.setURLModifier((url) => {
77
+ try {
78
+ const raw = String(url || '');
79
+ const clean = raw.split('#')[0].split('?')[0];
80
+ const parts = clean.replace(/\\/g, '/').split('/');
81
+ const last = (parts[parts.length - 1] || '').trim();
82
+ if (!last) return raw;
83
+ requestedBasenames.add(last);
84
+ const blob = getBlobUrl(last);
85
+ return blob || raw;
86
+ } catch (_) {
87
+ return url;
88
+ }
89
+ });
90
+
91
+ try {
92
+ logger?.log?.('[TdsModelLoader] loadFiles', {
93
+ file: mainFile.name,
94
+ files: arr.map((f) => f?.name).filter(Boolean),
95
+ });
96
+
97
+ const buf = await mainFile.arrayBuffer();
98
+ const loader = new TDSLoader(manager);
99
+
100
+ // parse() accepts ArrayBuffer
101
+ const obj = loader.parse(buf, '');
102
+
103
+ // Axis fix BEFORE Viewer.replaceWithModel(): ensures bbox/shadowReceiver computed correctly.
104
+ if (this._rotateXNeg90) {
105
+ try {
106
+ obj.rotation.x = -Math.PI / 2;
107
+ obj.updateMatrixWorld?.(true);
108
+ } catch (_) {}
109
+ }
110
+
111
+ // Wait for async texture loads (if any) before revoking blob URLs.
112
+ await this._waitManagerIdle(manager, 2500);
113
+
114
+ // Diagnostics: mesh/material/texture stats
115
+ try {
116
+ let meshes = 0;
117
+ let mats = 0;
118
+ let withMap = 0;
119
+ obj.traverse?.((n) => {
120
+ if (!n?.isMesh) return;
121
+ meshes++;
122
+ const m = n.material;
123
+ const arrM = Array.isArray(m) ? m : [m];
124
+ for (const mi of arrM) {
125
+ if (!mi) continue;
126
+ mats++;
127
+ if (mi.map) withMap++;
128
+ }
129
+ });
130
+ logger?.log?.('[TdsModelLoader] parsed', { file: mainFile.name, meshes, materials: mats, withMap });
131
+ } catch (_) {}
132
+
133
+ const missingAll = Array.from(new Set([...Array.from(failedUrls.values())]));
134
+ if (missingAll.length) {
135
+ logger?.warn?.('[TdsModelLoader] missing assets (select these files too):', missingAll);
136
+ } else {
137
+ // If no textures attached, warn with requested basenames (if any)
138
+ try {
139
+ let withMap = 0;
140
+ obj.traverse?.((n) => {
141
+ if (!n?.isMesh) return;
142
+ const m = n.material;
143
+ const arrM = Array.isArray(m) ? m : [m];
144
+ for (const mi of arrM) if (mi?.map) withMap++;
145
+ });
146
+ if (withMap === 0 && requestedBasenames.size) {
147
+ logger?.warn?.('[TdsModelLoader] Textures were referenced but not attached. Ensure you selected all referenced image files.', {
148
+ requested: Array.from(requestedBasenames.values()).slice(0, 30),
149
+ });
150
+ }
151
+ } catch (_) {}
152
+ }
153
+
154
+ return {
155
+ object3D: obj,
156
+ format: this.id,
157
+ name: mainFile.name,
158
+ replacedInViewer: false,
159
+ capabilities: {
160
+ kind: 'generic',
161
+ missingAssets: missingAll,
162
+ },
163
+ };
164
+ } finally {
165
+ revokeAll();
166
+ }
167
+ }
168
+
169
+ _isExt(name, ext) {
170
+ const n = String(name || '').toLowerCase();
171
+ return n.endsWith(String(ext).toLowerCase());
172
+ }
173
+
174
+ _buildFileMap(arr) {
175
+ // Map by basename only (common in texture references)
176
+ const map = new Map();
177
+ for (const f of arr) {
178
+ const full = String(f?.name || '');
179
+ if (!full) continue;
180
+ const base = (full.split(/[/\\]/).pop() || full).toLowerCase();
181
+ if (!map.has(base)) map.set(base, f);
182
+ }
183
+ return map;
184
+ }
185
+
186
+ _waitManagerIdle(manager, timeoutMs = 2000) {
187
+ return new Promise((resolve) => {
188
+ let done = false;
189
+ const finish = () => {
190
+ if (done) return;
191
+ done = true;
192
+ resolve();
193
+ };
194
+ try {
195
+ const prevOnLoad = manager.onLoad;
196
+ manager.onLoad = () => {
197
+ try { prevOnLoad?.(); } catch (_) {}
198
+ finish();
199
+ };
200
+ } catch (_) {}
201
+ setTimeout(finish, Math.max(0, Number(timeoutMs) || 0));
202
+ });
203
+ }
204
+ }
205
+