@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 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.49.0",
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
+
@@ -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;