@ifc-lite/viewer 1.17.2 → 1.17.3

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.
Files changed (62) hide show
  1. package/.turbo/turbo-build.log +30 -29
  2. package/.turbo/turbo-typecheck.log +1 -42
  3. package/CHANGELOG.md +9 -0
  4. package/dist/assets/arrow-DJf2ErbF.js +20 -0
  5. package/dist/assets/basketViewActivator-aojwdomq.js +1 -0
  6. package/dist/assets/bcf-D5-QWGO9.js +281 -0
  7. package/dist/assets/{browser-BDShTXzi.js → browser-CKs-FY1P.js} +1 -1
  8. package/dist/assets/drawing-2d-gWfpdfYe.js +257 -0
  9. package/dist/assets/epsg-index.generated-BjJrt_0S.js +1 -0
  10. package/dist/assets/exporters-C_6J153K.js +79896 -0
  11. package/dist/assets/geometry.worker-Nz9_YIqh.js +1 -0
  12. package/dist/assets/ids-B4jTqB1O.js +1 -0
  13. package/dist/assets/{ifc-lite_bg-FNRmpSvM.wasm → ifc-lite_bg-eSkBTizQ.wasm} +0 -0
  14. package/dist/assets/index-jhBr1wbn.js +101666 -0
  15. package/dist/assets/index-pbE7itQS.css +1 -0
  16. package/dist/assets/lens-CSASnhAL.js +1 -0
  17. package/dist/assets/maplibre-gl-BpvwNKKy.js +811 -0
  18. package/dist/assets/{native-bridge-Crsb7TKz.js → native-bridge-DSIyEYXG.js} +6 -4
  19. package/dist/assets/{arrow2-bb-jcVEo.js → parquet-CEXmQNRO.js} +2 -2
  20. package/dist/assets/sandbox-B79eavQ3.js +5933 -0
  21. package/dist/assets/server-client-D3bUPJJc.js +626 -0
  22. package/dist/assets/wasm-bridge-B0J07fZZ.js +1 -0
  23. package/dist/assets/zip-B-jFFAGa.js +12 -0
  24. package/dist/index.html +11 -2
  25. package/package.json +24 -19
  26. package/src/components/viewer/ExportChangesButton.tsx +18 -3
  27. package/src/components/viewer/ExportDialog.tsx +16 -3
  28. package/src/components/viewer/HierarchyPanel.tsx +6 -6
  29. package/src/components/viewer/PropertiesPanel.tsx +96 -60
  30. package/src/components/viewer/Section2DPanel.tsx +3 -2
  31. package/src/components/viewer/ViewportContainer.tsx +5 -4
  32. package/src/components/viewer/hierarchy/treeDataBuilder.ts +2 -1
  33. package/src/components/viewer/properties/EpsgLookupDialog.tsx +418 -0
  34. package/src/components/viewer/properties/GeoreferencingPanel.tsx +591 -0
  35. package/src/components/viewer/properties/LocationMap.tsx +289 -0
  36. package/src/components/viewer/properties/ModelMetadataPanel.tsx +3 -70
  37. package/src/hooks/bcfIdLookup.ts +13 -11
  38. package/src/hooks/ids/idsColorSystem.ts +3 -8
  39. package/src/hooks/useIDS.ts +31 -16
  40. package/src/hooks/useIfcFederation.ts +2 -2
  41. package/src/lib/geo/kmz-exporter.ts +112 -0
  42. package/src/lib/geo/reproject.ts +244 -0
  43. package/src/lib/lens/adapter.ts +3 -1
  44. package/src/main.tsx +1 -0
  45. package/src/sdk/adapters/export-adapter.ts +14 -1
  46. package/src/sdk/adapters/viewer-adapter.ts +5 -9
  47. package/src/sdk/adapters/visibility-adapter.ts +6 -9
  48. package/src/store/basketVisibleSet.ts +3 -4
  49. package/src/store/globalId.ts +79 -0
  50. package/src/store/index.ts +1 -0
  51. package/src/store/slices/mutationSlice.ts +178 -0
  52. package/src/store/slices/pinboardSlice.ts +4 -8
  53. package/vite.config.ts +17 -0
  54. package/dist/assets/Arrow.dom-BhOg9lpn.js +0 -20
  55. package/dist/assets/arrow2_bg-BlXl-cSQ.js +0 -1
  56. package/dist/assets/basketViewActivator-BRG5DBmM.js +0 -1
  57. package/dist/assets/geometry.worker-kgiT_Qhh.js +0 -1
  58. package/dist/assets/index-B1Ecw4AU.js +0 -189756
  59. package/dist/assets/index-Ba4eoTe7.css +0 -1
  60. package/dist/assets/index-CrgYBjTn.js +0 -229
  61. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +0 -6
  62. package/dist/assets/wasm-bridge-mJUhb7uk.js +0 -1
