@ifc-lite/viewer 1.14.4 → 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 +21 -0
- package/dist/assets/{Arrow.dom-_vGzMMKs.js → Arrow.dom-OVBBPqOB.js} +1 -1
- package/dist/assets/{basketViewActivator-BZcoCL3V.js → basketViewActivator-Bx6QU4ma.js} +1 -1
- package/dist/assets/{browser-Czmf34bo.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-D7nEDctQ.js → index-DsX-NCtx.js} +4 -4
- package/dist/assets/{native-bridge-DAOWftxE.js → native-bridge-D6tKFqGO.js} +1 -1
- package/dist/assets/{wasm-bridge-D7jYpn8a.js → wasm-bridge-D4kvZVDw.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +7 -7
- package/src/components/viewer/ExportDialog.tsx +40 -2
- package/src/components/viewer/HierarchyPanel.tsx +99 -22
- package/src/components/viewer/ViewportContainer.tsx +30 -25
- 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-CMQ_Dgkr.css +0 -1
- package/dist/assets/index-DX-Qf5fA.js +0 -116950
|
@@ -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,19 +46,19 @@
|
|
|
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.
|
|
53
|
-
"@ifc-lite/lists": "^1.14.3",
|
|
52
|
+
"@ifc-lite/ids": "^1.14.4",
|
|
54
53
|
"@ifc-lite/lens": "^1.14.3",
|
|
54
|
+
"@ifc-lite/lists": "^1.14.4",
|
|
55
55
|
"@ifc-lite/mutations": "^1.14.3",
|
|
56
|
-
"@ifc-lite/parser": "^2.
|
|
56
|
+
"@ifc-lite/parser": "^2.1.0",
|
|
57
57
|
"@ifc-lite/query": "^1.14.4",
|
|
58
58
|
"@ifc-lite/renderer": "^1.14.3",
|
|
59
|
+
"@ifc-lite/sandbox": "^1.14.3",
|
|
59
60
|
"@ifc-lite/server-client": "^1.14.3",
|
|
60
61
|
"@ifc-lite/spatial": "^1.14.3",
|
|
61
|
-
"@ifc-lite/sandbox": "^1.14.3",
|
|
62
62
|
"@ifc-lite/wasm": "^1.14.3"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -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
|
}
|
|
@@ -357,7 +388,7 @@ export function HierarchyPanel() {
|
|
|
357
388
|
setSelectedEntity(resolveEntityRef(globalId));
|
|
358
389
|
}
|
|
359
390
|
}
|
|
360
|
-
}, [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]);
|
|
361
392
|
|
|
362
393
|
// Compute selection and visibility state for a node
|
|
363
394
|
const computeNodeState = useCallback((node: TreeNode): { isSelected: boolean; nodeHidden: boolean; modelVisible?: boolean } => {
|
|
@@ -551,21 +582,44 @@ export function HierarchyPanel() {
|
|
|
551
582
|
</div>
|
|
552
583
|
|
|
553
584
|
{/* Footer status */}
|
|
554
|
-
{
|
|
585
|
+
{hasActiveFilters ? (
|
|
555
586
|
<div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 bg-primary text-white dark:bg-primary">
|
|
556
|
-
<div className="flex items-center justify-between text-xs font-medium">
|
|
557
|
-
<
|
|
558
|
-
{selectedStoreys.size
|
|
559
|
-
|
|
560
|
-
|
|
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">
|
|
561
615
|
<span className="opacity-70 text-[10px] font-mono">ESC</span>
|
|
562
616
|
<Button
|
|
563
617
|
variant="ghost"
|
|
564
618
|
size="sm"
|
|
565
619
|
className="h-6 text-[10px] uppercase border border-white/20 hover:bg-white/20 hover:text-white rounded-none px-2"
|
|
566
|
-
onClick={clearStoreySelection}
|
|
620
|
+
onClick={() => { clearStoreySelection(); clearAllFilters(); }}
|
|
567
621
|
>
|
|
568
|
-
Clear
|
|
622
|
+
Clear all
|
|
569
623
|
</Button>
|
|
570
624
|
</div>
|
|
571
625
|
</div>
|
|
@@ -614,21 +668,44 @@ export function HierarchyPanel() {
|
|
|
614
668
|
</div>
|
|
615
669
|
|
|
616
670
|
{/* Footer status */}
|
|
617
|
-
{
|
|
671
|
+
{hasActiveFilters ? (
|
|
618
672
|
<div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 bg-primary text-white dark:bg-primary">
|
|
619
|
-
<div className="flex items-center justify-between text-xs font-medium">
|
|
620
|
-
<
|
|
621
|
-
{selectedStoreys.size
|
|
622
|
-
|
|
623
|
-
|
|
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">
|
|
624
701
|
<span className="opacity-70 text-[10px] font-mono">ESC</span>
|
|
625
702
|
<Button
|
|
626
703
|
variant="ghost"
|
|
627
704
|
size="sm"
|
|
628
705
|
className="h-6 text-[10px] uppercase border border-white/20 hover:bg-white/20 hover:text-white rounded-none px-2"
|
|
629
|
-
onClick={clearStoreySelection}
|
|
706
|
+
onClick={() => { clearStoreySelection(); clearAllFilters(); }}
|
|
630
707
|
>
|
|
631
|
-
Clear
|
|
708
|
+
Clear all
|
|
632
709
|
</Button>
|
|
633
710
|
</div>
|
|
634
711
|
</div>
|
|
@@ -9,6 +9,7 @@ import { ToolOverlays } from './ToolOverlays';
|
|
|
9
9
|
import { Section2DPanel } from './Section2DPanel';
|
|
10
10
|
import { BasketPresentationDock } from './BasketPresentationDock';
|
|
11
11
|
import { useViewerStore } from '@/store';
|
|
12
|
+
import { collectIfcBuildingStoreyElementsWithIfcSpace } from '@/store/basketVisibleSet';
|
|
12
13
|
import { useIfc } from '@/hooks/useIfc';
|
|
13
14
|
import { useWebGPU } from '@/hooks/useWebGPU';
|
|
14
15
|
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus } from 'lucide-react';
|
|
@@ -27,6 +28,7 @@ export function ViewportContainer() {
|
|
|
27
28
|
const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
|
|
28
29
|
const typeVisibility = useViewerStore((s) => s.typeVisibility);
|
|
29
30
|
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
31
|
+
const classFilter = useViewerStore((s) => s.classFilter);
|
|
30
32
|
// Multi-model support: get all loaded models from store (for merged geometry)
|
|
31
33
|
const storeModels = useViewerStore((s) => s.models);
|
|
32
34
|
const resetViewerState = useViewerStore((s) => s.resetViewerState);
|
|
@@ -251,15 +253,8 @@ export function ViewportContainer() {
|
|
|
251
253
|
// Now supports multi-model: aggregates elements from all models for selected storeys
|
|
252
254
|
// IMPORTANT: Returns globalIds (meshes use globalIds after federation registry transformation)
|
|
253
255
|
const computedIsolatedIds = useMemo(() => {
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
return isolatedEntities;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// If storeys are selected, compute combined element IDs from all selected storeys
|
|
260
|
-
// across ALL models (multi-model support)
|
|
261
|
-
// NOTE: Storey hierarchy uses original expressIds, but meshes use globalIds
|
|
262
|
-
// We must transform expressIds -> globalIds using the model's offset
|
|
256
|
+
// Compute storey isolation if storeys are selected
|
|
257
|
+
let storeyIsolation: Set<number> | null = null;
|
|
263
258
|
if (selectedStoreys.size > 0) {
|
|
264
259
|
const combinedGlobalIds = new Set<number>();
|
|
265
260
|
|
|
@@ -268,46 +263,56 @@ export function ViewportContainer() {
|
|
|
268
263
|
const hierarchy = model.ifcDataStore?.spatialHierarchy;
|
|
269
264
|
if (!hierarchy) continue;
|
|
270
265
|
|
|
271
|
-
// Get this model's offset directly from the model (no need for registry)
|
|
272
266
|
const offset = model.idOffset ?? 0;
|
|
273
267
|
|
|
274
268
|
for (const storeyId of selectedStoreys) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// For now, try both the storeyId and storeyId - offset
|
|
278
|
-
const storeyElementIds = hierarchy.byStorey.get(storeyId) || hierarchy.byStorey.get(storeyId - offset);
|
|
269
|
+
const localStoreyId = hierarchy.byStorey.has(storeyId) ? storeyId : storeyId - offset;
|
|
270
|
+
const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, localStoreyId);
|
|
279
271
|
if (storeyElementIds) {
|
|
280
272
|
for (const originalExpressId of storeyElementIds) {
|
|
281
|
-
|
|
282
|
-
const globalId = originalExpressId + offset;
|
|
283
|
-
combinedGlobalIds.add(globalId);
|
|
273
|
+
combinedGlobalIds.add(originalExpressId + offset);
|
|
284
274
|
}
|
|
285
275
|
}
|
|
286
276
|
}
|
|
287
277
|
}
|
|
288
278
|
|
|
289
|
-
//
|
|
290
|
-
// In this case, offset is 0, so globalId = expressId
|
|
279
|
+
// Legacy single-model mode (offset = 0)
|
|
291
280
|
if (ifcDataStore?.spatialHierarchy && storeModels.size === 0) {
|
|
292
281
|
const hierarchy = ifcDataStore.spatialHierarchy;
|
|
293
282
|
for (const storeyId of selectedStoreys) {
|
|
294
|
-
const storeyElementIds = hierarchy
|
|
283
|
+
const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, storeyId);
|
|
295
284
|
if (storeyElementIds) {
|
|
296
285
|
for (const id of storeyElementIds) {
|
|
297
|
-
combinedGlobalIds.add(id);
|
|
286
|
+
combinedGlobalIds.add(id);
|
|
298
287
|
}
|
|
299
288
|
}
|
|
300
289
|
}
|
|
301
290
|
}
|
|
302
291
|
|
|
303
292
|
if (combinedGlobalIds.size > 0) {
|
|
304
|
-
|
|
293
|
+
storeyIsolation = combinedGlobalIds;
|
|
305
294
|
}
|
|
306
295
|
}
|
|
307
296
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
297
|
+
// Collect all active filters and intersect them
|
|
298
|
+
const filters: Set<number>[] = [];
|
|
299
|
+
if (storeyIsolation !== null) filters.push(storeyIsolation);
|
|
300
|
+
if (classFilter !== null) filters.push(classFilter.ids);
|
|
301
|
+
if (isolatedEntities !== null) filters.push(isolatedEntities);
|
|
302
|
+
|
|
303
|
+
if (filters.length === 0) return null;
|
|
304
|
+
if (filters.length === 1) return filters[0];
|
|
305
|
+
|
|
306
|
+
// Intersect all active filters — start from smallest for efficiency
|
|
307
|
+
const sorted = filters.sort((a, b) => a.size - b.size);
|
|
308
|
+
const intersection = new Set<number>();
|
|
309
|
+
for (const id of sorted[0]) {
|
|
310
|
+
if (sorted.every(s => s.has(id))) {
|
|
311
|
+
intersection.add(id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return intersection;
|
|
315
|
+
}, [storeModels, ifcDataStore, selectedStoreys, isolatedEntities, classFilter]);
|
|
311
316
|
|
|
312
317
|
// Grid Pattern
|
|
313
318
|
const GridPattern = () => (
|
|
@@ -4,7 +4,84 @@
|
|
|
4
4
|
|
|
5
5
|
import type { EntityRef, VisibilityBackendMethods } from '@ifc-lite/sdk';
|
|
6
6
|
import type { StoreApi } from './types.js';
|
|
7
|
-
import { getModelForRef } from './model-compat.js';
|
|
7
|
+
import { getModelForRef, type ModelLike } from './model-compat.js';
|
|
8
|
+
import { collectIfcBuildingStoreyElementsWithIfcSpace } from '../../store/basketVisibleSet.js';
|
|
9
|
+
import { IfcTypeEnum, type SpatialNode } from '@ifc-lite/data';
|
|
10
|
+
|
|
11
|
+
const SPATIAL_TYPES = new Set([
|
|
12
|
+
'IfcBuildingStorey',
|
|
13
|
+
'IfcBuilding',
|
|
14
|
+
'IfcSite',
|
|
15
|
+
'IfcProject',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function findDescendantNode(root: SpatialNode, expressId: number): SpatialNode | null {
|
|
19
|
+
const stack: SpatialNode[] = [root];
|
|
20
|
+
while (stack.length > 0) {
|
|
21
|
+
const node = stack.pop()!;
|
|
22
|
+
if (node.expressId === expressId) return node;
|
|
23
|
+
for (const child of node.children) {
|
|
24
|
+
stack.push(child);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function collectDescendantStoreyIds(node: SpatialNode): number[] {
|
|
31
|
+
const storeyIds: number[] = [];
|
|
32
|
+
const stack: SpatialNode[] = [node];
|
|
33
|
+
while (stack.length > 0) {
|
|
34
|
+
const current = stack.pop()!;
|
|
35
|
+
if (current.type === IfcTypeEnum.IfcBuildingStorey) {
|
|
36
|
+
storeyIds.push(current.expressId);
|
|
37
|
+
}
|
|
38
|
+
for (const child of current.children) {
|
|
39
|
+
stack.push(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return storeyIds;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* If `ref` points to a spatial structure element (storey, building, etc.),
|
|
47
|
+
* expand it to the local expressIds of all contained elements.
|
|
48
|
+
* Otherwise return the original expressId as-is.
|
|
49
|
+
*/
|
|
50
|
+
function expandSpatialRef(ref: EntityRef, model: ModelLike): number[] {
|
|
51
|
+
const dataStore = model.ifcDataStore;
|
|
52
|
+
const typeName = dataStore.entities.getTypeName(ref.expressId) || '';
|
|
53
|
+
if (!SPATIAL_TYPES.has(typeName)) return [ref.expressId];
|
|
54
|
+
|
|
55
|
+
const hierarchy = dataStore.spatialHierarchy;
|
|
56
|
+
if (!hierarchy) return [ref.expressId];
|
|
57
|
+
|
|
58
|
+
if (typeName === 'IfcBuildingStorey') {
|
|
59
|
+
const ids = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, ref.expressId);
|
|
60
|
+
return ids && ids.length > 0 ? ids : [ref.expressId];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// For higher-level containers (IfcBuilding, IfcSite, IfcProject),
|
|
64
|
+
// walk the spatial tree from ref.expressId to find descendant storeys only
|
|
65
|
+
const startNode = findDescendantNode(hierarchy.project, ref.expressId);
|
|
66
|
+
if (!startNode) return [ref.expressId];
|
|
67
|
+
|
|
68
|
+
const descendantStoreyIds = collectDescendantStoreyIds(startNode);
|
|
69
|
+
|
|
70
|
+
const allIds: number[] = [];
|
|
71
|
+
const seen = new Set<number>();
|
|
72
|
+
for (const storeyId of descendantStoreyIds) {
|
|
73
|
+
const storeyIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, storeyId);
|
|
74
|
+
if (storeyIds) {
|
|
75
|
+
for (const id of storeyIds) {
|
|
76
|
+
if (!seen.has(id)) {
|
|
77
|
+
seen.add(id);
|
|
78
|
+
allIds.push(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return allIds.length > 0 ? allIds : [ref.expressId];
|
|
84
|
+
}
|
|
8
85
|
|
|
9
86
|
export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMethods {
|
|
10
87
|
return {
|
|
@@ -44,7 +121,10 @@ export function createVisibilityAdapter(store: StoreApi): VisibilityBackendMetho
|
|
|
44
121
|
for (const ref of refs) {
|
|
45
122
|
const model = getModelForRef(state, ref.modelId);
|
|
46
123
|
if (model) {
|
|
47
|
-
|
|
124
|
+
const expanded = expandSpatialRef(ref, model);
|
|
125
|
+
for (const id of expanded) {
|
|
126
|
+
globalIds.push(id + model.idOffset);
|
|
127
|
+
}
|
|
48
128
|
}
|
|
49
129
|
}
|
|
50
130
|
if (globalIds.length > 0) {
|