@sequent-org/ifc-viewer 1.0.2-ci.2.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/fragments.html ADDED
@@ -0,0 +1,46 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Fragments Viewer (That Open)</title>
7
+ <style>
8
+ html, body { height: 100%; margin: 0; }
9
+ #app { height: 100%; display: flex; }
10
+ /* simple fallback layout */
11
+ .fallback { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; padding: 16px; }
12
+ </style>
13
+ <!-- Опционально: свои стили. CSS пакета опустим, чтобы избежать 404 с CDN. -->
14
+ </head>
15
+ <body>
16
+ <div id="app">
17
+ <!-- Если компоненты не подхватятся, пользователь увидит fallback -->
18
+ <div class="fallback">
19
+ <h3>Загрузка компонентов...</h3>
20
+ <p>Если интерфейс не появился, проверьте доступность CDN и перезагрузите страницу.</p>
21
+ </div>
22
+ </div>
23
+
24
+ <script type="module">
25
+ // Подключаем бандл через esm.sh с согласованной версией three (>=0.175)
26
+ import 'https://esm.sh/@thatopen/components@3.1.3?bundle&deps=three@0.176.0';
27
+ const app = document.querySelector('#app');
28
+ app.innerHTML = `
29
+ <to-app style="width:100%;height:100%">
30
+ <to-toolbar slot="toolbar"></to-toolbar>
31
+ <to-scene slot="main" id="scene"></to-scene>
32
+ <to-panels-right slot="right">
33
+ <to-panel-properties></to-panel-properties>
34
+ <to-panel-sections></to-panel-sections>
35
+ <to-panel-measure></to-panel-measure>
36
+ </to-panels-right>
37
+ <to-panels-left slot="left">
38
+ <to-panel-loader></to-panel-loader>
39
+ </to-panels-left>
40
+ </to-app>
41
+ `;
42
+ </script>
43
+ </body>
44
+ </html>
45
+
46
+
package/index.html ADDED
@@ -0,0 +1,101 @@
1
+ <!doctype html>
2
+ <html lang="ru" data-theme="light" class="h-dvh overflow-hidden">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="stylesheet" href="/src/style.css" />
7
+ <title>3D </title>
8
+ </head>
9
+ <body class="h-full flex flex-col overflow-hidden bg-base-100 text-base-content">
10
+ <div class="navbar bg-neutral text-neutral-content shrink-0">
11
+ <div class="navbar-start">
12
+ <div class="dropdown">
13
+ <div tabindex="0" role="button" class="btn btn-ghost lg:hidden" aria-label="Открыть меню">
14
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
15
+ </div>
16
+ <ul tabindex="0" class="menu menu-sm dropdown-content bg-base-200 rounded-box z-10 mt-3 w-52 p-2 shadow">
17
+ <li><a>Главная</a></li>
18
+ <li><a>О нас</a></li>
19
+ <li><a>Контакты</a></li>
20
+ </ul>
21
+ </div>
22
+ <a class="btn btn-ghost text-xl">3D Viewer</a>
23
+ </div>
24
+ <div class="navbar-end hidden lg:flex gap-2">
25
+ <div class="join mr-2">
26
+ <button class="btn join-item" id="qualLow">Low</button>
27
+ <button class="btn join-item btn-active" id="qualMed">Med</button>
28
+ <button class="btn join-item" id="qualHigh">High</button>
29
+ </div>
30
+ <div class="join mr-2">
31
+ <button class="btn join-item" id="toggleEdges">Edges</button>
32
+ <button class="btn join-item" id="toggleShading">Flat</button>
33
+ </div>
34
+ <div class="join mr-2">
35
+ <button class="btn join-item" id="clipX">Clip X</button>
36
+ <button class="btn join-item" id="clipY">Clip Y</button>
37
+ <button class="btn join-item" id="clipZ">Clip Z</button>
38
+ </div>
39
+ <button id="uploadBtn" class="btn bg-white text-black">Загрузить модель</button>
40
+ </div>
41
+ </div>
42
+
43
+ <div id="app" class="w-full flex-1 relative min-h-0 overflow-hidden">
44
+ <!-- Кнопка открытия левой панели -->
45
+ <button id="sidebarToggle" class="btn btn-square btn-sm absolute left-3 top-3 z-30">☰</button>
46
+
47
+ <!-- ЛЕВАЯ ПАНЕЛЬ IFC -->
48
+ <div id="ifcSidebar" class="absolute inset-y-0 left-0 z-30 w-80 max-w-[80%] -translate-x-full transition-transform duration-300 ease-in-out pointer-events-none">
49
+ <div class="h-full bg-base-200/95 backdrop-blur-md shadow-xl border-r border-base-300 flex flex-col">
50
+ <div class="p-3 flex items-center justify-between border-b border-base-300">
51
+ <div class="font-semibold">IFC Structure</div>
52
+ <button id="sidebarClose" class="btn btn-ghost btn-sm">✕</button>
53
+ </div>
54
+ <div id="ifcInfo" class="p-3 text-xs opacity-80 border-b border-base-300"></div>
55
+ <div id="ifcActions" class="px-3 py-2 border-b border-base-300 flex items-center gap-2">
56
+ <label class="label cursor-pointer justify-start gap-2">
57
+ <input id="ifcIsolateToggle" type="checkbox" class="toggle toggle-sm" />
58
+ <span class="label-text text-xs">Скрывать остальные</span>
59
+ </label>
60
+ </div>
61
+ <div id="ifcTree" class="flex-1 overflow-auto p-2"></div>
62
+ </div>
63
+ </div>
64
+ <div id="preloader" class="fixed inset-0 z-50 bg-black text-white opacity-100 transition-opacity duration-500 will-change-[opacity]">
65
+ <div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[150px] h-[150px]">
66
+ <svg viewBox="0 0 100 100" class="w-full h-full">
67
+ <g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
68
+ <path d="M 21 40 V 59">
69
+ <animateTransform attributeName="transform" attributeType="XML" type="rotate" values="0 21 59; 180 21 59" dur="2s" repeatCount="indefinite" />
70
+ </path>
71
+ <path d="M 79 40 V 59">
72
+ <animateTransform attributeName="transform" attributeType="XML" type="rotate" values="0 79 59; -180 79 59" dur="2s" repeatCount="indefinite" />
73
+ </path>
74
+ <path d="M 50 21 V 40">
75
+ <animate attributeName="d" values="M 50 21 V 40; M 50 59 V 40" dur="2s" repeatCount="indefinite" />
76
+ </path>
77
+ <path d="M 50 60 V 79">
78
+ <animate attributeName="d" values="M 50 60 V 79; M 50 98 V 79" dur="2s" repeatCount="indefinite" />
79
+ </path>
80
+ <path d="M 50 21 L 79 40 L 50 60 L 21 40 Z">
81
+ <animate attributeName="stroke" values="rgba(255,255,255,1); rgba(100,100,100,0)" dur="2s" repeatCount="indefinite" />
82
+ </path>
83
+ <path d="M 50 40 L 79 59 L 50 79 L 21 59 Z"/>
84
+ <path d="M 50 59 L 79 78 L 50 98 L 21 78 Z">
85
+ <animate attributeName="stroke" values="rgba(100,100,100,0); rgba(255,255,255,1)" dur="2s" repeatCount="indefinite" />
86
+ </path>
87
+ <animateTransform attributeName="transform" attributeType="XML" type="translate" values="0 0; 0 -19" dur="2s" repeatCount="indefinite" />
88
+ </g>
89
+ </svg>
90
+ </div>
91
+ </div>
92
+ <input id="ifcInput" type="file" accept=".ifc,.ifczip,.ifs" class="hidden" />
93
+ <div id="zoomPanel" class="absolute bottom-4 right-4 join invisible">
94
+ <button id="zoomOut" class="btn join-item">-</button>
95
+ <div id="zoomValue" class="btn join-item btn-ghost select-none">100%</div>
96
+ <button id="zoomIn" class="btn join-item">+</button>
97
+ </div>
98
+ </div>
99
+ <script type="module" src="/src/main.js"></script>
100
+ </body>
101
+ </html>
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@sequent-org/ifc-viewer",
3
+ "private": false,
4
+ "version": "1.0.2-ci.2.0",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "dev": "vite",
11
+ "build": "vite build",
12
+ "preview": "vite preview"
13
+ },
14
+ "devDependencies": {
15
+ "@tailwindcss/postcss": "^4.1.13",
16
+ "autoprefixer": "^10.4.21",
17
+ "postcss": "^8.5.6",
18
+ "tailwindcss": "^4.1.13",
19
+ "vite": "^7.1.2"
20
+ },
21
+ "dependencies": {
22
+ "daisyui": "^5.1.12",
23
+ "three": "^0.149.0",
24
+ "web-ifc": "^0.0.39",
25
+ "web-ifc-three": "^0.0.126"
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ // postcss.config.cjs
2
+ module.exports = {
3
+ plugins: {
4
+ "@tailwindcss/postcss": {},
5
+ autoprefixer: {},
6
+ },
7
+ };
Binary file
@@ -0,0 +1,39 @@
1
+ // Совместимость для web-ifc-three с three@0.149
2
+ // В этой версии нет mergeGeometries, только mergeBufferGeometries.
3
+ // Некоторые геометрии могут быть без индексов — подготовим их.
4
+
5
+ import * as THREE from 'three';
6
+ import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
7
+
8
+ export function mergeGeometries(geometries, useGroups = false) {
9
+ if (!Array.isArray(geometries) || geometries.length === 0) return null;
10
+ const prepared = [];
11
+ for (const g of geometries) {
12
+ if (!g) continue;
13
+ const geom = g.isBufferGeometry ? g : new THREE.BufferGeometry().copy(g);
14
+ // Гарантируем наличие индекса, иначе mergeBufferGeometries может падать
15
+ if (!geom.index) {
16
+ const position = geom.getAttribute('position');
17
+ if (!position) continue;
18
+ const count = position.count;
19
+ const IndexArray = count > 65535 ? Uint32Array : Uint16Array;
20
+ const indices = new IndexArray(count);
21
+ for (let i = 0; i < count; i++) indices[i] = i;
22
+ geom.setIndex(new THREE.BufferAttribute(indices, 1));
23
+ }
24
+ prepared.push(geom);
25
+ }
26
+ if (prepared.length === 0) {
27
+ const empty = new THREE.BufferGeometry();
28
+ empty.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
29
+ empty.setIndex(new THREE.BufferAttribute(new Uint16Array(0), 1));
30
+ return empty;
31
+ }
32
+ const merged = mergeBufferGeometries(prepared, useGroups);
33
+ if (merged) return merged;
34
+ const fallback = new THREE.BufferGeometry();
35
+ fallback.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
36
+ fallback.setIndex(new THREE.BufferAttribute(new Uint16Array(0), 1));
37
+ return fallback;
38
+ }
39
+
@@ -0,0 +1,22 @@
1
+ // Подключаем That Open Components из node_modules (нужна установка пакета)
2
+ import '@thatopen/components';
3
+
4
+ const app = document.querySelector('#app');
5
+ if (app) {
6
+ app.innerHTML = `
7
+ <to-app style="width:100%;height:100%">
8
+ <to-toolbar slot="toolbar"></to-toolbar>
9
+ <to-scene slot="main" id="scene"></to-scene>
10
+ <to-panels-right slot="right">
11
+ <to-panel-properties></to-panel-properties>
12
+ <to-panel-sections></to-panel-sections>
13
+ <to-panel-measure></to-panel-measure>
14
+ </to-panels-right>
15
+ <to-panels-left slot="left">
16
+ <to-panel-loader></to-panel-loader>
17
+ </to-panels-left>
18
+ </to-app>
19
+ `;
20
+ }
21
+
22
+
@@ -0,0 +1,268 @@
1
+ // Сервис загрузки IFC моделей и добавления их в сцену three.js
2
+ // Требует three@^0.149 и web-ifc-three совместимой версии
3
+
4
+ import { IFCLoader } from "web-ifc-three/IFCLoader";
5
+ // Абсолютный URL до wasm-асета из папки public (Vite подставит корректный путь)
6
+ import WEBIFC_WASM_URL from '/wasm/web-ifc.wasm?url';
7
+ // URL собранного воркера через Vite (даёт корректный путь для useWebWorkers)
8
+ // Важно: используем ?url, чтобы получить сырой URL ассета и создать классический Worker,
9
+ // т.к. web-ifc-three создаёт воркер без { type: 'module' }
10
+ import IFCWorkerUrl from 'web-ifc-three/IFCWorker.js?url';
11
+
12
+ export class IfcService {
13
+ /**
14
+ * @param {import('../viewer/Viewer').Viewer} viewer
15
+ */
16
+ constructor(viewer) {
17
+ this.viewer = viewer;
18
+ this.loader = null;
19
+ this.lastModel = null; // THREE.Object3D модели IFC
20
+ this.lastFileName = null;
21
+ this.selectionMaterial = null;
22
+ this.selectionCustomID = 'ifc-selection';
23
+ this.isolateMode = false;
24
+ }
25
+
26
+ init() {
27
+ this.loader = new IFCLoader();
28
+ // Отключаем Web Worker: временно парсим в главном потоке для стабильности
29
+ try { this.loader.ifcManager.useWebWorkers?.(false); } catch(_) {}
30
+ // Путь к wasm файлу (скопируйте web-ifc.wasm в public/wasm)
31
+ try {
32
+ // Преобразуем URL файла wasm в URL каталога и передадим в воркер
33
+ const wasmDir = new URL('.', WEBIFC_WASM_URL).href;
34
+ this.loader.ifcManager.setWasmPath(wasmDir);
35
+ // Дополнительно подстрахуемся передачей полного файла, если версия это поддерживает
36
+ try { this.loader.ifcManager.setWasmPath(WEBIFC_WASM_URL); } catch(_) {}
37
+ } catch (_) {
38
+ this.loader.ifcManager.setWasmPath('/wasm/');
39
+ }
40
+ try {
41
+ this.loader.ifcManager.applyWebIfcConfig?.({
42
+ COORDINATE_TO_ORIGIN: true,
43
+ USE_FAST_BOOLS: true,
44
+ // Порог игнорирования очень мелких полигонов (уменьшаем шум)
45
+ // Некоторые сборки поддерживают SMALL_TRIANGLE_THRESHOLD
46
+ SMALL_TRIANGLE_THRESHOLD: 1e-9,
47
+ });
48
+ } catch(_) {}
49
+ }
50
+
51
+ /**
52
+ * Возвращает пространственную структуру IFC (иерархия) для активной модели
53
+ * Структура: { expressID, type, children: [...] }
54
+ */
55
+ async getSpatialStructure(modelID) {
56
+ if (!this.loader) this.init();
57
+ const mgr = this.loader.ifcManager;
58
+ if (!mgr) return null;
59
+ try {
60
+ // Определим корректный modelID надёжно
61
+ const id = modelID != null ? modelID : (this.lastModel?.modelID);
62
+ if (id == null) return null;
63
+ // Во многих версиях getSpatialStructure синхронен; await совместим с обоими случаями
64
+ const structure = await mgr.getSpatialStructure(id, true);
65
+ return structure;
66
+ } catch (e) {
67
+ console.error("getSpatialStructure error", e);
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Собирает плоский список узлов из пространственной структуры
74
+ * Возвращает массив объектов { expressID, type }
75
+ */
76
+ async flattenSpatialStructure(modelID) {
77
+ const structure = await this.getSpatialStructure(modelID);
78
+ if (!structure) return [];
79
+ const out = [];
80
+ const stack = [structure];
81
+ while (stack.length) {
82
+ const n = stack.pop();
83
+ if (!n) continue;
84
+ if (n.expressID != null) out.push({ expressID: n.expressID, type: n.type || '' });
85
+ const ch = Array.isArray(n.children) ? n.children : [];
86
+ for (let i = ch.length - 1; i >= 0; i--) stack.push(ch[i]);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ /**
92
+ * Формирует тестовый дамп свойств: для первых limit элементов
93
+ * возвращает { total, count, limit, items: [{ id, type, props, psets }] }
94
+ */
95
+ async dumpAllProperties(limit = 200, modelID) {
96
+ if (!this.loader) this.init();
97
+ const mgr = this.loader.ifcManager;
98
+ if (!mgr) return { total: 0, count: 0, limit, items: [] };
99
+ const id = modelID != null ? modelID : (this.lastModel?.modelID);
100
+ if (id == null) return { total: 0, count: 0, limit, items: [] };
101
+
102
+ const flat = await this.flattenSpatialStructure(id);
103
+ const total = flat.length;
104
+ const slice = flat.slice(0, Math.max(0, limit | 0));
105
+ const items = [];
106
+ for (const entry of slice) {
107
+ const eid = entry.expressID;
108
+ let props = null;
109
+ let psets = [];
110
+ try { props = await mgr.getItemProperties(id, eid, true); } catch (_) { props = null; }
111
+ try { psets = await mgr.getPropertySets(id, eid, true); } catch (_) { psets = []; }
112
+ items.push({ id: eid, type: entry.type || '', props, psets });
113
+ }
114
+ return { total, count: items.length, limit, items };
115
+ }
116
+
117
+ /**
118
+ * Загружает файл IFC/IFCZIP из File и добавляет в сцену
119
+ * @param {File} file
120
+ */
121
+ async loadFile(file) {
122
+ if (!this.loader) this.init();
123
+ // Проверка расширения: поддерживаются .ifc и .ifczip
124
+ const name = (file?.name || "").toLowerCase();
125
+ const isIFC = name.endsWith(".ifc");
126
+ const isIFS = name.endsWith(".ifs");
127
+ const isZIP = name.endsWith(".ifczip") || name.endsWith(".zip");
128
+ if (!isIFC && !isIFS && !isZIP) {
129
+ alert("Формат не поддерживается. Используйте .ifc, .ifs или .ifczip");
130
+ return null;
131
+ }
132
+ const url = URL.createObjectURL(file);
133
+ try {
134
+ const model = await this.loader.loadAsync(url);
135
+ // Показать модель вместо демо-куба
136
+ if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
137
+ if (this.viewer.focusObject) this.viewer.focusObject(model);
138
+ this.lastModel = model;
139
+ this.lastFileName = file?.name || null;
140
+ // Сообщим, что модель загружена
141
+ try { document.dispatchEvent(new CustomEvent('ifc:model-loaded', { detail: { modelID: model.modelID } })); } catch(_) {}
142
+ return model;
143
+ } catch (err) {
144
+ console.error("IFC load error:", err);
145
+ alert("Ошибка загрузки IFC: " + (err?.message || err));
146
+ return null;
147
+ } finally {
148
+ URL.revokeObjectURL(url);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Загружает модель IFC по URL (например, из /public/ifc/...)
154
+ * @param {string} url
155
+ */
156
+ async loadUrl(url) {
157
+ if (!this.loader) this.init();
158
+ if (!url) return null;
159
+ try {
160
+ // Защитим загрузку: перехватим возможные исключения на уровне воркера
161
+ const model = await this.loader.loadAsync(url);
162
+ if (!model || !model.geometry) throw new Error('IFC model returned without geometry');
163
+ if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
164
+ if (this.viewer.focusObject) this.viewer.focusObject(model);
165
+ this.lastModel = model;
166
+ try {
167
+ // Показать имя файла из URL
168
+ const u = new URL(url, window.location.origin);
169
+ this.lastFileName = decodeURIComponent(u.pathname.split('/').pop() || url);
170
+ } catch (_) {
171
+ this.lastFileName = url;
172
+ }
173
+ // Сообщим, что модель загружена
174
+ try { document.dispatchEvent(new CustomEvent('ifc:model-loaded', { detail: { modelID: model.modelID } })); } catch(_) {}
175
+ return model;
176
+ } catch (err) {
177
+ console.error("IFC loadUrl error:", err);
178
+ alert("Ошибка загрузки IFC по URL: " + (err?.message || err));
179
+ return null;
180
+ }
181
+ }
182
+
183
+ getLastInfo() {
184
+ const name = this.lastFileName || "";
185
+ const id = this.lastModel?.modelID != null ? String(this.lastModel.modelID) : "";
186
+ return { name, modelID: id };
187
+ }
188
+
189
+ dispose() {
190
+ if (this.loader?.ifcManager) this.loader.ifcManager.dispose();
191
+ this.loader = null;
192
+ }
193
+
194
+ setIsolateMode(enabled) {
195
+ this.isolateMode = !!enabled;
196
+ if (!enabled) {
197
+ // Вернуть модель, если выключили изоляцию
198
+ if (this.lastModel) this.lastModel.visible = true;
199
+ }
200
+ }
201
+
202
+ /** Возвращает массив expressID всех элементов в поддереве */
203
+ collectElementIDsFromStructure(node) {
204
+ const ids = [];
205
+ const stack = [node];
206
+ while (stack.length) {
207
+ const n = stack.pop();
208
+ if (!n) continue;
209
+ const hasChildren = Array.isArray(n.children) && n.children.length > 0;
210
+ if (hasChildren) {
211
+ for (const c of n.children) stack.push(c);
212
+ } else if (n.expressID != null) {
213
+ ids.push(n.expressID);
214
+ }
215
+ }
216
+ return ids;
217
+ }
218
+
219
+ async highlightByIds(ids) {
220
+ if (!this.loader || !this.viewer || !this.lastModel) return;
221
+ const mgr = this.loader.ifcManager;
222
+ if (!mgr) return;
223
+ const modelID = this.lastModel.modelID;
224
+ const scene = this.viewer.scene;
225
+ if (!scene) return;
226
+
227
+ // Очистить предыдущую подсветку
228
+ try { mgr.removeSubset(modelID, this.selectionCustomID); } catch (_) { /* older api? */ }
229
+ try { mgr.removeSubset({ modelID, customID: this.selectionCustomID }); } catch (_) {}
230
+
231
+ if (!ids || !ids.length) {
232
+ if (this.lastModel) this.lastModel.visible = true;
233
+ return;
234
+ }
235
+
236
+ if (!this.selectionMaterial) {
237
+ const THREEmod = await import('three');
238
+ this.selectionMaterial = new THREEmod.MeshBasicMaterial({ color: 0xffd54f, transparent: true, opacity: 0.9, depthTest: true });
239
+ }
240
+ const idsInt = ids.map((x) => (typeof x === 'number' ? x : parseInt(x, 10))).filter((v) => Number.isFinite(v));
241
+ if (!idsInt.length) return;
242
+
243
+ // Создать сабсет
244
+ let subset = null;
245
+ let ok = false;
246
+ try {
247
+ subset = mgr.createSubset(scene, modelID, idsInt, this.selectionMaterial, true, this.selectionCustomID);
248
+ ok = true;
249
+ } catch (_) {
250
+ // новая сигнатура
251
+ try {
252
+ subset = mgr.createSubset({ modelID, ids: idsInt, material: this.selectionMaterial, scene, removePrevious: true, customID: this.selectionCustomID });
253
+ ok = true;
254
+ } catch (_) {}
255
+ }
256
+ if (!ok) return;
257
+
258
+ // Изоляция: скрыть базовую модель, оставить только подсветку
259
+ if (this.isolateMode && this.lastModel) {
260
+ this.lastModel.visible = false;
261
+ if (subset) subset.visible = true;
262
+ } else if (this.lastModel) {
263
+ this.lastModel.visible = true;
264
+ }
265
+ }
266
+ }
267
+
268
+
@@ -0,0 +1,96 @@
1
+ // Рендерит древовидную структуру IFC в контейнер
2
+
3
+ export class IfcTreeView {
4
+ /**
5
+ * @param {HTMLElement} container
6
+ */
7
+ constructor(container) {
8
+ this.container = container;
9
+ this._onSelect = null;
10
+ }
11
+
12
+ /**
13
+ * Рендерит дерево
14
+ * @param {*} structure
15
+ */
16
+ render(structure) {
17
+ if (!this.container) return;
18
+ this.container.innerHTML = "";
19
+ if (!structure) {
20
+ this.container.innerHTML = `<div class="text-sm opacity-70 p-2">Нет данных IFC</div>`;
21
+ return;
22
+ }
23
+ // Рендерим только верхние 2 уровня, чтобы не подвесить браузер
24
+ const rootEl = this._createNode(structure, 0, 2);
25
+ this.container.appendChild(rootEl);
26
+ }
27
+
28
+ _createNode(node, depth = 0, maxDepth = Infinity) {
29
+ const el = document.createElement("div");
30
+ el.className = "collapse collapse-arrow bg-base-100 mb-1 border border-base-300";
31
+ const title = document.createElement("div");
32
+ title.className = "collapse-title text-sm font-medium cursor-pointer";
33
+ title.textContent = `${node.type || 'IFC'} #${node.expressID || ''}`;
34
+ const content = document.createElement("div");
35
+ content.className = "collapse-content";
36
+
37
+ el.appendChild(title);
38
+ el.appendChild(content);
39
+
40
+ // Клик по заголовку — выбор узла
41
+ title.addEventListener("click", (e) => {
42
+ e.stopPropagation();
43
+ if (this._onSelect) this._onSelect(node);
44
+ });
45
+
46
+ if (depth < maxDepth && Array.isArray(node.children) && node.children.length) {
47
+ const list = document.createElement("div");
48
+ list.className = "pl-2";
49
+ node.children.forEach((ch) => list.appendChild(this._createNode(ch, depth + 1, maxDepth)));
50
+ content.appendChild(list);
51
+ } else {
52
+ const stub = document.createElement("div");
53
+ stub.className = "text-xs opacity-70";
54
+ stub.textContent = depth >= maxDepth ? "…" : "Пусто";
55
+ content.appendChild(stub);
56
+ }
57
+ return el;
58
+ }
59
+
60
+ /**
61
+ * Установить обработчик выбора узла
62
+ * @param {(node:any)=>void} handler
63
+ */
64
+ onSelect(handler) {
65
+ this._onSelect = handler;
66
+ }
67
+
68
+ // Временный тестовый рендер: плоский список свойств
69
+ renderFlatProps(dump) {
70
+ if (!this.container) return;
71
+ const { total, count, limit, items } = dump || { total: 0, count: 0, limit: 0, items: [] };
72
+ const wrap = document.createElement('div');
73
+ wrap.className = 'p-2 text-xs space-y-2';
74
+ const header = document.createElement('div');
75
+ header.className = 'opacity-70';
76
+ header.textContent = `Всего элементов: ${total}. Показано: ${count}/${limit}`;
77
+ wrap.appendChild(header);
78
+ items.forEach((it) => {
79
+ const block = document.createElement('div');
80
+ block.className = 'bg-base-100 border border-base-300 rounded p-2';
81
+ const title = document.createElement('div');
82
+ title.className = 'font-medium';
83
+ title.textContent = `${it.type || 'ITEM'} #${it.id}`;
84
+ const pre = document.createElement('pre');
85
+ pre.className = 'whitespace-pre-wrap break-all max-h-60 overflow-auto mt-1';
86
+ pre.textContent = JSON.stringify({ props: it.props, psets: it.psets }, null, 2);
87
+ block.appendChild(title);
88
+ block.appendChild(pre);
89
+ wrap.appendChild(block);
90
+ });
91
+ this.container.innerHTML = '';
92
+ this.container.appendChild(wrap);
93
+ }
94
+ }
95
+
96
+