@ifc-lite/viewer 1.14.3 → 1.15.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/CHANGELOG.md +32 -0
- package/dist/assets/{Arrow.dom-BgkZDIQm.js → Arrow.dom-OVBBPqOB.js} +1 -1
- package/dist/assets/{basketViewActivator-h_M3YbMW.js → basketViewActivator-Bx6QU4ma.js} +1 -1
- package/dist/assets/{browser-CRQ0bPh1.js → browser-BMqEoJw4.js} +1 -1
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-CJr7Itua.css +1 -0
- package/dist/assets/index-DZY6uD8A.js +185948 -0
- package/dist/assets/{index-C4VVJRL-.js → index-DsX-NCtx.js} +4 -4
- package/dist/assets/{native-bridge-DtcJqlOi.js → native-bridge-D6tKFqGO.js} +1 -1
- package/dist/assets/{wasm-bridge-BJJVu9P2.js → wasm-bridge-D4kvZVDw.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -7
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/ExportDialog.tsx +40 -2
- package/src/components/viewer/HierarchyPanel.tsx +127 -35
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/ViewportContainer.tsx +30 -25
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/src/sdk/adapters/visibility-adapter.ts +82 -2
- package/src/store/basketVisibleSet.ts +72 -4
- package/src/store/index.ts +11 -1
- package/src/store/slices/visibilitySlice.ts +28 -2
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-Be6XjVeM.js +0 -116717
- package/dist/assets/index-DdwD4c-E.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browser-
|
|
2
|
-
import { _ as u, b as _, d as M, __tla as __tla_0 } from "./index-
|
|
3
|
-
import { N as Z, m as $, __tla as __tla_1 } from "./index-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browser-BMqEoJw4.js","assets/index-DZY6uD8A.js","assets/index-CJr7Itua.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import { _ as u, b as _, d as M, __tla as __tla_0 } from "./index-DZY6uD8A.js";
|
|
3
|
+
import { N as Z, m as $, __tla as __tla_1 } from "./index-DZY6uD8A.js";
|
|
4
4
|
let c, y, O, V, I, C, L;
|
|
5
5
|
let __tla = Promise.all([
|
|
6
6
|
(()=>{
|
|
@@ -91,7 +91,7 @@ let __tla = Promise.all([
|
|
|
91
91
|
function D() {
|
|
92
92
|
return m || (m = (async ()=>{
|
|
93
93
|
try {
|
|
94
|
-
const s = await u(()=>import("./browser-
|
|
94
|
+
const s = await u(()=>import("./browser-BMqEoJw4.js").then((i)=>i.b), __vite__mapDeps([0,1,2])), t = s.default ?? s;
|
|
95
95
|
let e;
|
|
96
96
|
try {
|
|
97
97
|
e = (await u(()=>import("./esbuild-COv63sf-.js"), [])).default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,a as m}from"./index-
|
|
1
|
+
import{I as f,a as m}from"./index-DZY6uD8A.js";class u{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}async processGeometry(s){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),s),n=i.collectMeshes(),r=i.getBuildingRotation();performance.now();let e=0,o=0;for(const c of n)e+=c.positions.length/3,o+=c.indices.length/3;return{meshes:n,totalVertices:e,totalTriangles:o,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:r}}}async processGeometryStreaming(s,i){this.initialized||await this.init();const n=performance.now(),r=new m(this.bridge.getApi(),s);let e=0,o=0,a=0;try{for await(const t of r.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;e+=l.length;for(const d of l)o+=d.positions.length/3,a+=d.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:e,total:e,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const h=performance.now()-n,g={totalMeshes:e,totalVertices:o,totalTriangles:a,parseTimeMs:h*.3,geometryTimeMs:h*.7};return i.onComplete?.(g),g}getApi(){return this.bridge.getApi()}}export{u as WasmBridge};
|
package/dist/index.html
CHANGED
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
<meta name="theme-color" content="#7aa2f7">
|
|
45
45
|
<meta name="msapplication-TileColor" content="#1a1b26">
|
|
46
46
|
<meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
|
|
47
|
-
<script type="module" crossorigin src="/assets/index-
|
|
48
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-DZY6uD8A.js"></script>
|
|
48
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CJr7Itua.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
|
51
51
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -46,15 +46,15 @@
|
|
|
46
46
|
"@ifc-lite/cache": "^1.14.3",
|
|
47
47
|
"@ifc-lite/data": "^1.14.3",
|
|
48
48
|
"@ifc-lite/drawing-2d": "^1.14.3",
|
|
49
|
-
"@ifc-lite/encoding": "^1.14.
|
|
50
|
-
"@ifc-lite/export": "^1.
|
|
49
|
+
"@ifc-lite/encoding": "^1.14.4",
|
|
50
|
+
"@ifc-lite/export": "^1.15.0",
|
|
51
51
|
"@ifc-lite/geometry": "^1.14.3",
|
|
52
|
-
"@ifc-lite/ids": "^1.14.
|
|
52
|
+
"@ifc-lite/ids": "^1.14.4",
|
|
53
53
|
"@ifc-lite/lens": "^1.14.3",
|
|
54
|
-
"@ifc-lite/lists": "^1.14.
|
|
54
|
+
"@ifc-lite/lists": "^1.14.4",
|
|
55
55
|
"@ifc-lite/mutations": "^1.14.3",
|
|
56
|
-
"@ifc-lite/parser": "^1.
|
|
57
|
-
"@ifc-lite/query": "^1.14.
|
|
56
|
+
"@ifc-lite/parser": "^2.1.0",
|
|
57
|
+
"@ifc-lite/query": "^1.14.4",
|
|
58
58
|
"@ifc-lite/renderer": "^1.14.3",
|
|
59
59
|
"@ifc-lite/sandbox": "^1.14.3",
|
|
60
60
|
"@ifc-lite/server-client": "^1.14.3",
|
|
@@ -218,6 +218,7 @@ function activateBottomPanel(panel: 'script' | 'list') {
|
|
|
218
218
|
s.setListPanelVisible(false);
|
|
219
219
|
|
|
220
220
|
if (!isActive) {
|
|
221
|
+
s.setRightPanelCollapsed(false);
|
|
221
222
|
if (panel === 'script') s.setScriptPanelVisible(true);
|
|
222
223
|
else s.setListPanelVisible(true);
|
|
223
224
|
}
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
import { useViewerStore } from '@/store';
|
|
52
52
|
import { configureMutationView } from '@/utils/configureMutationView';
|
|
53
53
|
import { toast } from '@/components/ui/toast';
|
|
54
|
-
import { StepExporter, MergedExporter, Ifc5Exporter, type MergeModelInput } from '@ifc-lite/export';
|
|
54
|
+
import { StepExporter, MergedExporter, Ifc5Exporter, IFC5_KNOWN_PROP_NAMES, type MergeModelInput } from '@ifc-lite/export';
|
|
55
55
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
56
56
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
57
57
|
|
|
@@ -84,6 +84,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
84
84
|
const [applyMutations, setApplyMutations] = useState(true);
|
|
85
85
|
const [changesOnly, setChangesOnly] = useState(false);
|
|
86
86
|
const [visibleOnly, setVisibleOnly] = useState(false);
|
|
87
|
+
const [onlyKnownProperties, setOnlyKnownProperties] = useState(true);
|
|
87
88
|
const [isExporting, setIsExporting] = useState(false);
|
|
88
89
|
const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
89
90
|
|
|
@@ -244,6 +245,29 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
244
245
|
return localIds.size > 0 ? localIds : null;
|
|
245
246
|
}, [models, isolatedEntities, isolatedEntitiesByModel]);
|
|
246
247
|
|
|
248
|
+
// Detect if the model has properties that would be filtered by onlyKnownProperties.
|
|
249
|
+
// Only relevant for IFC5 exports — show the toggle only when there's something to filter.
|
|
250
|
+
const hasFilterableProperties = useMemo(() => {
|
|
251
|
+
if (!isIfc5 || !selectedModel?.ifcDataStore) return false;
|
|
252
|
+
const mutationView = getMutationView(selectedModelId);
|
|
253
|
+
const propSource = mutationView || selectedModel.ifcDataStore.properties;
|
|
254
|
+
if (!propSource) return false;
|
|
255
|
+
|
|
256
|
+
// Sample a few entities to check for unknown property names
|
|
257
|
+
const entities = selectedModel.ifcDataStore.entities;
|
|
258
|
+
const limit = Math.min(entities.count, 50);
|
|
259
|
+
for (let i = 0; i < limit; i++) {
|
|
260
|
+
const id = entities.expressId[i];
|
|
261
|
+
const psets = propSource.getForEntity(id);
|
|
262
|
+
for (const pset of psets) {
|
|
263
|
+
for (const prop of pset.properties) {
|
|
264
|
+
if (!IFC5_KNOWN_PROP_NAMES.has(prop.name)) return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}, [isIfc5, selectedModel, selectedModelId, getMutationView]);
|
|
270
|
+
|
|
247
271
|
// Compute output format description for UI
|
|
248
272
|
const outputInfo = useMemo(() => {
|
|
249
273
|
if (changesOnly) {
|
|
@@ -356,6 +380,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
356
380
|
visibleOnly: effectiveVisibleOnly,
|
|
357
381
|
hiddenEntityIds: localHidden,
|
|
358
382
|
isolatedEntityIds: localIsolated,
|
|
383
|
+
onlyKnownProperties,
|
|
359
384
|
author: 'ifc-lite',
|
|
360
385
|
});
|
|
361
386
|
|
|
@@ -440,7 +465,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
440
465
|
} finally {
|
|
441
466
|
setIsExporting(false);
|
|
442
467
|
}
|
|
443
|
-
}, [selectedModel, selectedModelId, schema, isIfc5, exportScope, includeGeometry, applyMutations, changesOnly, visibleOnly, getMutationView, getLocalHiddenIds, getLocalIsolatedIds, modifiedCount, models]);
|
|
468
|
+
}, [selectedModel, selectedModelId, schema, isIfc5, exportScope, includeGeometry, applyMutations, changesOnly, visibleOnly, onlyKnownProperties, getMutationView, getLocalHiddenIds, getLocalIsolatedIds, modifiedCount, models]);
|
|
444
469
|
|
|
445
470
|
return (
|
|
446
471
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
@@ -583,6 +608,19 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
583
608
|
</div>
|
|
584
609
|
)}
|
|
585
610
|
|
|
611
|
+
{/* IFC5: strict property schema filtering */}
|
|
612
|
+
{isIfc5 && hasFilterableProperties && (
|
|
613
|
+
<div className="flex items-center justify-between">
|
|
614
|
+
<div>
|
|
615
|
+
<Label>Only Known IFC5 Properties</Label>
|
|
616
|
+
<p className="text-xs text-muted-foreground">
|
|
617
|
+
Skip properties without an official IFC5 schema (avoids viewer warnings)
|
|
618
|
+
</p>
|
|
619
|
+
</div>
|
|
620
|
+
<Switch checked={onlyKnownProperties} onCheckedChange={setOnlyKnownProperties} />
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
|
|
586
624
|
{/* Stats */}
|
|
587
625
|
{modifiedCount > 0 && (
|
|
588
626
|
<Alert>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
5
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
6
6
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
7
7
|
import {
|
|
8
8
|
Search,
|
|
@@ -46,6 +46,12 @@ export function HierarchyPanel() {
|
|
|
46
46
|
const setStoreysSelection = useViewerStore((s) => s.setStoreysSelection);
|
|
47
47
|
const clearStoreySelection = useViewerStore((s) => s.clearStoreySelection);
|
|
48
48
|
const isolateEntities = useViewerStore((s) => s.isolateEntities);
|
|
49
|
+
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
50
|
+
const clearIsolation = useViewerStore((s) => s.clearIsolation);
|
|
51
|
+
const classFilter = useViewerStore((s) => s.classFilter);
|
|
52
|
+
const setClassFilter = useViewerStore((s) => s.setClassFilter);
|
|
53
|
+
const clearClassFilter = useViewerStore((s) => s.clearClassFilter);
|
|
54
|
+
const clearAllFilters = useViewerStore((s) => s.clearAllFilters);
|
|
49
55
|
const setHierarchyBasketSelection = useViewerStore((s) => s.setHierarchyBasketSelection);
|
|
50
56
|
|
|
51
57
|
const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
|
|
@@ -54,6 +60,26 @@ export function HierarchyPanel() {
|
|
|
54
60
|
const toggleEntityVisibility = useViewerStore((s) => s.toggleEntityVisibility);
|
|
55
61
|
const clearSelection = useViewerStore((s) => s.clearSelection);
|
|
56
62
|
|
|
63
|
+
// Derive label for type isolation (from Type tab) by checking mesh ifcType
|
|
64
|
+
const typeIsolationLabel = useMemo(() => {
|
|
65
|
+
if (!isolatedEntities || isolatedEntities.size === 0) return null;
|
|
66
|
+
const sampleId = isolatedEntities.values().next().value!;
|
|
67
|
+
for (const [, model] of models) {
|
|
68
|
+
const gr = model.geometryResult;
|
|
69
|
+
if (!gr?.meshes) continue;
|
|
70
|
+
const offset = model.idOffset ?? 0;
|
|
71
|
+
const mesh = gr.meshes.find((m: { expressId: number }) => m.expressId + offset === sampleId);
|
|
72
|
+
if (mesh?.ifcType) return mesh.ifcType;
|
|
73
|
+
}
|
|
74
|
+
if (geometryResult?.meshes) {
|
|
75
|
+
const mesh = geometryResult.meshes.find((m: { expressId: number }) => m.expressId === sampleId);
|
|
76
|
+
if (mesh?.ifcType) return mesh.ifcType;
|
|
77
|
+
}
|
|
78
|
+
return `${isolatedEntities.size} elements`;
|
|
79
|
+
}, [isolatedEntities, models, geometryResult]);
|
|
80
|
+
|
|
81
|
+
const hasActiveFilters = selectedStoreys.size > 0 || isolatedEntities !== null || classFilter !== null;
|
|
82
|
+
|
|
57
83
|
// Resizable panel split (percentage for storeys section, 0.5 = 50%)
|
|
58
84
|
const [splitRatio, setSplitRatio] = useState(0.5);
|
|
59
85
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -198,15 +224,20 @@ export function HierarchyPanel() {
|
|
|
198
224
|
}]);
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
// Type group nodes - click to isolate entities, expand via chevron only
|
|
227
|
+
// Type group nodes - click to filter/isolate entities, expand via chevron only
|
|
202
228
|
if (node.type === 'type-group') {
|
|
203
229
|
const elements = getNodeElements(node);
|
|
204
230
|
if (elements.length > 0) {
|
|
205
|
-
// Clear multi-selection highlight
|
|
206
|
-
// but we don't want every element highlighted/selected
|
|
231
|
+
// Clear multi-selection highlight
|
|
207
232
|
setSelectedEntityIds([]);
|
|
208
233
|
setSelectedEntity(resolveEntityRef(elements[0]));
|
|
209
|
-
|
|
234
|
+
if (groupingMode === 'type') {
|
|
235
|
+
// Class tab → class filter (combinable with storey + type isolation)
|
|
236
|
+
setClassFilter(elements, node.ifcType || node.name);
|
|
237
|
+
} else {
|
|
238
|
+
// Type tab → type isolation (combinable with storey + class filter)
|
|
239
|
+
isolateEntities(elements);
|
|
240
|
+
}
|
|
210
241
|
}
|
|
211
242
|
return;
|
|
212
243
|
}
|
|
@@ -319,30 +350,45 @@ export function HierarchyPanel() {
|
|
|
319
350
|
setStoreysSelection(storeyIds);
|
|
320
351
|
}
|
|
321
352
|
}
|
|
353
|
+
} else if (node.type === 'IfcSpace') {
|
|
354
|
+
const spaceId = node.expressIds[0];
|
|
355
|
+
const modelId = node.modelIds[0];
|
|
356
|
+
const globalId = node.globalIds[0] ?? spaceId;
|
|
357
|
+
|
|
358
|
+
setSelectedEntityIds([]);
|
|
359
|
+
|
|
360
|
+
if (modelId && modelId !== 'legacy') {
|
|
361
|
+
setSelectedEntityId(globalId);
|
|
362
|
+
setSelectedEntity({ modelId, expressId: spaceId });
|
|
363
|
+
setActiveModel(modelId);
|
|
364
|
+
} else {
|
|
365
|
+
setSelectedEntityId(globalId);
|
|
366
|
+
setSelectedEntity({ modelId: 'legacy', expressId: spaceId });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (node.hasChildren) {
|
|
370
|
+
toggleExpand(node.id);
|
|
371
|
+
}
|
|
322
372
|
} else if (node.type === 'element') {
|
|
323
373
|
// Element click - select it
|
|
324
|
-
const elementId = node.expressIds[0];
|
|
374
|
+
const elementId = node.expressIds[0];
|
|
325
375
|
const modelId = node.modelIds[0];
|
|
376
|
+
const globalId = node.globalIds[0] ?? elementId;
|
|
326
377
|
|
|
327
378
|
// Clear multi-selection (e.g. from a prior type-group click) so only
|
|
328
379
|
// this single element is highlighted, matching Viewport pick behavior
|
|
329
380
|
setSelectedEntityIds([]);
|
|
330
381
|
|
|
331
382
|
if (modelId !== 'legacy') {
|
|
332
|
-
// Multi-model: need to convert to globalId for renderer
|
|
333
|
-
const model = models.get(modelId);
|
|
334
|
-
const globalId = elementId + (model?.idOffset ?? 0);
|
|
335
383
|
setSelectedEntityId(globalId);
|
|
336
384
|
setSelectedEntity({ modelId, expressId: elementId });
|
|
337
385
|
setActiveModel(modelId);
|
|
338
386
|
} else {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// Also set selectedEntity for property panel (was missing, causing blank panel)
|
|
342
|
-
setSelectedEntity(resolveEntityRef(elementId));
|
|
387
|
+
setSelectedEntityId(globalId);
|
|
388
|
+
setSelectedEntity(resolveEntityRef(globalId));
|
|
343
389
|
}
|
|
344
390
|
}
|
|
345
|
-
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntityIds, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements, setHierarchyBasketSelection, toGlobalId]);
|
|
391
|
+
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntityIds, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements, setHierarchyBasketSelection, toGlobalId, groupingMode, setClassFilter]);
|
|
346
392
|
|
|
347
393
|
// Compute selection and visibility state for a node
|
|
348
394
|
const computeNodeState = useCallback((node: TreeNode): { isSelected: boolean; nodeHidden: boolean; modelVisible?: boolean } => {
|
|
@@ -352,8 +398,8 @@ export function HierarchyPanel() {
|
|
|
352
398
|
? node.expressIds.some(id => selectedStoreys.has(id))
|
|
353
399
|
: node.type === 'IfcBuildingStorey'
|
|
354
400
|
? selectedStoreys.has(node.expressIds[0])
|
|
355
|
-
: node.type === 'element'
|
|
356
|
-
? selectedEntityId === node.expressIds[0]
|
|
401
|
+
: node.type === 'IfcSpace' || node.type === 'element'
|
|
402
|
+
? selectedEntityId === (node.globalIds[0] ?? node.expressIds[0])
|
|
357
403
|
: node.type === 'ifc-type'
|
|
358
404
|
? (() => {
|
|
359
405
|
const typeExpressId = node.entityExpressId;
|
|
@@ -369,8 +415,8 @@ export function HierarchyPanel() {
|
|
|
369
415
|
// Compute visibility inline - for elements check directly, for storeys use getNodeElements
|
|
370
416
|
let nodeHidden = false;
|
|
371
417
|
if (node.type === 'element') {
|
|
372
|
-
nodeHidden = hiddenEntities.has(node.expressIds[0]);
|
|
373
|
-
} else if (node.type === 'IfcBuildingStorey' || node.type === 'unified-storey' ||
|
|
418
|
+
nodeHidden = hiddenEntities.has(node.globalIds[0] ?? node.expressIds[0]);
|
|
419
|
+
} else if (node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' ||
|
|
374
420
|
node.type === 'type-group' || node.type === 'ifc-type' ||
|
|
375
421
|
(node.type === 'model-header' && node.id.startsWith('contrib-'))) {
|
|
376
422
|
const elements = getNodeElements(node);
|
|
@@ -385,7 +431,7 @@ export function HierarchyPanel() {
|
|
|
385
431
|
}
|
|
386
432
|
|
|
387
433
|
return { isSelected, nodeHidden, modelVisible };
|
|
388
|
-
}, [selectedStoreys, selectedEntityId, hiddenEntities, getNodeElements, models]);
|
|
434
|
+
}, [selectedStoreys, selectedEntityId, hiddenEntities, getNodeElements, models, toGlobalId]);
|
|
389
435
|
|
|
390
436
|
if (!ifcDataStore && models.size === 0) {
|
|
391
437
|
return (
|
|
@@ -536,21 +582,44 @@ export function HierarchyPanel() {
|
|
|
536
582
|
</div>
|
|
537
583
|
|
|
538
584
|
{/* Footer status */}
|
|
539
|
-
{
|
|
585
|
+
{hasActiveFilters ? (
|
|
540
586
|
<div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 bg-primary text-white dark:bg-primary">
|
|
541
|
-
<div className="flex items-center justify-between text-xs font-medium">
|
|
542
|
-
<
|
|
543
|
-
{selectedStoreys.size
|
|
544
|
-
|
|
545
|
-
|
|
587
|
+
<div className="flex items-center justify-between text-xs font-medium gap-2">
|
|
588
|
+
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
|
589
|
+
{selectedStoreys.size > 0 && (
|
|
590
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
591
|
+
{selectedStoreys.size} {selectedStoreys.size === 1 ? 'Storey' : 'Storeys'}
|
|
592
|
+
<button onClick={clearStoreySelection} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear storey filter">×</button>
|
|
593
|
+
</span>
|
|
594
|
+
)}
|
|
595
|
+
{classFilter !== null && (
|
|
596
|
+
<>
|
|
597
|
+
{selectedStoreys.size > 0 && <span className="text-[10px] opacity-50">+</span>}
|
|
598
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
599
|
+
{classFilter.label}
|
|
600
|
+
<button onClick={clearClassFilter} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear class filter">×</button>
|
|
601
|
+
</span>
|
|
602
|
+
</>
|
|
603
|
+
)}
|
|
604
|
+
{isolatedEntities !== null && (
|
|
605
|
+
<>
|
|
606
|
+
{(selectedStoreys.size > 0 || classFilter !== null) && <span className="text-[10px] opacity-50">+</span>}
|
|
607
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
608
|
+
{typeIsolationLabel}
|
|
609
|
+
<button onClick={clearIsolation} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear type filter">×</button>
|
|
610
|
+
</span>
|
|
611
|
+
</>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
546
615
|
<span className="opacity-70 text-[10px] font-mono">ESC</span>
|
|
547
616
|
<Button
|
|
548
617
|
variant="ghost"
|
|
549
618
|
size="sm"
|
|
550
619
|
className="h-6 text-[10px] uppercase border border-white/20 hover:bg-white/20 hover:text-white rounded-none px-2"
|
|
551
|
-
onClick={clearStoreySelection}
|
|
620
|
+
onClick={() => { clearStoreySelection(); clearAllFilters(); }}
|
|
552
621
|
>
|
|
553
|
-
Clear
|
|
622
|
+
Clear all
|
|
554
623
|
</Button>
|
|
555
624
|
</div>
|
|
556
625
|
</div>
|
|
@@ -599,21 +668,44 @@ export function HierarchyPanel() {
|
|
|
599
668
|
</div>
|
|
600
669
|
|
|
601
670
|
{/* Footer status */}
|
|
602
|
-
{
|
|
671
|
+
{hasActiveFilters ? (
|
|
603
672
|
<div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 bg-primary text-white dark:bg-primary">
|
|
604
|
-
<div className="flex items-center justify-between text-xs font-medium">
|
|
605
|
-
<
|
|
606
|
-
{selectedStoreys.size
|
|
607
|
-
|
|
608
|
-
|
|
673
|
+
<div className="flex items-center justify-between text-xs font-medium gap-2">
|
|
674
|
+
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
|
675
|
+
{selectedStoreys.size > 0 && (
|
|
676
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
677
|
+
{selectedStoreys.size} {selectedStoreys.size === 1 ? 'Storey' : 'Storeys'}
|
|
678
|
+
<button onClick={clearStoreySelection} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear storey filter">×</button>
|
|
679
|
+
</span>
|
|
680
|
+
)}
|
|
681
|
+
{classFilter !== null && (
|
|
682
|
+
<>
|
|
683
|
+
{selectedStoreys.size > 0 && <span className="text-[10px] opacity-50">+</span>}
|
|
684
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
685
|
+
{classFilter.label}
|
|
686
|
+
<button onClick={clearClassFilter} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear class filter">×</button>
|
|
687
|
+
</span>
|
|
688
|
+
</>
|
|
689
|
+
)}
|
|
690
|
+
{isolatedEntities !== null && (
|
|
691
|
+
<>
|
|
692
|
+
{(selectedStoreys.size > 0 || classFilter !== null) && <span className="text-[10px] opacity-50">+</span>}
|
|
693
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
694
|
+
{typeIsolationLabel}
|
|
695
|
+
<button onClick={clearIsolation} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear type filter">×</button>
|
|
696
|
+
</span>
|
|
697
|
+
</>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
609
701
|
<span className="opacity-70 text-[10px] font-mono">ESC</span>
|
|
610
702
|
<Button
|
|
611
703
|
variant="ghost"
|
|
612
704
|
size="sm"
|
|
613
705
|
className="h-6 text-[10px] uppercase border border-white/20 hover:bg-white/20 hover:text-white rounded-none px-2"
|
|
614
|
-
onClick={clearStoreySelection}
|
|
706
|
+
onClick={() => { clearStoreySelection(); clearAllFilters(); }}
|
|
615
707
|
>
|
|
616
|
-
Clear
|
|
708
|
+
Clear all
|
|
617
709
|
</Button>
|
|
618
710
|
</div>
|
|
619
711
|
</div>
|