@@ -27,11 +27,12 @@ import { ScrollArea } from '@/components/ui/scroll-area';
27
27
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
28
28
  import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
29
29
  import { useViewerStore } from '@/store';
30
+ import { toGlobalIdFromModels } from '@/store/globalId';
30
31
  import { useIfc } from '@/hooks/useIfc';
31
32
  import { configureMutationView } from '@/utils/configureMutationView';
32
33
  import { IfcQuery } from '@ifc-lite/query';
33
34
  import { MutablePropertyView } from '@ifc-lite/mutations';
34
- import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, type IfcDataStore } from '@ifc-lite/parser';
35
+ import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, type IfcDataStore } from '@ifc-lite/parser';
35
36
  import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
36
37
  import type { EntityRef, FederatedModel } from '@/store/types';
37
38
 
@@ -45,6 +46,7 @@ import { DocumentCard } from './properties/DocumentCard';
45
46
  import { RelationshipsCard } from './properties/RelationshipsCard';
46
47
  import type { PropertySet, QuantitySet } from './properties/encodingUtils';
47
48
  import { BsddCard } from './properties/BsddCard';
49
+ import { GeoreferencingPanel } from './properties/GeoreferencingPanel';
48
50
 
49
51
  type DisplayProperty = { name: string; value: unknown; isMutated: boolean };
