@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.
@@ -1,6 +1,6 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/browser-D6lgLpkA.js","assets/index-UaDsJsCR.js","assets/index-Qp8stcGO.css"])))=>i.map(i=>d[i]);
2
- import { _ as u, b as S, __tla as __tla_0 } from "./index-UaDsJsCR.js";
3
- import { N as j, m as B, __tla as __tla_1 } from "./index-UaDsJsCR.js";
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-D6lgLpkA.js").then((i)=>i.b), __vite__mapDeps([0,1,2])), t = e.default ?? e;
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,4 +1,4 @@
1
- import { _ as c, __tla as __tla_0 } from "./index-UaDsJsCR.js";
1
+ import { _ as c, __tla as __tla_0 } from "./index-DJbbSLF9.js";
2
2
  let m;
3
3
  let __tla = Promise.all([
4
4
  (()=>{
@@ -1 +1 @@
1
- import{I as f,a as m}from"./index-UaDsJsCR.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};
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-UaDsJsCR.js"></script>
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.0",
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.0",
45
- "@ifc-lite/cache": "^1.14.0",
46
- "@ifc-lite/data": "^1.14.0",
47
- "@ifc-lite/drawing-2d": "^1.14.0",
48
- "@ifc-lite/encoding": "^1.14.0",
49
- "@ifc-lite/export": "^1.14.0",
50
- "@ifc-lite/geometry": "^1.14.0",
51
- "@ifc-lite/ids": "^1.14.0",
52
- "@ifc-lite/lens": "^1.14.0",
53
- "@ifc-lite/lists": "^1.14.0",
54
- "@ifc-lite/mutations": "^1.14.0",
55
- "@ifc-lite/parser": "^1.14.0",
56
- "@ifc-lite/query": "^1.14.0",
57
- "@ifc-lite/renderer": "^1.14.0",
58
- "@ifc-lite/sandbox": "^1.14.0",
59
- "@ifc-lite/server-client": "^1.14.0",
60
- "@ifc-lite/spatial": "^1.14.0",
61
- "@ifc-lite/wasm": "^1.14.0"
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
- setSelectedEntityIds(elements);
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 result = { spaces: false, openings: false, site: false };
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 all federated models
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
- for (const m of meshes) {
209
- if (m.ifcType === 'IfcSpace') result.spaces = true;
210
- else if (m.ifcType === 'IfcOpeningElement') result.openings = true;
211
- else if (m.ifcType === 'IfcSite') result.site = true;
212
- // Early exit if all found
213
- if (result.spaces && result.openings && result.site) return result;
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
- // Fallback: also check legacy single-model geometryResult
247
+ // Legacy single-model path (scan only new meshes)
219
248
  if (geometryResult?.meshes) {
220
- for (const m of geometryResult.meshes) {
221
- if (m.ifcType === 'IfcSpace') result.spaces = true;
222
- else if (m.ifcType === 'IfcOpeningElement') result.openings = true;
223
- else if (m.ifcType === 'IfcSite') result.site = true;
224
- if (result.spaces && result.openings && result.site) return result;
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
- return result;
229
- }, [models, geometryResult]);
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
- // Filter geometry based on type visibility only
177
- // PERFORMANCE FIX: Don't filter by storey or hiddenEntities here
178
- // Instead, let the renderer handle visibility filtering at the batch level
179
- // This avoids expensive batch rebuilding when visibility changes
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
- let meshes = mergedGeometryResult.meshes;
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
- // Filter by type visibility (spatial elements)
188
- meshes = meshes.filter(mesh => {
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
- // Check type visibility
192
- if (ifcType === 'IfcSpace' && !typeVisibility.spaces) {
193
- return false;
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
- // Apply transparency for spatial elements
206
- meshes = meshes.map(mesh => {
207
- const ifcType = mesh.ifcType;
208
- const isSpace = ifcType === 'IfcSpace';
209
- const isOpening = ifcType === 'IfcOpeningElement';
210
-
211
- if (isSpace || isOpening) {
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
- return mesh;
223
- });
232
+ filteredSourceLenRef.current = allMeshes.length;
224
233
 
225
- return meshes;
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): unknown {
62
+ function defaultValue(_bsddType: string | null): PropertyValue {
63
63
  // Always return empty string – user fills in values manually
64
64
  return '';
65
65
  }