@ifc-lite/viewer 1.14.0 → 1.14.1
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 +24 -0
- package/dist/assets/{Arrow.dom-CNguvlQi.js → Arrow.dom-HLSMJR_v.js} +1 -1
- package/dist/assets/{browser-D6lgLpkA.js → browser-Ch0OnmZN.js} +1 -1
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/{index-UaDsJsCR.js → index-DJbbSLF9.js} +19897 -19545
- package/dist/assets/{index-BMwpw264.js → index-JPFMj8C9.js} +4 -4
- package/dist/assets/{native-bridge-DqELq4X0.js → native-bridge-BzC7HkDs.js} +1 -1
- package/dist/assets/{wasm-bridge-CVWvHlfH.js → wasm-bridge-B_7dPwOa.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +19 -19
- package/src/components/viewer/HierarchyPanel.tsx +7 -1
- package/src/components/viewer/MainToolbar.tsx +51 -18
- package/src/components/viewer/Viewport.tsx +5 -1
- package/src/components/viewer/ViewportContainer.tsx +59 -37
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +8 -4
- package/src/components/viewer/properties/BsddCard.tsx +2 -2
- package/src/components/viewer/useGeometryStreaming.ts +189 -55
- package/src/components/viewer/useMouseControls.ts +55 -14
- package/src/components/viewer/useTouchControls.ts +2 -0
- package/src/hooks/useIfc.ts +19 -1
- package/src/hooks/useIfcCache.ts +6 -1
- package/src/hooks/useIfcFederation.ts +16 -1
- package/src/hooks/useIfcLoader.ts +16 -4
- package/src/store/slices/dataSlice.ts +9 -4
- package/src/utils/localParsingUtils.ts +3 -1
- package/tsconfig.json +12 -1
- package/dist/assets/ifc-lite_bg-B6s-pcv0.wasm +0 -0
|
@@ -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 S, __tla as __tla_0 } from "./index-
|
|
3
|
-
import { N as j, m as B, __tla as __tla_1 } from "./index-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browser-Ch0OnmZN.js","assets/index-DJbbSLF9.js","assets/index-Qp8stcGO.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import { _ as u, b as S, __tla as __tla_0 } from "./index-DJbbSLF9.js";
|
|
3
|
+
import { N as j, m as B, __tla as __tla_1 } from "./index-DJbbSLF9.js";
|
|
4
4
|
let c, g, L, D, x, R, A;
|
|
5
5
|
let __tla = Promise.all([
|
|
6
6
|
(()=>{
|
|
@@ -87,7 +87,7 @@ let __tla = Promise.all([
|
|
|
87
87
|
function k() {
|
|
88
88
|
return m || (m = (async ()=>{
|
|
89
89
|
try {
|
|
90
|
-
const e = await u(()=>import("./browser-
|
|
90
|
+
const e = await u(()=>import("./browser-Ch0OnmZN.js").then((i)=>i.b), __vite__mapDeps([0,1,2])), t = e.default ?? e;
|
|
91
91
|
let s;
|
|
92
92
|
try {
|
|
93
93
|
s = (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-DJbbSLF9.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,7 +44,7 @@
|
|
|
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-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-DJbbSLF9.js"></script>
|
|
48
48
|
<link rel="stylesheet" crossorigin href="/assets/index-Qp8stcGO.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -41,24 +41,24 @@
|
|
|
41
41
|
"tailwind-merge": "^3.4.0",
|
|
42
42
|
"tailwindcss": "^4.1.18",
|
|
43
43
|
"zustand": "^4.4.0",
|
|
44
|
-
"@ifc-lite/bcf": "^1.14.
|
|
45
|
-
"@ifc-lite/cache": "^1.14.
|
|
46
|
-
"@ifc-lite/data": "^1.14.
|
|
47
|
-
"@ifc-lite/drawing-2d": "^1.14.
|
|
48
|
-
"@ifc-lite/encoding": "^1.14.
|
|
49
|
-
"@ifc-lite/export": "^1.14.
|
|
50
|
-
"@ifc-lite/geometry": "^1.14.
|
|
51
|
-
"@ifc-lite/ids": "^1.14.
|
|
52
|
-
"@ifc-lite/lens": "^1.14.
|
|
53
|
-
"@ifc-lite/lists": "^1.14.
|
|
54
|
-
"@ifc-lite/mutations": "^1.14.
|
|
55
|
-
"@ifc-lite/parser": "^1.14.
|
|
56
|
-
"@ifc-lite/query": "^1.14.
|
|
57
|
-
"@ifc-lite/renderer": "^1.14.
|
|
58
|
-
"@ifc-lite/sandbox": "^1.14.
|
|
59
|
-
"@ifc-lite/server-client": "^1.14.
|
|
60
|
-
"@ifc-lite/spatial": "^1.14.
|
|
61
|
-
"@ifc-lite/wasm": "^1.14.
|
|
44
|
+
"@ifc-lite/bcf": "^1.14.1",
|
|
45
|
+
"@ifc-lite/cache": "^1.14.1",
|
|
46
|
+
"@ifc-lite/data": "^1.14.1",
|
|
47
|
+
"@ifc-lite/drawing-2d": "^1.14.1",
|
|
48
|
+
"@ifc-lite/encoding": "^1.14.1",
|
|
49
|
+
"@ifc-lite/export": "^1.14.1",
|
|
50
|
+
"@ifc-lite/geometry": "^1.14.1",
|
|
51
|
+
"@ifc-lite/ids": "^1.14.1",
|
|
52
|
+
"@ifc-lite/lens": "^1.14.1",
|
|
53
|
+
"@ifc-lite/lists": "^1.14.1",
|
|
54
|
+
"@ifc-lite/mutations": "^1.14.1",
|
|
55
|
+
"@ifc-lite/parser": "^1.14.1",
|
|
56
|
+
"@ifc-lite/query": "^1.14.1",
|
|
57
|
+
"@ifc-lite/renderer": "^1.14.1",
|
|
58
|
+
"@ifc-lite/sandbox": "^1.14.1",
|
|
59
|
+
"@ifc-lite/server-client": "^1.14.1",
|
|
60
|
+
"@ifc-lite/spatial": "^1.14.1",
|
|
61
|
+
"@ifc-lite/wasm": "^1.14.1"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@tailwindcss/postcss": "^4.1.18",
|
|
@@ -200,7 +200,9 @@ export function HierarchyPanel() {
|
|
|
200
200
|
if (node.type === 'type-group') {
|
|
201
201
|
const elements = getNodeElements(node);
|
|
202
202
|
if (elements.length > 0) {
|
|
203
|
-
|
|
203
|
+
// Clear multi-selection highlight — isolate shows the class members,
|
|
204
|
+
// but we don't want every element highlighted/selected
|
|
205
|
+
setSelectedEntityIds([]);
|
|
204
206
|
setSelectedEntity(resolveEntityRef(elements[0]));
|
|
205
207
|
isolateEntities(elements);
|
|
206
208
|
}
|
|
@@ -288,6 +290,10 @@ export function HierarchyPanel() {
|
|
|
288
290
|
const elementId = node.expressIds[0]; // Original expressId
|
|
289
291
|
const modelId = node.modelIds[0];
|
|
290
292
|
|
|
293
|
+
// Clear multi-selection (e.g. from a prior type-group click) so only
|
|
294
|
+
// this single element is highlighted, matching Viewport pick behavior
|
|
295
|
+
setSelectedEntityIds([]);
|
|
296
|
+
|
|
291
297
|
if (modelId !== 'legacy') {
|
|
292
298
|
// Multi-model: need to convert to globalId for renderer
|
|
293
299
|
const model = models.get(modelId);
|
|
@@ -196,37 +196,70 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
196
196
|
const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
|
|
197
197
|
const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
|
|
198
198
|
|
|
199
|
-
// Check which type geometries exist across ALL loaded models (federation-aware)
|
|
199
|
+
// Check which type geometries exist across ALL loaded models (federation-aware).
|
|
200
|
+
// PERF: Use meshes.length as dep proxy instead of full geometryResult, and
|
|
201
|
+
// scan incrementally — once a type is found it stays found, so we only scan
|
|
202
|
+
// NEW meshes since the last check. Per-model cursors ensure federated models
|
|
203
|
+
// each track their own scan position independently.
|
|
204
|
+
const typeGeomScanRef = useRef({
|
|
205
|
+
spaces: false, openings: false, site: false,
|
|
206
|
+
legacyLastLen: 0,
|
|
207
|
+
modelLastLen: new Map<string | number, number>(),
|
|
208
|
+
});
|
|
209
|
+
const meshLen = geometryResult?.meshes.length ?? 0;
|
|
200
210
|
const typeGeometryExists = useMemo(() => {
|
|
201
|
-
const
|
|
211
|
+
const scan = typeGeomScanRef.current;
|
|
212
|
+
|
|
213
|
+
// Reset if legacy meshes array shrunk (new file loaded)
|
|
214
|
+
if (meshLen < scan.legacyLastLen) {
|
|
215
|
+
scan.spaces = false;
|
|
216
|
+
scan.openings = false;
|
|
217
|
+
scan.site = false;
|
|
218
|
+
scan.legacyLastLen = 0;
|
|
219
|
+
scan.modelLastLen.clear();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Already found all types — nothing to do
|
|
223
|
+
if (scan.spaces && scan.openings && scan.site) {
|
|
224
|
+
return { spaces: scan.spaces, openings: scan.openings, site: scan.site };
|
|
225
|
+
}
|
|
202
226
|
|
|
203
|
-
// Check
|
|
227
|
+
// Check federated models (scan only new meshes per model)
|
|
204
228
|
if (models.size > 0) {
|
|
205
|
-
for (const [, model] of models) {
|
|
229
|
+
for (const [modelId, model] of models) {
|
|
206
230
|
const meshes = model.geometryResult?.meshes;
|
|
207
231
|
if (!meshes) continue;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
232
|
+
const modelStart = scan.modelLastLen.get(modelId) ?? 0;
|
|
233
|
+
// Reset cursor if model was reloaded (mesh array shrunk)
|
|
234
|
+
const start = meshes.length < modelStart ? 0 : modelStart;
|
|
235
|
+
for (let i = start; i < meshes.length; i++) {
|
|
236
|
+
const t = meshes[i].ifcType;
|
|
237
|
+
if (t === 'IfcSpace') scan.spaces = true;
|
|
238
|
+
else if (t === 'IfcOpeningElement') scan.openings = true;
|
|
239
|
+
else if (t === 'IfcSite') scan.site = true;
|
|
240
|
+
if (scan.spaces && scan.openings && scan.site) break;
|
|
214
241
|
}
|
|
242
|
+
scan.modelLastLen.set(modelId, meshes.length);
|
|
243
|
+
if (scan.spaces && scan.openings && scan.site) break;
|
|
215
244
|
}
|
|
216
245
|
}
|
|
217
246
|
|
|
218
|
-
//
|
|
247
|
+
// Legacy single-model path (scan only new meshes)
|
|
219
248
|
if (geometryResult?.meshes) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
249
|
+
const meshes = geometryResult.meshes;
|
|
250
|
+
for (let i = scan.legacyLastLen; i < meshes.length; i++) {
|
|
251
|
+
const t = meshes[i].ifcType;
|
|
252
|
+
if (t === 'IfcSpace') scan.spaces = true;
|
|
253
|
+
else if (t === 'IfcOpeningElement') scan.openings = true;
|
|
254
|
+
else if (t === 'IfcSite') scan.site = true;
|
|
255
|
+
if (scan.spaces && scan.openings && scan.site) break;
|
|
225
256
|
}
|
|
226
257
|
}
|
|
227
258
|
|
|
228
|
-
|
|
229
|
-
|
|
259
|
+
scan.legacyLastLen = meshLen;
|
|
260
|
+
return { spaces: scan.spaces, openings: scan.openings, site: scan.site };
|
|
261
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- meshLen is a stable proxy for geometryResult
|
|
262
|
+
}, [models, meshLen]);
|
|
230
263
|
|
|
231
264
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
232
265
|
const files = e.target.files;
|
|
@@ -40,12 +40,15 @@ import { useRenderUpdates } from './useRenderUpdates.js';
|
|
|
40
40
|
|
|
41
41
|
interface ViewportProps {
|
|
42
42
|
geometry: MeshData[] | null;
|
|
43
|
+
/** Monotonic counter that increments when geometry changes — used to trigger
|
|
44
|
+
* streaming effects even when the geometry array reference is stable. */
|
|
45
|
+
geometryVersion?: number;
|
|
43
46
|
coordinateInfo?: CoordinateInfo;
|
|
44
47
|
computedIsolatedIds?: Set<number> | null;
|
|
45
48
|
modelIdToIndex?: Map<string, number>;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelIdToIndex }: ViewportProps) {
|
|
51
|
+
export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIsolatedIds, modelIdToIndex }: ViewportProps) {
|
|
49
52
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
50
53
|
const rendererRef = useRef<Renderer | null>(null);
|
|
51
54
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
@@ -789,6 +792,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
789
792
|
rendererRef,
|
|
790
793
|
isInitialized,
|
|
791
794
|
geometry,
|
|
795
|
+
geometryVersion,
|
|
792
796
|
coordinateInfo,
|
|
793
797
|
isStreaming,
|
|
794
798
|
geometryBoundsRef,
|
|
@@ -173,58 +173,79 @@ export function ViewportContainer() {
|
|
|
173
173
|
// Check if any models are loaded (even if hidden) - used to show empty 3D vs starting UI
|
|
174
174
|
const hasLoadedModels = storeModels.size > 0 || (geometryResult?.meshes && geometryResult.meshes.length > 0);
|
|
175
175
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
176
|
+
// PERF: Incremental geometry filtering using refs.
|
|
177
|
+
// Instead of creating a new 200K+ element array every batch (~200ms),
|
|
178
|
+
// we push ONLY new meshes into a cached array — O(batch_size) not O(total).
|
|
179
|
+
// A version counter triggers downstream re-renders via the Viewport prop.
|
|
180
|
+
const filteredCacheRef = useRef<MeshData[]>([]);
|
|
181
|
+
const filteredSourceLenRef = useRef(0);
|
|
182
|
+
const filteredTypeVisRef = useRef(typeVisibility);
|
|
183
|
+
const filteredVersionRef = useRef(0);
|
|
184
|
+
|
|
180
185
|
const filteredGeometry = useMemo(() => {
|
|
181
186
|
if (!mergedGeometryResult?.meshes) {
|
|
187
|
+
filteredCacheRef.current = [];
|
|
188
|
+
filteredSourceLenRef.current = 0;
|
|
189
|
+
filteredVersionRef.current = 0;
|
|
182
190
|
return null;
|
|
183
191
|
}
|
|
184
192
|
|
|
185
|
-
|
|
193
|
+
const allMeshes = mergedGeometryResult.meshes;
|
|
194
|
+
const cache = filteredCacheRef.current;
|
|
195
|
+
|
|
196
|
+
// Full rebuild if: type visibility changed, source shrunk (new file), or empty cache
|
|
197
|
+
const prevVis = filteredTypeVisRef.current;
|
|
198
|
+
const typeVisChanged =
|
|
199
|
+
prevVis.spaces !== typeVisibility.spaces ||
|
|
200
|
+
prevVis.openings !== typeVisibility.openings ||
|
|
201
|
+
prevVis.site !== typeVisibility.site;
|
|
202
|
+
if (typeVisChanged || allMeshes.length < filteredSourceLenRef.current) {
|
|
203
|
+
cache.length = 0;
|
|
204
|
+
filteredSourceLenRef.current = 0;
|
|
205
|
+
filteredTypeVisRef.current = typeVisibility;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const needsFilter = !typeVisibility.spaces || !typeVisibility.openings || !typeVisibility.site;
|
|
209
|
+
const prevCacheLen = cache.length;
|
|
186
210
|
|
|
187
|
-
//
|
|
188
|
-
|
|
211
|
+
// Only process NEW meshes since last run — O(batch_size) not O(total)
|
|
212
|
+
for (let i = filteredSourceLenRef.current; i < allMeshes.length; i++) {
|
|
213
|
+
const mesh = allMeshes[i];
|
|
189
214
|
const ifcType = mesh.ifcType;
|
|
190
215
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
if (needsFilter) {
|
|
217
|
+
if (ifcType === 'IfcSpace' && !typeVisibility.spaces) continue;
|
|
218
|
+
if (ifcType === 'IfcOpeningElement' && !typeVisibility.openings) continue;
|
|
219
|
+
if (ifcType === 'IfcSite' && !typeVisibility.site) continue;
|
|
194
220
|
}
|
|
195
|
-
if (ifcType === 'IfcOpeningElement' && !typeVisibility.openings) {
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
if (ifcType === 'IfcSite' && !typeVisibility.site) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return true;
|
|
203
|
-
});
|
|
204
221
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// Create a new color array with reduced opacity
|
|
213
|
-
const newColor: [number, number, number, number] = [
|
|
214
|
-
mesh.color[0],
|
|
215
|
-
mesh.color[1],
|
|
216
|
-
mesh.color[2],
|
|
217
|
-
Math.min(mesh.color[3] * 0.3, 0.3), // Semi-transparent (30% opacity max)
|
|
218
|
-
];
|
|
219
|
-
return { ...mesh, color: newColor };
|
|
222
|
+
if (ifcType === 'IfcSpace' || ifcType === 'IfcOpeningElement') {
|
|
223
|
+
cache.push({
|
|
224
|
+
...mesh,
|
|
225
|
+
color: [mesh.color[0], mesh.color[1], mesh.color[2], Math.min(mesh.color[3] * 0.3, 0.3)],
|
|
226
|
+
});
|
|
227
|
+
} else {
|
|
228
|
+
cache.push(mesh);
|
|
220
229
|
}
|
|
230
|
+
}
|
|
221
231
|
|
|
222
|
-
|
|
223
|
-
});
|
|
232
|
+
filteredSourceLenRef.current = allMeshes.length;
|
|
224
233
|
|
|
225
|
-
|
|
234
|
+
// Only bump version when cache content actually changed — avoids
|
|
235
|
+
// unnecessary downstream re-renders when memo runs with same data.
|
|
236
|
+
if (cache.length !== prevCacheLen || typeVisChanged) {
|
|
237
|
+
filteredVersionRef.current++;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Return the same array reference — downstream change detection uses
|
|
241
|
+
// geometryVersion (which increments each batch) instead of array identity.
|
|
242
|
+
return cache;
|
|
226
243
|
}, [mergedGeometryResult, typeVisibility]);
|
|
227
244
|
|
|
245
|
+
// Version counter that changes every batch — triggers useGeometryStreaming
|
|
246
|
+
// without requiring a new geometry array reference.
|
|
247
|
+
const geometryVersion = filteredVersionRef.current;
|
|
248
|
+
|
|
228
249
|
// Compute combined isolation set (storeys + manual isolation)
|
|
229
250
|
// This is passed to the renderer for batch-level visibility filtering
|
|
230
251
|
// Now supports multi-model: aggregates elements from all models for selected storeys
|
|
@@ -580,6 +601,7 @@ export function ViewportContainer() {
|
|
|
580
601
|
|
|
581
602
|
<Viewport
|
|
582
603
|
geometry={filteredGeometry}
|
|
604
|
+
geometryVersion={geometryVersion}
|
|
583
605
|
coordinateInfo={mergedGeometryResult?.coordinateInfo}
|
|
584
606
|
computedIsolatedIds={computedIsolatedIds}
|
|
585
607
|
modelIdToIndex={modelIdToIndex}
|
|
@@ -164,11 +164,15 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
|
|
|
164
164
|
return geometryResult?.meshes.length ?? 0;
|
|
165
165
|
}, [models, geometryResult?.meshes.length]);
|
|
166
166
|
|
|
167
|
-
// Pre-computed set of global IDs with geometry — stable across color changes
|
|
167
|
+
// Pre-computed set of global IDs with geometry — stable across color changes.
|
|
168
|
+
// PERF: Skip when no geometry source exists (during initial streaming before
|
|
169
|
+
// any data is ready). Gate on models OR ifcDataStore so federated scenarios
|
|
170
|
+
// (models.size > 0 but ifcDataStore is null) still build the set correctly.
|
|
171
|
+
const hasGeometrySource = models.size > 0 || !!ifcDataStore;
|
|
168
172
|
const geometricIds = useMemo(
|
|
169
|
-
() => buildGeometricIdSet(models, geometryResult),
|
|
170
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- meshCount is a stable proxy
|
|
171
|
-
[models, meshCount]
|
|
173
|
+
() => hasGeometrySource ? buildGeometricIdSet(models, geometryResult) : new Set<number>(),
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- meshCount is a stable proxy; hasGeometrySource gates streaming
|
|
175
|
+
[models, hasGeometrySource ? meshCount : 0]
|
|
172
176
|
);
|
|
173
177
|
|
|
174
178
|
// Build the tree data structure based on grouping mode
|
|
@@ -16,7 +16,7 @@ import { Button } from '@/components/ui/button';
|
|
|
16
16
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
17
17
|
import { Badge } from '@/components/ui/badge';
|
|
18
18
|
import { useViewerStore } from '@/store';
|
|
19
|
-
import { PropertyValueType, QuantityType } from '@ifc-lite/data';
|
|
19
|
+
import { type PropertyValue, PropertyValueType, QuantityType } from '@ifc-lite/data';
|
|
20
20
|
import {
|
|
21
21
|
fetchClassInfo,
|
|
22
22
|
bsddDataTypeLabel,
|
|
@@ -59,7 +59,7 @@ function toPropertyValueType(bsddType: string | null): PropertyValueType {
|
|
|
59
59
|
return PropertyValueType.Label;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function defaultValue(_bsddType: string | null):
|
|
62
|
+
function defaultValue(_bsddType: string | null): PropertyValue {
|
|
63
63
|
// Always return empty string – user fills in values manually
|
|
64
64
|
return '';
|
|
65
65
|
}
|