50
52
  type DisplayPropertySet = {
@@ -247,7 +249,7 @@ export function PropertiesPanel() {
247
249
  if (!geoResult?.meshes?.length) return null;
248
250
 
249
251
  // In multi-model mode, meshes use globalIds (originalExpressId + idOffset)
250
- const targetExpressId = selectedEntity.expressId + (model?.idOffset ?? 0);
252
+ const targetExpressId = toGlobalIdFromModels(models, selectedEntity.modelId, selectedEntity.expressId);
251
253
 
252
254
  // Compute bounding box from matching mesh positions
253
255
  let minX = Infinity, minY = Infinity, minZ = Infinity;
@@ -515,6 +517,14 @@ export function PropertiesPanel() {
515
517
  return totalCount > 0 ? rels : null;
516
518
  }, [selectedEntity, model, ifcDataStore]);
517
519
 
520
+ // Extract georeferencing info for the model (used in coordinates section)
521
+ const georef = useMemo(() => {
522
+ const dataStore = model?.ifcDataStore ?? ifcDataStore;
523
+ if (!dataStore) return null;
524
+ const info = extractGeoreferencingOnDemand(dataStore as IfcDataStore);
525
+ return info?.hasGeoreference ? info : null;
526
+ }, [model, ifcDataStore]);
527
+
518
528
  // Extract type-level properties (e.g., from IfcWallType's HasPropertySets)
519
529
  const typeProperties = useMemo(() => {
520
530
  if (!selectedEntity) return null;
@@ -644,8 +654,8 @@ export function PropertiesPanel() {
644
654
 
645
655
  // Separate occurrence (instance) and inherited type properties.
646
656
  // Occurrence properties are displayed first, type properties in a separate section.
647
- // When a pset exists at both levels, occurrence takes precedence; the type-only
648
- // properties (not overridden at instance level) are shown in the inherited section.
657
+ // All type property sets are always shown in the inherited section so users can see
658
+ // what the type defines, even when the same pset exists at occurrence level.
649
659
  const { occurrenceProperties, inheritedTypeProperties } = useMemo(() => {
650
660
  const occ: PropertySet[] = properties.map(p => ({ ...p, source: 'instance' as const }));
651
661
 
@@ -653,23 +663,10 @@ export function PropertiesPanel() {
653
663
  return { occurrenceProperties: occ, inheritedTypeProperties: [] as PropertySet[] };
654
664
  }
655
665
 
656
- const instanceByName = new Set(properties.map(p => p.name));
657
- const inherited: PropertySet[] = [];
658
-
659
- for (const typePset of typeProperties.psets) {
660
- if (instanceByName.has(typePset.name)) {
661
- // Pset exists at instance level - add type-only props that aren't overridden
662
- const instancePset = properties.find(p => p.name === typePset.name)!;
663
- const instancePropNames = new Set(instancePset.properties.map(p => p.name));
664
- const extraProps = typePset.properties.filter(p => !instancePropNames.has(p.name));
665
- if (extraProps.length > 0) {
666
- inherited.push({ ...typePset, properties: extraProps, source: 'type' });
667
- }
668
- } else {
669
- // Type-only pset - show fully in inherited section
670
- inherited.push({ ...typePset, source: 'type' });
671
- }
672
- }
666
+ const inherited: PropertySet[] = typeProperties.psets.map(typePset => ({
667
+ ...typePset,
668
+ source: 'type' as const,
669
+ }));
673
670
 
674
671
  return { occurrenceProperties: occ, inheritedTypeProperties: inherited };
675
672
  }, [properties, typeProperties]);
@@ -770,6 +767,23 @@ export function PropertiesPanel() {
770
767
  }
771
768
 
772
769
  if (!selectedEntityId || !modelQuery || !entityNode) {
770
+ // Show model metadata when a single legacy model is loaded and nothing selected
771
+ if (ifcDataStore && models.size === 0 && geometryResult) {
772
+ const legacyModel: FederatedModel = {
773
+ id: '__legacy__',
774
+ name: 'Model',
775
+ ifcDataStore: ifcDataStore as IfcDataStore,
776
+ geometryResult,
777
+ visible: true,
778
+ collapsed: false,
779
+ schemaVersion: ((ifcDataStore as IfcDataStore).schemaVersion ?? 'IFC4') as FederatedModel['schemaVersion'],
780
+ loadedAt: Date.now(),
781
+ fileSize: (ifcDataStore as IfcDataStore).fileSize ?? 0,
782
+ idOffset: 0,
783
+ maxExpressId: (ifcDataStore as IfcDataStore).entityCount ?? 0,
784
+ };
785
+ return <ModelMetadataPanel model={legacyModel} />;
786
+ }
773
787
  return (
774
788
  <div className="h-full flex flex-col border-l-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black">
775
789
  <div className="p-3 border-b-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
@@ -781,7 +795,7 @@ export function PropertiesPanel() {
781
795
  </div>
782
796
  <p className="font-bold uppercase text-zinc-900 dark:text-zinc-100 mb-2">No Selection</p>
783
797
  <p className="text-xs font-mono text-zinc-500 dark:text-zinc-400 max-w-[150px]">
784
- Select an element to view details
798
+ {models.size > 1 ? 'Select a model or element to view details' : 'Select an element to view details'}
785
799
  </p>
786
800
  </div>
787
801
  </div>
@@ -808,6 +822,13 @@ export function PropertiesPanel() {
808
822
  {entityName || `${entityType}`}
809
823
  </h3>
810
824
  <p className="text-xs font-mono text-zinc-500 dark:text-zinc-400">{entityType}</p>
825
+ {/* Show associated type entity for occurrences */}
826
+ {!isTypeEntity && typeProperties && (
827
+ <p className="text-[11px] font-mono text-indigo-500 dark:text-indigo-400 truncate" title={`${activeDataStore?.entities.getTypeName(typeProperties.typeId) || 'Type'}: ${typeProperties.typeName}`}>
828
+ <Building2 className="inline h-3 w-3 mr-1 -mt-0.5" />
829
+ {activeDataStore?.entities.getTypeName(typeProperties.typeId) || 'Type'}: {typeProperties.typeName}
830
+ </p>
831
+ )}
811
832
  </div>
812
833
  <div className="flex gap-1 shrink-0">
813
834
  <Tooltip>
@@ -932,55 +953,70 @@ export function PropertiesPanel() {
932
953
  </div>
933
954
  )}
934
955
 
935
- {/* Entity Position - teal tint to distinguish from emerald storey bar */}
936
- {entityCoordinates && (
956
+ {/* World coordinates + Georeferencing single consolidated section */}
957
+ {(entityCoordinates || georef || editMode) && (
937
958
  <Collapsible open={coordOpen} onOpenChange={setCoordOpen}>
938
959
  <CollapsibleTrigger className="flex items-center gap-2 w-full text-xs border border-teal-500/30 px-2 py-1.5 text-teal-800 dark:text-teal-400 min-w-0 text-left group/coord">
939
960
  <Crosshair className="h-3.5 w-3.5 shrink-0" />
940
961
  <span className="font-bold uppercase tracking-wide shrink-0">World</span>
941
962
  {!coordOpen && (
942
963
  <>
943
- <span className="font-mono text-[10px] text-teal-600/70 dark:text-teal-500/70 truncate min-w-0 flex-1 tabular-nums">
944
- <CoordVal axis="E" value={entityCoordinates.worldZup.center.x} />{' '}
945
- <CoordVal axis="N" value={entityCoordinates.worldZup.center.y} />{' '}
946
- <CoordVal axis="Z" value={entityCoordinates.worldZup.center.z} />
947
- </span>
964
+ {entityCoordinates && (
965
+ <span className="font-mono text-[10px] text-teal-600/70 dark:text-teal-500/70 truncate min-w-0 flex-1 tabular-nums">
966
+ <CoordVal axis="E" value={entityCoordinates.worldZup.center.x} />{' '}
967
+ <CoordVal axis="N" value={entityCoordinates.worldZup.center.y} />{' '}
968
+ <CoordVal axis="Z" value={entityCoordinates.worldZup.center.z} />
969
+ </span>
970
+ )}
971
+ {georef?.projectedCRS?.name && (
972
+ <span className="font-mono text-[9px] text-teal-500/60 shrink-0">{georef.projectedCRS.name}</span>
973
+ )}
948
974
  <span className="text-[9px] text-teal-500/0 group-hover/coord:text-teal-500/40 transition-colors shrink-0">details</span>
949
975
  </>
950
976
  )}
951
977
  </CollapsibleTrigger>
952
978
  <CollapsibleContent>
953
- <div className="px-2 py-1.5 space-y-0.5">
954
- <CoordRow
955
- label=""
956
- values={[
957
- { axis: 'E', value: entityCoordinates.worldZup.center.x },
958
- { axis: 'N', value: entityCoordinates.worldZup.center.y },
959
- { axis: 'Z', value: entityCoordinates.worldZup.center.z },
960
- ]}
961
- primary
962
- copyLabel="world"
963
- coordCopied={coordCopied}
964
- onCopy={copyCoords}
965
- />
966
- <CoordRow
967
- label="Local"
968
- values={[
969
- { axis: 'X', value: entityCoordinates.local.center.x },
970
- { axis: 'Y', value: entityCoordinates.local.center.y },
971
- { axis: 'Z', value: entityCoordinates.local.center.z },
972
- ]}
973
- copyLabel="local"
974
- coordCopied={coordCopied}
975
- onCopy={copyCoords}
976
- />
977
- <div className="flex items-start gap-1.5">
978
- <span className="text-[9px] font-medium text-muted-foreground/50 uppercase tracking-wider w-[34px] shrink-0 pt-px">Size</span>
979
- <span className="font-mono text-[10px] text-muted-foreground/50 tabular-nums">
980
- {(entityCoordinates.local.max.x - entityCoordinates.local.min.x).toFixed(2)} x {(entityCoordinates.local.max.y - entityCoordinates.local.min.y).toFixed(2)} x {(entityCoordinates.local.max.z - entityCoordinates.local.min.z).toFixed(2)}
981
- </span>
979
+ {entityCoordinates && (
980
+ <div className="px-2 py-1.5 space-y-0.5">
981
+ <CoordRow
982
+ label=""
983
+ values={[
984
+ { axis: 'E', value: entityCoordinates.worldZup.center.x },
985
+ { axis: 'N', value: entityCoordinates.worldZup.center.y },
986
+ { axis: 'Z', value: entityCoordinates.worldZup.center.z },
987
+ ]}
988
+ primary
989
+ copyLabel="world"
990
+ coordCopied={coordCopied}
991
+ onCopy={copyCoords}
992
+ />
993
+ <CoordRow
994
+ label="Local"
995
+ values={[
996
+ { axis: 'X', value: entityCoordinates.local.center.x },
997
+ { axis: 'Y', value: entityCoordinates.local.center.y },
998
+ { axis: 'Z', value: entityCoordinates.local.center.z },
999
+ ]}
1000
+ copyLabel="local"
1001
+ coordCopied={coordCopied}
1002
+ onCopy={copyCoords}
1003
+ />
1004
+ <div className="flex items-start gap-1.5">
1005
+ <span className="text-[9px] font-medium text-muted-foreground/50 uppercase tracking-wider w-[34px] shrink-0 pt-px">Size</span>
1006
+ <span className="font-mono text-[10px] text-muted-foreground/50 tabular-nums">
1007
+ {(entityCoordinates.local.max.x - entityCoordinates.local.min.x).toFixed(2)} x {(entityCoordinates.local.max.y - entityCoordinates.local.min.y).toFixed(2)} x {(entityCoordinates.local.max.z - entityCoordinates.local.min.z).toFixed(2)}
1008
+ </span>
1009
+ </div>
982
1010
  </div>
983
- </div>
1011
+ )}
1012
+ <GeoreferencingPanel
1013
+ georef={georef}
1014
+ modelId={selectedEntity?.modelId === 'legacy' ? '__legacy__' : (model?.id ?? selectedEntity?.modelId)}
1015
+ enableEditing
1016
+ schemaVersion={activeDataStore?.schemaVersion}
1017
+ coordinateInfo={(model?.geometryResult ?? geometryResult)?.coordinateInfo}
1018
+ geometryResult={model?.geometryResult ?? geometryResult}
1019
+ />
984
1020
  </CollapsibleContent>
985
1021
  </Collapsible>
986
1022
  )}
@@ -1131,7 +1167,7 @@ export function PropertiesPanel() {
1131
1167
  )}
1132
1168
  <div className="flex items-center gap-2 px-1 pb-0.5 text-[11px] text-indigo-600/70 dark:text-indigo-400/60 uppercase tracking-wider font-semibold">
1133
1169
  <Building2 className="h-3 w-3 shrink-0" />
1134
- <span className="truncate">Inherited Type Properties</span>
1170
+ <span className="truncate">Type Properties ({typeProperties.typeName})</span>
1135
1171
  </div>
1136
1172
  {inheritedTypeProperties.map((pset: PropertySet) => (
1137
1173
  <PropertySetCard
@@ -22,6 +22,7 @@ import {
22
22
  DropdownMenuTrigger,
23
23
  } from '@/components/ui/dropdown-menu';
24
24
  import { useViewerStore } from '@/store';
25
+ import { toGlobalIdFromModels } from '@/store/globalId';
25
26
  import { useIfc } from '@/hooks/useIfc';
26
27
  import { GraphicOverrideEngine } from '@ifc-lite/drawing-2d';
27
28
  import { type GeometryResult } from '@ifc-lite/geometry';
@@ -224,7 +225,7 @@ export function Section2DPanel({
224
225
  const model = models.get(modelId);
225
226
  if (model && model.idOffset !== undefined) {
226
227
  for (const localId of localHiddenIds) {
227
- globalHiddenIds.add(localId + model.idOffset);
228
+ globalHiddenIds.add(toGlobalIdFromModels(models, model.id, localId));
228
229
  }
229
230
  }
230
231
  }
@@ -245,7 +246,7 @@ export function Section2DPanel({
245
246
  const model = models.get(modelId);
246
247
  if (model && model.idOffset !== undefined) {
247
248
  for (const localId of localIsolatedIds) {
248
- globalIsolatedIds.add(localId + model.idOffset);
249
+ globalIsolatedIds.add(toGlobalIdFromModels(models, model.id, localId));
249
250
  }
250
251
  }
251
252
  }
@@ -10,6 +10,7 @@ import { Section2DPanel } from './Section2DPanel';
10
10
  import { BasketPresentationDock } from './BasketPresentationDock';
11
11
  import { BCFOverlay } from './bcf/BCFOverlay';
12
12
  import { useViewerStore } from '@/store';
13
+ import { toGlobalIdFromModels } from '@/store/globalId';
13
14
  import { collectIfcBuildingStoreyElementsWithIfcSpace } from '@/store/basketVisibleSet';
14
15
  import { useIfc } from '@/hooks/useIfc';
15
16
  import { useWebGPU } from '@/hooks/useWebGPU';
@@ -265,14 +266,14 @@ export function ViewportContainer() {
265
266
  const hierarchy = model.ifcDataStore?.spatialHierarchy;
266
267
  if (!hierarchy) continue;
267
268
 
268
- const offset = model.idOffset ?? 0;
269
-
270
269
  for (const storeyId of selectedStoreys) {
271
- const localStoreyId = hierarchy.byStorey.has(storeyId) ? storeyId : storeyId - offset;
270
+ const localStoreyId = hierarchy.byStorey.has(storeyId)
271
+ ? storeyId
272
+ : storeyId - (model.idOffset ?? 0);
272
273
  const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, localStoreyId);
273
274
  if (storeyElementIds) {
274
275
  for (const originalExpressId of storeyElementIds) {
275
- combinedGlobalIds.add(originalExpressId + offset);
276
+ combinedGlobalIds.add(toGlobalIdFromModels(storeModels, model.id, originalExpressId));
276
277
  }
277
278
  }
278
279
  }
@@ -13,6 +13,7 @@ import {
13
13
  } from '@ifc-lite/data';
14
14
  import type { IfcDataStore } from '@ifc-lite/parser';
15
15
  import { useViewerStore, type FederatedModel } from '@/store';
16
+ import { toGlobalIdFromModels } from '@/store/globalId';
16
17
  import type { TreeNode, NodeType, StoreyData, UnifiedStorey } from './types';
17
18
 
18
19
  /** Helper to create elevation key (with 0.5m tolerance for matching) */
@@ -300,7 +301,7 @@ export function buildTreeData(
300
301
  nodes.push({
301
302
  id: storeyNodeId,
302
303
  expressIds: allStoreyIds,
303
- globalIds: unified.storeys.map((s) => s.storeyId + (models.get(s.modelId)?.idOffset ?? 0)),
304
+ globalIds: unified.storeys.map((s) => toGlobalIdFromModels(models, s.modelId, s.storeyId)),
304
305
  modelIds: unified.storeys.map(s => s.modelId),
305
306
  name: unified.name,
306
307
  type: 'unified-storey',