@sequent-org/ifc-viewer 1.2.4-ci.49.0 → 1.2.4-ci.51.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/README.md +1 -0
- package/package.json +3 -2
- package/scripts/copy-rhino3dm.mjs +90 -0
- package/scripts/patch-three-3dm-loader.mjs +60 -0
- package/src/IfcViewer.js +9 -1
- package/src/index.js +1 -0
- package/src/main.js +13 -4
- package/src/model-loading/loaders/ThreeDmModelLoader.js +147 -0
- package/src/styles-local.css +75 -0
- package/src/ui/CardPlacementController.js +774 -0
- package/src/viewer/Viewer.js +35 -4
package/README.md
CHANGED
|
@@ -367,6 +367,7 @@ npm run test:manual
|
|
|
367
367
|
- `.gltf` - glTF JSON (часто требует внешние .bin/текстуры; надёжнее грузить по URL)
|
|
368
368
|
- `.obj` - Wavefront OBJ (можно загружать один OBJ или OBJ+MTL(+текстуры) через мультивыбор файлов)
|
|
369
369
|
- `.3ds` - 3D Studio (3DS) (рекомендуется загружать .3ds + текстуры через мультивыбор файлов)
|
|
370
|
+
- `.3dm` - Rhino 3D (3DM) (требуются `rhino3dm.js`/`rhino3dm.wasm`, пакет копирует их в `/public/wasm/rhino3dm/` автоматически)
|
|
370
371
|
- `.stl` - STL (ASCII/Binary). Обычно без материалов/цветов — отображается с дефолтным материалом.
|
|
371
372
|
- `.dae` - COLLADA (DAE) (можно загружать .dae + текстуры через мультивыбор файлов)
|
|
372
373
|
|
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.51.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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"postinstall": "node scripts/copy-web-ifc-wasm.mjs",
|
|
35
|
+
"postinstall": "node scripts/copy-web-ifc-wasm.mjs && node scripts/copy-rhino3dm.mjs && node scripts/patch-three-3dm-loader.mjs",
|
|
36
36
|
"dev": "vite",
|
|
37
37
|
"build": "vite build",
|
|
38
38
|
"preview": "vite preview",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"vite": "^7.1.2"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"rhino3dm": "8.4.0",
|
|
45
46
|
"three": "^0.149.0",
|
|
46
47
|
"web-ifc": "^0.0.74"
|
|
47
48
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
const req = createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
/** @type {string|null} */
|
|
22
|
+
let pkgJsonPath = null;
|
|
23
|
+
try {
|
|
24
|
+
pkgJsonPath = req.resolve('rhino3dm/package.json', { paths: [appRoot, process.cwd()] });
|
|
25
|
+
} catch (_) {
|
|
26
|
+
pkgJsonPath = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pkgDir = pkgJsonPath ? path.dirname(pkgJsonPath) : path.join(appRoot, 'node_modules', 'rhino3dm');
|
|
30
|
+
|
|
31
|
+
const version = (() => {
|
|
32
|
+
try {
|
|
33
|
+
if (pkgJsonPath && fs.existsSync(pkgJsonPath)) {
|
|
34
|
+
const raw = fs.readFileSync(pkgJsonPath, 'utf8');
|
|
35
|
+
const json = JSON.parse(raw);
|
|
36
|
+
return String(json?.version || '').trim() || 'unknown';
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {}
|
|
39
|
+
return 'unknown';
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
const srcJs = path.join(pkgDir, 'rhino3dm.js');
|
|
43
|
+
const srcWasm = path.join(pkgDir, 'rhino3dm.wasm');
|
|
44
|
+
|
|
45
|
+
const dstDir = path.join(appRoot, 'public', 'wasm', 'rhino3dm');
|
|
46
|
+
const dstJs = path.join(dstDir, 'rhino3dm.js');
|
|
47
|
+
const dstWasm = path.join(dstDir, 'rhino3dm.wasm');
|
|
48
|
+
|
|
49
|
+
const dstDirV = path.join(dstDir, `v${version}`);
|
|
50
|
+
const dstJsV = path.join(dstDirV, 'rhino3dm.js');
|
|
51
|
+
const dstWasmV = path.join(dstDirV, 'rhino3dm.wasm');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(srcJs) || !fs.existsSync(srcWasm)) {
|
|
54
|
+
console.error('[copy-rhino3dm] Source not found', {
|
|
55
|
+
appRoot,
|
|
56
|
+
pkgDir,
|
|
57
|
+
srcJs,
|
|
58
|
+
srcWasm,
|
|
59
|
+
existsJs: fs.existsSync(srcJs),
|
|
60
|
+
existsWasm: fs.existsSync(srcWasm),
|
|
61
|
+
});
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
copyFile(srcJs, dstJs);
|
|
67
|
+
copyFile(srcWasm, dstWasm);
|
|
68
|
+
|
|
69
|
+
// Versioned location (recommended to avoid cache-mismatch for end-users)
|
|
70
|
+
copyFile(srcJs, dstJsV);
|
|
71
|
+
copyFile(srcWasm, dstWasmV);
|
|
72
|
+
|
|
73
|
+
const sJs = fs.statSync(srcJs);
|
|
74
|
+
const sWasm = fs.statSync(srcWasm);
|
|
75
|
+
const dJs = fs.statSync(dstJs);
|
|
76
|
+
const dWasm = fs.statSync(dstWasm);
|
|
77
|
+
|
|
78
|
+
console.log('[copy-rhino3dm] OK', {
|
|
79
|
+
appRoot,
|
|
80
|
+
versionDir: `v${version}`,
|
|
81
|
+
src: { js: srcJs, wasm: srcWasm },
|
|
82
|
+
dst: { js: dstJs, wasm: dstWasm },
|
|
83
|
+
dstV: { js: dstJsV, wasm: dstWasmV },
|
|
84
|
+
bytes: { js: dJs.size, wasm: dWasm.size },
|
|
85
|
+
sameSize: { js: sJs.size === dJs.size, wasm: sWasm.size === dWasm.size },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main();
|
|
90
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
function main() {
|
|
6
|
+
const appRoot = process.env.INIT_CWD || process.cwd();
|
|
7
|
+
const req = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
/** @type {string|null} */
|
|
10
|
+
let threePkgJson = null;
|
|
11
|
+
try {
|
|
12
|
+
threePkgJson = req.resolve('three/package.json', { paths: [appRoot, process.cwd()] });
|
|
13
|
+
} catch (_) {
|
|
14
|
+
threePkgJson = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const threeDir = threePkgJson ? path.dirname(threePkgJson) : path.join(appRoot, 'node_modules', 'three');
|
|
18
|
+
const target = path.join(threeDir, 'examples', 'jsm', 'loaders', '3DMLoader.js');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(target)) {
|
|
21
|
+
console.warn('[patch-three-3dm-loader] Target not found, skip', { target });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const src = fs.readFileSync(target, 'utf8');
|
|
26
|
+
// Already patched?
|
|
27
|
+
if (src.includes('doc.instanceDefinitions().count;') || src.includes('doc.materials().count;')) {
|
|
28
|
+
console.log('[patch-three-3dm-loader] Already patched');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let out = src;
|
|
33
|
+
const replacements = [
|
|
34
|
+
['doc.instanceDefinitions().count()', 'doc.instanceDefinitions().count'],
|
|
35
|
+
['doc.materials().count()', 'doc.materials().count'],
|
|
36
|
+
['doc.layers().count()', 'doc.layers().count'],
|
|
37
|
+
['doc.views().count()', 'doc.views().count'],
|
|
38
|
+
['doc.namedViews().count()', 'doc.namedViews().count'],
|
|
39
|
+
['doc.groups().count()', 'doc.groups().count'],
|
|
40
|
+
['doc.strings().count()', 'doc.strings().count'],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
let changed = 0;
|
|
44
|
+
for (const [from, to] of replacements) {
|
|
45
|
+
const before = out;
|
|
46
|
+
out = out.split(from).join(to);
|
|
47
|
+
if (out !== before) changed++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (out === src) {
|
|
51
|
+
console.warn('[patch-three-3dm-loader] No changes applied (unexpected)', { target });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(target, out, 'utf8');
|
|
56
|
+
console.log('[patch-three-3dm-loader] Patched', { target, rulesApplied: changed });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main();
|
|
60
|
+
|
package/src/IfcViewer.js
CHANGED
|
@@ -21,6 +21,7 @@ import { ObjModelLoader } from "./model-loading/loaders/ObjModelLoader.js";
|
|
|
21
21
|
import { TdsModelLoader } from "./model-loading/loaders/TdsModelLoader.js";
|
|
22
22
|
import { StlModelLoader } from "./model-loading/loaders/StlModelLoader.js";
|
|
23
23
|
import { DaeModelLoader } from "./model-loading/loaders/DaeModelLoader.js";
|
|
24
|
+
import { ThreeDmModelLoader } from "./model-loading/loaders/ThreeDmModelLoader.js";
|
|
24
25
|
import './style.css';
|
|
25
26
|
|
|
26
27
|
|
|
@@ -34,6 +35,7 @@ export class IfcViewer {
|
|
|
34
35
|
* @param {string} [options.modelUrl] - URL для загрузки модели (любой поддерживаемый формат)
|
|
35
36
|
* @param {File} [options.modelFile] - File объект модели (любой поддерживаемый формат)
|
|
36
37
|
* @param {string} [options.wasmUrl] - URL для загрузки WASM файла web-ifc
|
|
38
|
+
* @param {string} [options.rhino3dmLibraryPath] - Путь (директория) к rhino3dm.js и rhino3dm.wasm (для .3dm)
|
|
37
39
|
* @param {boolean} [options.useTestPreset=true] - Включать ли пресет "Тест" по умолчанию (рекомендованные тени/визуал)
|
|
38
40
|
* @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
|
|
39
41
|
* @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
|
|
@@ -65,6 +67,7 @@ export class IfcViewer {
|
|
|
65
67
|
modelUrl: options.modelUrl || null,
|
|
66
68
|
modelFile: options.modelFile || null,
|
|
67
69
|
wasmUrl: options.wasmUrl || null,
|
|
70
|
+
rhino3dmLibraryPath: options.rhino3dmLibraryPath || '/wasm/rhino3dm/',
|
|
68
71
|
// По умолчанию включаем пресет "Тест" для корректного вида теней (как в демо-настройках)
|
|
69
72
|
useTestPreset: options.useTestPreset !== false,
|
|
70
73
|
showSidebar: options.showSidebar === true, // по умолчанию false
|
|
@@ -207,6 +210,7 @@ export class IfcViewer {
|
|
|
207
210
|
result = await this.modelLoaders.loadUrl(loadSource, {
|
|
208
211
|
viewer: this.viewer,
|
|
209
212
|
wasmUrl: this.options.wasmUrl,
|
|
213
|
+
rhino3dmLibraryPath: this.options.rhino3dmLibraryPath,
|
|
210
214
|
logger: console,
|
|
211
215
|
});
|
|
212
216
|
} else if (Array.isArray(loadSource) || (typeof FileList !== 'undefined' && loadSource instanceof FileList)) {
|
|
@@ -215,17 +219,20 @@ export class IfcViewer {
|
|
|
215
219
|
? await this.modelLoaders.loadFiles(files, {
|
|
216
220
|
viewer: this.viewer,
|
|
217
221
|
wasmUrl: this.options.wasmUrl,
|
|
222
|
+
rhino3dmLibraryPath: this.options.rhino3dmLibraryPath,
|
|
218
223
|
logger: console,
|
|
219
224
|
})
|
|
220
225
|
: await this.modelLoaders.loadFile(files[0], {
|
|
221
226
|
viewer: this.viewer,
|
|
222
227
|
wasmUrl: this.options.wasmUrl,
|
|
228
|
+
rhino3dmLibraryPath: this.options.rhino3dmLibraryPath,
|
|
223
229
|
logger: console,
|
|
224
230
|
});
|
|
225
231
|
} else if (loadSource instanceof File) {
|
|
226
232
|
result = await this.modelLoaders.loadFile(loadSource, {
|
|
227
233
|
viewer: this.viewer,
|
|
228
234
|
wasmUrl: this.options.wasmUrl,
|
|
235
|
+
rhino3dmLibraryPath: this.options.rhino3dmLibraryPath,
|
|
229
236
|
logger: console,
|
|
230
237
|
});
|
|
231
238
|
} else {
|
|
@@ -531,7 +538,8 @@ export class IfcViewer {
|
|
|
531
538
|
.register(new ObjModelLoader())
|
|
532
539
|
.register(new TdsModelLoader())
|
|
533
540
|
.register(new StlModelLoader())
|
|
534
|
-
.register(new DaeModelLoader())
|
|
541
|
+
.register(new DaeModelLoader())
|
|
542
|
+
.register(new ThreeDmModelLoader({ libraryPath: this.options.rhino3dmLibraryPath }));
|
|
535
543
|
|
|
536
544
|
// Если в интерфейсе есть file input — настроим accept
|
|
537
545
|
try {
|
package/src/index.js
CHANGED
|
@@ -22,3 +22,4 @@ export { ObjModelLoader } from "./model-loading/loaders/ObjModelLoader.js";
|
|
|
22
22
|
export { TdsModelLoader } from "./model-loading/loaders/TdsModelLoader.js";
|
|
23
23
|
export { StlModelLoader } from "./model-loading/loaders/StlModelLoader.js";
|
|
24
24
|
export { DaeModelLoader } from "./model-loading/loaders/DaeModelLoader.js";
|
|
25
|
+
export { ThreeDmModelLoader } from "./model-loading/loaders/ThreeDmModelLoader.js";
|
package/src/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Viewer } from "./viewer/Viewer.js";
|
|
|
3
3
|
import { IfcService } from "./ifc/IfcService.js";
|
|
4
4
|
import { IfcTreeView } from "./ifc/IfcTreeView.js";
|
|
5
5
|
import { ModelLoaderRegistry } from "./model-loading/ModelLoaderRegistry.js";
|
|
6
|
+
import { CardPlacementController } from "./ui/CardPlacementController.js";
|
|
6
7
|
import { IfcModelLoader } from "./model-loading/loaders/IfcModelLoader.js";
|
|
7
8
|
import { FbxModelLoader } from "./model-loading/loaders/FbxModelLoader.js";
|
|
8
9
|
import { GltfModelLoader } from "./model-loading/loaders/GltfModelLoader.js";
|
|
@@ -10,6 +11,7 @@ import { ObjModelLoader } from "./model-loading/loaders/ObjModelLoader.js";
|
|
|
10
11
|
import { TdsModelLoader } from "./model-loading/loaders/TdsModelLoader.js";
|
|
11
12
|
import { StlModelLoader } from "./model-loading/loaders/StlModelLoader.js";
|
|
12
13
|
import { DaeModelLoader } from "./model-loading/loaders/DaeModelLoader.js";
|
|
14
|
+
import { ThreeDmModelLoader } from "./model-loading/loaders/ThreeDmModelLoader.js";
|
|
13
15
|
|
|
14
16
|
// Инициализация three.js Viewer в контейнере #app
|
|
15
17
|
const app = document.getElementById("app");
|
|
@@ -17,6 +19,9 @@ if (app) {
|
|
|
17
19
|
const viewer = new Viewer(app);
|
|
18
20
|
viewer.init();
|
|
19
21
|
|
|
22
|
+
// UI: режим постановки "карточек" (меток) по клику на модель
|
|
23
|
+
const cardPlacement = new CardPlacementController({ viewer, container: app, logger: console });
|
|
24
|
+
|
|
20
25
|
// ===== Диагностика (включается через query-параметры) =====
|
|
21
26
|
// ?debugViewer=1 -> window.__viewer = viewer
|
|
22
27
|
// ?debugViewer=1 -> window.__ifcService = ifc
|
|
@@ -354,7 +359,10 @@ if (app) {
|
|
|
354
359
|
.register(new ObjModelLoader())
|
|
355
360
|
.register(new TdsModelLoader())
|
|
356
361
|
.register(new StlModelLoader())
|
|
357
|
-
.register(new DaeModelLoader())
|
|
362
|
+
.register(new DaeModelLoader())
|
|
363
|
+
.register(new ThreeDmModelLoader());
|
|
364
|
+
|
|
365
|
+
const rhino3dmLibraryPath = '/wasm/rhino3dm/';
|
|
358
366
|
|
|
359
367
|
const uploadBtn = document.getElementById("uploadBtn");
|
|
360
368
|
const ifcInput = document.getElementById("ifcInput");
|
|
@@ -369,8 +377,8 @@ if (app) {
|
|
|
369
377
|
try {
|
|
370
378
|
// Multi-file: e.g. OBJ+MTL (+textures)
|
|
371
379
|
result = (files.length > 1)
|
|
372
|
-
? await modelLoaders.loadFiles(files, { viewer, wasmUrl: wasmOverride, logger: console })
|
|
373
|
-
: await modelLoaders.loadFile(files[0], { viewer, wasmUrl: wasmOverride, logger: console });
|
|
380
|
+
? await modelLoaders.loadFiles(files, { viewer, wasmUrl: wasmOverride, rhino3dmLibraryPath, logger: console })
|
|
381
|
+
: await modelLoaders.loadFile(files[0], { viewer, wasmUrl: wasmOverride, rhino3dmLibraryPath, logger: console });
|
|
374
382
|
activeCapabilities = result?.capabilities || null;
|
|
375
383
|
} catch (err) {
|
|
376
384
|
console.error('Model load error', err);
|
|
@@ -611,7 +619,7 @@ if (app) {
|
|
|
611
619
|
const params = new URLSearchParams(location.search);
|
|
612
620
|
const ifcUrlParam = params.get('ifc');
|
|
613
621
|
const ifcUrl = ifcUrlParam || DEFAULT_IFC_URL;
|
|
614
|
-
const result = await modelLoaders.loadUrl(encodeURI(ifcUrl), { viewer, wasmUrl: wasmOverride, logger: console });
|
|
622
|
+
const result = await modelLoaders.loadUrl(encodeURI(ifcUrl), { viewer, wasmUrl: wasmOverride, rhino3dmLibraryPath, logger: console });
|
|
615
623
|
activeCapabilities = result?.capabilities || null;
|
|
616
624
|
if (result?.object3D) {
|
|
617
625
|
if (activeCapabilities?.kind === 'ifc' && activeCapabilities?.ifcService) {
|
|
@@ -672,6 +680,7 @@ if (app) {
|
|
|
672
680
|
import.meta.hot.dispose(() => {
|
|
673
681
|
ifc.dispose();
|
|
674
682
|
viewer.dispose();
|
|
683
|
+
try { cardPlacement.dispose(); } catch (_) {}
|
|
675
684
|
});
|
|
676
685
|
}
|
|
677
686
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Box3, Vector3 } from 'three';
|
|
2
|
+
import { Rhino3dmLoader } from 'three/examples/jsm/loaders/3DMLoader.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rhino 3DM loader.
|
|
6
|
+
*
|
|
7
|
+
* Notes:
|
|
8
|
+
* - Requires `rhino3dm.js` and `rhino3dm.wasm` to be served from a library path.
|
|
9
|
+
* In this package we copy them to `/public/wasm/rhino3dm/` via postinstall script.
|
|
10
|
+
*/
|
|
11
|
+
export class ThreeDmModelLoader {
|
|
12
|
+
/**
|
|
13
|
+
* @param {{ libraryPath?: string, workerLimit?: number, rotateXNeg90?: boolean, alignToGround?: boolean }} [options]
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.id = '3dm';
|
|
17
|
+
this.extensions = ['.3dm'];
|
|
18
|
+
this._libraryPath = options.libraryPath || '/wasm/rhino3dm/';
|
|
19
|
+
this._workerLimit = Number.isFinite(options.workerLimit) ? Number(options.workerLimit) : 4;
|
|
20
|
+
// Many 3DM assets are effectively Z-up; viewer is Y-up. Keep consistent with other format loaders.
|
|
21
|
+
this._rotateXNeg90 = options.rotateXNeg90 !== false; // default true
|
|
22
|
+
// Bring model down to ground plane so shadow receiver is correct.
|
|
23
|
+
this._alignToGround = options.alignToGround !== false; // default true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {File} file
|
|
28
|
+
* @param {any} ctx
|
|
29
|
+
*/
|
|
30
|
+
async loadFile(file, ctx) {
|
|
31
|
+
const url = URL.createObjectURL(file);
|
|
32
|
+
try {
|
|
33
|
+
const obj = await this._loadInternal(url, ctx);
|
|
34
|
+
return {
|
|
35
|
+
object3D: obj,
|
|
36
|
+
format: this.id,
|
|
37
|
+
name: file?.name || '',
|
|
38
|
+
replacedInViewer: false,
|
|
39
|
+
capabilities: { kind: 'generic' },
|
|
40
|
+
};
|
|
41
|
+
} finally {
|
|
42
|
+
try { URL.revokeObjectURL(url); } catch (_) {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} url
|
|
48
|
+
* @param {any} ctx
|
|
49
|
+
*/
|
|
50
|
+
async loadUrl(url, ctx) {
|
|
51
|
+
const obj = await this._loadInternal(url, ctx);
|
|
52
|
+
return {
|
|
53
|
+
object3D: obj,
|
|
54
|
+
format: this.id,
|
|
55
|
+
name: String(url || ''),
|
|
56
|
+
replacedInViewer: false,
|
|
57
|
+
capabilities: { kind: 'generic' },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _loadInternal(url, ctx) {
|
|
62
|
+
const logger = ctx?.logger || console;
|
|
63
|
+
const libraryPath = (ctx?.rhino3dmLibraryPath || this._libraryPath || '').toString();
|
|
64
|
+
const normalizedPath = libraryPath.endsWith('/') ? libraryPath : `${libraryPath}/`;
|
|
65
|
+
|
|
66
|
+
const loader = new Rhino3dmLoader();
|
|
67
|
+
loader.setLibraryPath(normalizedPath);
|
|
68
|
+
loader.setWorkerLimit(this._workerLimit);
|
|
69
|
+
|
|
70
|
+
logger?.log?.('[ThreeDmModelLoader] load', {
|
|
71
|
+
url: String(url || ''),
|
|
72
|
+
libraryPath: normalizedPath,
|
|
73
|
+
workerLimit: this._workerLimit,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let obj = null;
|
|
77
|
+
try {
|
|
78
|
+
obj = await new Promise((resolve, reject) => {
|
|
79
|
+
try {
|
|
80
|
+
loader.load(
|
|
81
|
+
url,
|
|
82
|
+
(result) => resolve(result),
|
|
83
|
+
undefined,
|
|
84
|
+
(err) => reject(err)
|
|
85
|
+
);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
reject(e);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const msg = String(e?.message || e?.error?.message || e || '');
|
|
92
|
+
if (msg.includes('.count is not a function')) {
|
|
93
|
+
logger?.error?.(
|
|
94
|
+
'[ThreeDmModelLoader] rhino3dm API mismatch detected. This often happens when rhino3dm version is not compatible with three/examples 3DMLoader. ' +
|
|
95
|
+
'For three@0.149.0 a compatible rhino3dm version is ~8.4.0.'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Axis + grounding BEFORE Viewer.replaceWithModel(): ensures bbox/shadowReceiver computed correctly.
|
|
102
|
+
try {
|
|
103
|
+
if (obj) {
|
|
104
|
+
if (this._rotateXNeg90) {
|
|
105
|
+
obj.rotation.x = -Math.PI / 2;
|
|
106
|
+
obj.updateMatrixWorld?.(true);
|
|
107
|
+
}
|
|
108
|
+
if (this._alignToGround) {
|
|
109
|
+
const box = new Box3().setFromObject(obj);
|
|
110
|
+
const minY = box.min.y;
|
|
111
|
+
if (Number.isFinite(minY)) {
|
|
112
|
+
obj.position.y -= minY;
|
|
113
|
+
obj.position.y += 0.001; // epsilon to avoid z-fighting with shadow receiver
|
|
114
|
+
obj.updateMatrixWorld?.(true);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (_) {}
|
|
119
|
+
|
|
120
|
+
// Basic diagnostics
|
|
121
|
+
try {
|
|
122
|
+
let meshes = 0;
|
|
123
|
+
let mats = 0;
|
|
124
|
+
obj?.traverse?.((n) => {
|
|
125
|
+
if (!n?.isMesh) return;
|
|
126
|
+
meshes++;
|
|
127
|
+
const m = n.material;
|
|
128
|
+
const arr = Array.isArray(m) ? m : [m];
|
|
129
|
+
for (const mi of arr) if (mi) mats++;
|
|
130
|
+
});
|
|
131
|
+
let bbox = null;
|
|
132
|
+
try {
|
|
133
|
+
const b = new Box3().setFromObject(obj);
|
|
134
|
+
const size = b.getSize(new Vector3());
|
|
135
|
+
const center = b.getCenter(new Vector3());
|
|
136
|
+
bbox = {
|
|
137
|
+
size: { x: +size.x.toFixed(3), y: +size.y.toFixed(3), z: +size.z.toFixed(3) },
|
|
138
|
+
center: { x: +center.x.toFixed(3), y: +center.y.toFixed(3), z: +center.z.toFixed(3) },
|
|
139
|
+
};
|
|
140
|
+
} catch (_) {}
|
|
141
|
+
logger?.log?.('[ThreeDmModelLoader] parsed', { meshes, materials: mats, bbox });
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
|
|
144
|
+
return obj;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
package/src/styles-local.css
CHANGED
|
@@ -110,6 +110,81 @@
|
|
|
110
110
|
.duration-300 { transition-duration: 300ms; }
|
|
111
111
|
.pointer-events-none { pointer-events: none; }
|
|
112
112
|
|
|
113
|
+
/* === IFC CARD MARKERS (UI overlay) === */
|
|
114
|
+
.ifc-card-add-btn {
|
|
115
|
+
position: absolute;
|
|
116
|
+
left: 12px;
|
|
117
|
+
top: 12px;
|
|
118
|
+
z-index: 9999;
|
|
119
|
+
pointer-events: auto;
|
|
120
|
+
|
|
121
|
+
padding: 8px 12px;
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
line-height: 1;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
|
|
126
|
+
border-radius: 10px;
|
|
127
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
128
|
+
background: rgba(255, 255, 255, 0.92);
|
|
129
|
+
color: #1f2937;
|
|
130
|
+
backdrop-filter: blur(6px);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ifc-card-add-btn:hover {
|
|
134
|
+
background: rgba(255, 255, 255, 0.98);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ifc-card-ghost,
|
|
138
|
+
.ifc-card-marker {
|
|
139
|
+
position: absolute;
|
|
140
|
+
z-index: 9999;
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
display: block;
|
|
143
|
+
transform: translate(-50%, -50%);
|
|
144
|
+
will-change: transform;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ifc-card-ghost {
|
|
148
|
+
display: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.ifc-card-dot {
|
|
152
|
+
width: 18px;
|
|
153
|
+
height: 18px;
|
|
154
|
+
border-radius: 9999px;
|
|
155
|
+
background: rgba(37, 99, 235, 0.95); /* blue */
|
|
156
|
+
border: 2px solid rgba(255, 255, 255, 0.95);
|
|
157
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ifc-card-num {
|
|
161
|
+
position: absolute;
|
|
162
|
+
left: 0;
|
|
163
|
+
top: 0;
|
|
164
|
+
width: 18px;
|
|
165
|
+
height: 18px;
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
font-size: 11px;
|
|
170
|
+
font-weight: 700;
|
|
171
|
+
color: #ffffff;
|
|
172
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.ifc-card-marker {
|
|
176
|
+
width: 18px;
|
|
177
|
+
height: 18px;
|
|
178
|
+
pointer-events: auto;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Клик ловим на контейнере метки, дочерние элементы пусть не перехватывают */
|
|
183
|
+
.ifc-card-marker .ifc-card-dot,
|
|
184
|
+
.ifc-card-marker .ifc-card-num {
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
113
188
|
/* === NAVBAR COMPONENT === */
|
|
114
189
|
.navbar {
|
|
115
190
|
display: flex;
|