@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sequent-org/ifc-viewer",
3
3
  "private": false,
4
- "version": "1.2.4-ci.35.0",
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.39"
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
+
@@ -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);
@@ -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
- this.loader.ifcManager.applyWebIfcConfig?.({
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
- USE_FAST_BOOLS: true,
110
- // Порог игнорирования очень мелких полигонов (уменьшаем шум)
111
- SMALL_TRIANGLE_THRESHOLD: 1e-9,
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
- const ifc = new IfcService(viewer);
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;