@sequent-org/ifc-viewer 1.2.4-ci.35.0 → 1.2.4-ci.37.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/package.json +4 -2
- package/scripts/copy-web-ifc-wasm.mjs +55 -0
- package/src/compat/IFCLoader.js +148 -0
- package/src/ifc/IfcService.js +61 -5
- package/src/main.js +26 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequent-org/ifc-viewer",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.2.4-ci.
|
|
4
|
+
"version": "1.2.4-ci.37.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "IFC 3D model viewer component for web applications - fully self-contained with local IFCLoader",
|
|
7
7
|
"main": "src/index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src/",
|
|
16
|
+
"scripts/",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"access": "public"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
35
|
+
"postinstall": "node scripts/copy-web-ifc-wasm.mjs",
|
|
34
36
|
"dev": "vite",
|
|
35
37
|
"build": "vite build",
|
|
36
38
|
"preview": "vite preview",
|
|
@@ -41,6 +43,6 @@
|
|
|
41
43
|
},
|
|
42
44
|
"dependencies": {
|
|
43
45
|
"three": "^0.149.0",
|
|
44
|
-
"web-ifc": "^0.0.
|
|
46
|
+
"web-ifc": "^0.0.74"
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
function ensureDir(p) {
|
|
6
|
+
fs.mkdirSync(p, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function copyFile(src, dst) {
|
|
10
|
+
ensureDir(path.dirname(dst));
|
|
11
|
+
fs.copyFileSync(src, dst);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function main() {
|
|
15
|
+
// npm sets INIT_CWD to the original working directory where `npm install` was invoked.
|
|
16
|
+
// That is the app root we need to copy into.
|
|
17
|
+
const appRoot = process.env.INIT_CWD || process.cwd();
|
|
18
|
+
|
|
19
|
+
// Resolve actual installed web-ifc location (hoisted or nested).
|
|
20
|
+
const req = createRequire(import.meta.url);
|
|
21
|
+
let webIfcPkgJson = null;
|
|
22
|
+
try {
|
|
23
|
+
webIfcPkgJson = req.resolve('web-ifc/package.json', { paths: [appRoot, process.cwd()] });
|
|
24
|
+
} catch (_) {
|
|
25
|
+
webIfcPkgJson = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const src = webIfcPkgJson
|
|
29
|
+
? path.join(path.dirname(webIfcPkgJson), 'web-ifc.wasm')
|
|
30
|
+
: path.join(appRoot, 'node_modules', 'web-ifc', 'web-ifc.wasm');
|
|
31
|
+
|
|
32
|
+
const dst = path.join(appRoot, 'public', 'wasm', 'web-ifc.wasm');
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(src)) {
|
|
35
|
+
console.error(`[copy-web-ifc-wasm] Source not found: ${src}`);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
copyFile(src, dst);
|
|
41
|
+
|
|
42
|
+
const s = fs.statSync(src);
|
|
43
|
+
const d = fs.statSync(dst);
|
|
44
|
+
console.log('[copy-web-ifc-wasm] OK', {
|
|
45
|
+
appRoot: appRoot,
|
|
46
|
+
src: src,
|
|
47
|
+
dst: dst,
|
|
48
|
+
bytes: d.size,
|
|
49
|
+
sameSize: s.size === d.size,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main();
|
|
54
|
+
|
|
55
|
+
|
package/src/compat/IFCLoader.js
CHANGED
|
@@ -5,6 +5,87 @@ import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUt
|
|
|
5
5
|
|
|
6
6
|
const nullIfcManagerErrorMessage = 'IfcManager is null!';
|
|
7
7
|
|
|
8
|
+
// ===== Diagnostics / optional geometry cleanup (query-params) =====
|
|
9
|
+
function __getQueryParam(name) {
|
|
10
|
+
try {
|
|
11
|
+
if (typeof window === 'undefined' || window?.location?.search == null) return null;
|
|
12
|
+
const params = new URLSearchParams(window.location.search);
|
|
13
|
+
const v = params.get(name);
|
|
14
|
+
return (v == null) ? null : String(v);
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function __parseBoolParam(name) {
|
|
21
|
+
const v = __getQueryParam(name);
|
|
22
|
+
if (v == null) return false;
|
|
23
|
+
const s = v.toLowerCase();
|
|
24
|
+
return v === '1' || s === 'true' || s === 'yes';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function __parseNumberParam(name, fallback) {
|
|
28
|
+
const v = __getQueryParam(name);
|
|
29
|
+
if (v == null || v === '') return fallback;
|
|
30
|
+
const n = Number(v);
|
|
31
|
+
return Number.isFinite(n) ? n : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Removes duplicate triangles from indexed geometry by comparing quantized vertex positions.
|
|
36
|
+
* Query params:
|
|
37
|
+
* ?dedupe=1
|
|
38
|
+
* ?dedupeEps=1e-5
|
|
39
|
+
*/
|
|
40
|
+
function __dedupeTrianglesByPosition(geom, eps) {
|
|
41
|
+
try {
|
|
42
|
+
const posAttr = geom?.attributes?.position;
|
|
43
|
+
const idxAttr = geom?.index;
|
|
44
|
+
if (!posAttr?.array || !idxAttr?.array) return { changed: false, removed: 0, kept: 0 };
|
|
45
|
+
|
|
46
|
+
const pos = posAttr.array;
|
|
47
|
+
const idx = idxAttr.array;
|
|
48
|
+
const triCount = Math.floor(idx.length / 3);
|
|
49
|
+
if (triCount <= 0) return { changed: false, removed: 0, kept: 0 };
|
|
50
|
+
|
|
51
|
+
const q = (v) => Math.round(v / eps);
|
|
52
|
+
const vkey = (vi) => {
|
|
53
|
+
const x = q(pos[vi * 3]);
|
|
54
|
+
const y = q(pos[vi * 3 + 1]);
|
|
55
|
+
const z = q(pos[vi * 3 + 2]);
|
|
56
|
+
return `${x},${y},${z}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const out = new Uint32Array(idx.length);
|
|
61
|
+
let outI = 0;
|
|
62
|
+
let removed = 0;
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < idx.length; i += 3) {
|
|
65
|
+
const a = idx[i], b = idx[i + 1], c = idx[i + 2];
|
|
66
|
+
const ka = vkey(a), kb = vkey(b), kc = vkey(c);
|
|
67
|
+
const arr = [ka, kb, kc];
|
|
68
|
+
arr.sort();
|
|
69
|
+
const k = `${arr[0]}|${arr[1]}|${arr[2]}`;
|
|
70
|
+
if (seen.has(k)) {
|
|
71
|
+
removed++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
seen.add(k);
|
|
75
|
+
out[outI++] = a;
|
|
76
|
+
out[outI++] = b;
|
|
77
|
+
out[outI++] = c;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (removed === 0) return { changed: false, removed: 0, kept: triCount };
|
|
81
|
+
|
|
82
|
+
geom.setIndex(new BufferAttribute(out.subarray(0, outI), 1));
|
|
83
|
+
return { changed: true, removed, kept: Math.floor(outI / 3) };
|
|
84
|
+
} catch (_) {
|
|
85
|
+
return { changed: false, removed: 0, kept: 0 };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
8
89
|
class IFCModel extends Mesh {
|
|
9
90
|
|
|
10
91
|
constructor() {
|
|
@@ -167,6 +248,19 @@ class IFCParser {
|
|
|
167
248
|
this.streamMesh(modelID, mesh);
|
|
168
249
|
});
|
|
169
250
|
this.notifyLoadingEnded();
|
|
251
|
+
|
|
252
|
+
// A/B диагностика: отключить общий merge (между материалами), чтобы проверить,
|
|
253
|
+
// теряются ли треугольники на стадии mergeBufferGeometries(geometries, true).
|
|
254
|
+
// ?mergeMode=perMaterial -> создаём IFCModel-обёртку с дочерними Mesh по материалам
|
|
255
|
+
let mergeMode = 'combined';
|
|
256
|
+
try {
|
|
257
|
+
if (typeof window !== 'undefined' && window?.location?.search != null) {
|
|
258
|
+
const params = new URLSearchParams(window.location.search);
|
|
259
|
+
const v = params.get('mergeMode');
|
|
260
|
+
if (v) mergeMode = String(v);
|
|
261
|
+
}
|
|
262
|
+
} catch (_) {}
|
|
263
|
+
|
|
170
264
|
const geometries = [];
|
|
171
265
|
const materials = [];
|
|
172
266
|
Object.keys(this.geometriesByMaterials).forEach((key) => {
|
|
@@ -175,7 +269,61 @@ class IFCParser {
|
|
|
175
269
|
materials.push(this.geometriesByMaterials[key].material);
|
|
176
270
|
geometries.push(merged);
|
|
177
271
|
});
|
|
272
|
+
|
|
273
|
+
const dedupeEnabled = __parseBoolParam('dedupe');
|
|
274
|
+
const dedupeEps = Math.max(1e-12, __parseNumberParam('dedupeEps', 1e-5));
|
|
275
|
+
|
|
276
|
+
if (mergeMode === 'perMaterial') {
|
|
277
|
+
// ВАЖНО: не делаем общий merge; вместо этого создаём IFCModel (Mesh) без геометрии
|
|
278
|
+
// и добавляем внутрь дочерние Mesh по каждому материалу.
|
|
279
|
+
const root = new IFCModel(new BufferGeometry(), new MeshLambertMaterial({ transparent: true, opacity: 0 }));
|
|
280
|
+
try { root.material.depthWrite = false; } catch (_) {}
|
|
281
|
+
try { root.material.depthTest = false; } catch (_) {}
|
|
282
|
+
root.name = 'ifc-model-perMaterial';
|
|
283
|
+
try { root.userData.__mergeMode = 'perMaterial'; } catch (_) {}
|
|
284
|
+
// Сохраним modelID как у обычного IFCModel, но подменим на текущий
|
|
285
|
+
root.modelID = this.currentModelID;
|
|
286
|
+
root.mesh = root;
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < geometries.length; i++) {
|
|
289
|
+
const g = geometries[i];
|
|
290
|
+
const m = materials[i];
|
|
291
|
+
if (!g || !m) continue;
|
|
292
|
+
if (dedupeEnabled) {
|
|
293
|
+
const r = __dedupeTrianglesByPosition(g, dedupeEps);
|
|
294
|
+
if (r.changed) {
|
|
295
|
+
// eslint-disable-next-line no-console
|
|
296
|
+
console.log('[IFCLoader][dedupe][perMaterial]', { part: i, eps: dedupeEps, removed: r.removed, kept: r.kept });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const child = new Mesh(g, m);
|
|
300
|
+
child.name = `ifc-material-part-${i}`;
|
|
301
|
+
root.add(child);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// В этой ветке не используем BVH по объединённой геометрии (её нет).
|
|
305
|
+
// Память: освободим оригинальные (до-merge) геометрии, но НЕ трогаем merged-геометрию (она в child Mesh).
|
|
306
|
+
try {
|
|
307
|
+
Object.keys(this.geometriesByMaterials).forEach((materialID) => {
|
|
308
|
+
const bucket = this.geometriesByMaterials[materialID];
|
|
309
|
+
const list = bucket?.geometries || [];
|
|
310
|
+
list.forEach((g) => g && g.dispose && g.dispose());
|
|
311
|
+
if (bucket) bucket.geometries = [];
|
|
312
|
+
});
|
|
313
|
+
this.geometriesByMaterials = {};
|
|
314
|
+
} catch (_) {
|
|
315
|
+
// fallback: оставим как есть
|
|
316
|
+
}
|
|
317
|
+
this.state.models[this.currentModelID].mesh = root;
|
|
318
|
+
return root;
|
|
319
|
+
}
|
|
320
|
+
|
|
178
321
|
const combinedGeometry = mergeBufferGeometries(geometries, true);
|
|
322
|
+
if (dedupeEnabled) {
|
|
323
|
+
const r = __dedupeTrianglesByPosition(combinedGeometry, dedupeEps);
|
|
324
|
+
// eslint-disable-next-line no-console
|
|
325
|
+
console.log('[IFCLoader][dedupe][combined]', { eps: dedupeEps, removed: r.removed, kept: r.kept });
|
|
326
|
+
}
|
|
179
327
|
this.cleanUpGeometryMemory(geometries);
|
|
180
328
|
if (this.BVH)
|
|
181
329
|
this.BVH.applyThreeMeshBVH(combinedGeometry);
|
package/src/ifc/IfcService.js
CHANGED
|
@@ -22,6 +22,7 @@ export class IfcService {
|
|
|
22
22
|
this.selectionMaterial = null;
|
|
23
23
|
this.selectionCustomID = 'ifc-selection';
|
|
24
24
|
this.isolateMode = false;
|
|
25
|
+
this._webIfcConfig = null; // для диагностики: фактически применённый конфиг web-ifc
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
init() {
|
|
@@ -104,17 +105,72 @@ export class IfcService {
|
|
|
104
105
|
*/
|
|
105
106
|
_setupWebIfcConfig() {
|
|
106
107
|
try {
|
|
107
|
-
|
|
108
|
+
// ВАЖНО: web-ifc (>=0.0.74) принимает только LoaderSettings (см. web-ifc-api.d.ts).
|
|
109
|
+
// Ранее используемые флаги вроде USE_FAST_BOOLS/SMALL_TRIANGLE_THRESHOLD библиотекой web-ifc не используются.
|
|
110
|
+
const config = {
|
|
108
111
|
COORDINATE_TO_ORIGIN: true,
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
CIRCLE_SEGMENTS: 12,
|
|
113
|
+
// Tolerances: см. web-ifc CreateSettings defaults
|
|
114
|
+
TOLERANCE_PLANE_INTERSECTION: 1e-4,
|
|
115
|
+
TOLERANCE_PLANE_DEVIATION: 1e-4,
|
|
116
|
+
TOLERANCE_BACK_DEVIATION_DISTANCE: 1e-4,
|
|
117
|
+
TOLERANCE_INSIDE_OUTSIDE_PERIMETER: 1e-10,
|
|
118
|
+
TOLERANCE_SCALAR_EQUALITY: 1e-4,
|
|
119
|
+
PLANE_REFIT_ITERATIONS: 1,
|
|
120
|
+
BOOLEAN_UNION_THRESHOLD: 150,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Диагностика / A-B: allow override via query params
|
|
124
|
+
try {
|
|
125
|
+
if (typeof window !== 'undefined' && window?.location?.search != null) {
|
|
126
|
+
const params = new URLSearchParams(window.location.search);
|
|
127
|
+
// ?coordToOrigin=0/1 -> COORDINATE_TO_ORIGIN
|
|
128
|
+
const cto = params.get('coordToOrigin');
|
|
129
|
+
if (cto === '0') config.COORDINATE_TO_ORIGIN = false;
|
|
130
|
+
if (cto === '1') config.COORDINATE_TO_ORIGIN = true;
|
|
131
|
+
|
|
132
|
+
// ?circleSeg=NUMBER -> CIRCLE_SEGMENTS
|
|
133
|
+
const cs = params.get('circleSeg');
|
|
134
|
+
if (cs != null && cs !== '') {
|
|
135
|
+
const n = Number(cs);
|
|
136
|
+
if (Number.isFinite(n) && n >= 3) config.CIRCLE_SEGMENTS = Math.round(n);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Tolerances / iterations / threshold
|
|
140
|
+
const setNum = (key, name, min = -Infinity, max = Infinity) => {
|
|
141
|
+
const raw = params.get(name);
|
|
142
|
+
if (raw == null || raw === '') return;
|
|
143
|
+
const n = Number(raw);
|
|
144
|
+
if (!Number.isFinite(n)) return;
|
|
145
|
+
config[key] = Math.min(max, Math.max(min, n));
|
|
146
|
+
};
|
|
147
|
+
setNum('TOLERANCE_PLANE_INTERSECTION', 'tolPlaneInter', 0);
|
|
148
|
+
setNum('TOLERANCE_PLANE_DEVIATION', 'tolPlaneDev', 0);
|
|
149
|
+
setNum('TOLERANCE_BACK_DEVIATION_DISTANCE', 'tolBackDev', 0);
|
|
150
|
+
setNum('TOLERANCE_INSIDE_OUTSIDE_PERIMETER', 'tolInside', 0);
|
|
151
|
+
setNum('TOLERANCE_SCALAR_EQUALITY', 'tolScalar', 0);
|
|
152
|
+
setNum('PLANE_REFIT_ITERATIONS', 'planeRefit', 0, 1000);
|
|
153
|
+
setNum('BOOLEAN_UNION_THRESHOLD', 'boolUnion', 0, 1e9);
|
|
154
|
+
}
|
|
155
|
+
} catch (_) {}
|
|
156
|
+
|
|
157
|
+
this._webIfcConfig = { ...config };
|
|
158
|
+
this.loader.ifcManager.applyWebIfcConfig?.(config);
|
|
159
|
+
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log('IfcService: web-ifc config applied', this._webIfcConfig);
|
|
113
162
|
} catch (error) {
|
|
114
163
|
console.warn('IfcService: не удалось применить конфигурацию web-ifc:', error.message);
|
|
115
164
|
}
|
|
116
165
|
}
|
|
117
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Возвращает фактически применённый web-ifc config (для диагностики).
|
|
169
|
+
*/
|
|
170
|
+
getWebIfcConfig() {
|
|
171
|
+
return this._webIfcConfig ? { ...this._webIfcConfig } : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
118
174
|
|
|
119
175
|
/**
|
|
120
176
|
* Обработка критических ошибок инициализации
|
package/src/main.js
CHANGED
|
@@ -11,12 +11,17 @@ if (app) {
|
|
|
11
11
|
|
|
12
12
|
// ===== Диагностика (включается через query-параметры) =====
|
|
13
13
|
// ?debugViewer=1 -> window.__viewer = viewer
|
|
14
|
+
// ?debugViewer=1 -> window.__ifcService = ifc
|
|
14
15
|
// ?zoomDebug=1 -> включает логирование zoom-to-cursor
|
|
15
16
|
// ?zoomCursor=0 -> отключает zoom-to-cursor (для сравнения с OrbitControls)
|
|
17
|
+
let __debugViewerEnabled = false;
|
|
18
|
+
let __startupParams = null;
|
|
16
19
|
try {
|
|
17
20
|
const params = new URLSearchParams(location.search);
|
|
21
|
+
__startupParams = params;
|
|
18
22
|
const debugViewer = params.get("debugViewer") === "1" || params.get("zoomDebug") === "1";
|
|
19
23
|
if (debugViewer) {
|
|
24
|
+
__debugViewerEnabled = true;
|
|
20
25
|
// eslint-disable-next-line no-undef
|
|
21
26
|
window.__viewer = viewer;
|
|
22
27
|
}
|
|
@@ -304,8 +309,28 @@ if (app) {
|
|
|
304
309
|
* - realtime-quality UI lock + test UI lock snapshots
|
|
305
310
|
*/
|
|
306
311
|
// IFC загрузка
|
|
307
|
-
|
|
312
|
+
// ?wasm=/wasm/ -> переопределяет директорию, из которой web-ifc будет грузить web-ifc.wasm
|
|
313
|
+
// ВАЖНО: параметр должен указывать ДИРЕКТОРИЮ (web-ifc сам добавляет 'web-ifc.wasm')
|
|
314
|
+
let wasmOverride = null;
|
|
315
|
+
try {
|
|
316
|
+
const p = __startupParams || new URLSearchParams(location.search);
|
|
317
|
+
const w = p.get('wasm');
|
|
318
|
+
if (w != null && w !== '') {
|
|
319
|
+
wasmOverride = String(w);
|
|
320
|
+
// нормализуем: web-ifc ожидает путь-директорию
|
|
321
|
+
if (!wasmOverride.endsWith('/')) wasmOverride += '/';
|
|
322
|
+
}
|
|
323
|
+
} catch (_) {}
|
|
324
|
+
|
|
325
|
+
const ifc = new IfcService(viewer, wasmOverride);
|
|
308
326
|
ifc.init();
|
|
327
|
+
// Экспортируем сервис только после инициализации (иначе ifc ещё не определён)
|
|
328
|
+
if (__debugViewerEnabled) {
|
|
329
|
+
try {
|
|
330
|
+
// eslint-disable-next-line no-undef
|
|
331
|
+
window.__ifcService = ifc;
|
|
332
|
+
} catch (_) {}
|
|
333
|
+
}
|
|
309
334
|
const ifcTreeEl = document.getElementById("ifcTree");
|
|
310
335
|
const ifcInfoEl = document.getElementById("ifcInfo");
|
|
311
336
|
const ifcTree = ifcTreeEl ? new IfcTreeView(ifcTreeEl) : null;
|