@ifc-lite/viewer 1.27.0 → 1.28.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.
Files changed (162) hide show
  1. package/.turbo/turbo-build.log +35 -42
  2. package/CHANGELOG.md +74 -0
  3. package/dist/assets/{basketViewActivator-B3CdrLsb.js → basketViewActivator-Ce38DhXd.js} +8 -8
  4. package/dist/assets/{bcf-QeHK_Aud.js → bcf-Cv_O3JfD.js} +56 -56
  5. package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
  6. package/dist/assets/{deflate-B-d0SYQM.js → deflate-HbyMq59o.js} +1 -1
  7. package/dist/assets/drawing-2d-DW98umlt.js +257 -0
  8. package/dist/assets/e57-source-2wI9jkCA.js +1 -0
  9. package/dist/assets/{exporters-B4LbZFeT.js → exporters-BuD3XRzB.js} +1309 -1153
  10. package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
  11. package/dist/assets/{geotiff-CrVtDRFq.js → geotiff-B2HA8Bwm.js} +10 -10
  12. package/dist/assets/{ids-DjsGFN10.js → ids-DYUFMd5f.js} +952 -945
  13. package/dist/assets/{ifc-lite_bg-DsYUIHm3.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
  14. package/dist/assets/index-E9wB0zWt.css +1 -0
  15. package/dist/assets/{index-COYokSKc.js → index-n5O1QJMM.js} +37877 -38126
  16. package/dist/assets/{index.es-CY202jA3.js → index.es-BKVIpZgL.js} +9 -9
  17. package/dist/assets/{jpeg-D4wOkf5h.js → jpeg-C7hjKjPX.js} +1 -1
  18. package/dist/assets/{jspdf.es.min-DIGb9BHN.js → jspdf.es.min-oWlFc42Y.js} +4 -4
  19. package/dist/assets/lens-C4p1kQ0p.js +1 -0
  20. package/dist/assets/{lerc-DmW0_tgf.js → lerc-BfIOGhQz.js} +1 -1
  21. package/dist/assets/{lzw-oWetY-d6.js → lzw-B0jRuuW5.js} +1 -1
  22. package/dist/assets/{native-bridge-BX8_tHXE.js → native-bridge-DpB-dtEn.js} +6 -3
  23. package/dist/assets/{packbits-F8Nkp4NY.js → packbits-DVvBTC39.js} +1 -1
  24. package/dist/assets/parser.worker-BDsWQ6rc.js +182 -0
  25. package/dist/assets/{pdf-Dsh3HPZB.js → pdf-dVIqI5ac.js} +10 -10
  26. package/dist/assets/raw-C0ZJYGmN.js +1 -0
  27. package/dist/assets/{sandbox-BAC3a-eN.js → sandbox-qpJlrNN0.js} +2962 -2554
  28. package/dist/assets/server-client-DVZ2huNS.js +719 -0
  29. package/dist/assets/{webimage-BLV1dgmd.js → webimage-B394g0Tw.js} +1 -1
  30. package/dist/assets/{xlsx-Bc2HTrjC.js → xlsx-D-oHO76J.js} +8 -8
  31. package/dist/assets/{zstd-C_1HxVrA.js → zstd-Bf38MwV2.js} +1 -1
  32. package/dist/index.html +9 -9
  33. package/package.json +24 -23
  34. package/src/App.tsx +1 -3
  35. package/src/components/mcp/playground-dispatcher.ts +3 -0
  36. package/src/components/mcp/playground-files.ts +33 -1
  37. package/src/components/viewer/BCFPanel.tsx +1 -16
  38. package/src/components/viewer/ChatPanel.tsx +11 -46
  39. package/src/components/viewer/CommandPalette.tsx +6 -1
  40. package/src/components/viewer/ComparePanel.tsx +420 -0
  41. package/src/components/viewer/HierarchyPanel.tsx +48 -183
  42. package/src/components/viewer/IDSPanel.tsx +1 -26
  43. package/src/components/viewer/MainToolbar.tsx +94 -187
  44. package/src/components/viewer/MobileToolbar.tsx +1 -9
  45. package/src/components/viewer/PropertiesPanel.tsx +98 -127
  46. package/src/components/viewer/ScriptPanel.tsx +8 -34
  47. package/src/components/viewer/Section2DPanel.tsx +32 -1
  48. package/src/components/viewer/ViewerLayout.tsx +5 -2
  49. package/src/components/viewer/Viewport.tsx +3 -0
  50. package/src/components/viewer/ViewportContainer.tsx +24 -42
  51. package/src/components/viewer/ViewportOverlays.tsx +1 -4
  52. package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
  53. package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
  54. package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
  55. package/src/components/viewer/hierarchy/types.ts +1 -0
  56. package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
  57. package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
  58. package/src/components/viewer/useGeometryStreaming.ts +0 -2
  59. package/src/hooks/federationLoadGate.test.ts +12 -2
  60. package/src/hooks/federationLoadGate.ts +9 -2
  61. package/src/hooks/ingest/federationAlign.ts +488 -0
  62. package/src/hooks/ingest/viewerModelIngest.ts +3 -212
  63. package/src/hooks/useCompare.ts +0 -0
  64. package/src/hooks/useCompareOverlay.ts +119 -0
  65. package/src/hooks/useDrawingGeneration.ts +234 -14
  66. package/src/hooks/useIfc.ts +1 -1
  67. package/src/hooks/useIfcCache.ts +100 -24
  68. package/src/hooks/useIfcFederation.ts +42 -811
  69. package/src/hooks/useIfcLoader.ts +349 -1517
  70. package/src/hooks/useIfcServer.ts +3 -0
  71. package/src/hooks/useLens.ts +5 -1
  72. package/src/hooks/useSymbolicAnnotations.ts +70 -38
  73. package/src/lib/compare/buildFingerprints.ts +173 -0
  74. package/src/lib/compare/describeChange.ts +0 -0
  75. package/src/lib/compare/geometricData.test.ts +54 -0
  76. package/src/lib/compare/geometricData.ts +37 -0
  77. package/src/lib/compare/overlay.test.ts +99 -0
  78. package/src/lib/compare/overlay.ts +91 -0
  79. package/src/lib/geo/cesium-placement.ts +1 -1
  80. package/src/lib/geo/reproject.ts +4 -1
  81. package/src/lib/llm/script-edit-ops.ts +23 -0
  82. package/src/lib/llm/stream-client.ts +8 -1
  83. package/src/lib/search/result-export.ts +7 -1
  84. package/src/sdk/adapters/export-adapter.ts +6 -1
  85. package/src/services/cacheService.ts +9 -25
  86. package/src/services/desktop-export.ts +2 -59
  87. package/src/services/file-dialog.ts +8 -142
  88. package/src/store/constants.ts +23 -0
  89. package/src/store/globalId.ts +15 -13
  90. package/src/store/index.ts +19 -6
  91. package/src/store/slices/cesiumSlice.ts +8 -1
  92. package/src/store/slices/compareSlice.ts +96 -0
  93. package/src/store/slices/drawing2DSlice.ts +8 -0
  94. package/src/store/slices/lensSlice.ts +8 -0
  95. package/src/store/slices/visibilitySlice.ts +22 -1
  96. package/src/store/types.ts +1 -71
  97. package/src/utils/acquireFileBuffer.test.ts +12 -4
  98. package/src/utils/ifcConfig.ts +0 -12
  99. package/src/utils/loadingUtils.ts +32 -0
  100. package/src/utils/spatialHierarchy.test.ts +53 -1
  101. package/src/utils/spatialHierarchy.ts +42 -2
  102. package/src/vite-env.d.ts +2 -0
  103. package/vite.config.ts +6 -3
  104. package/DESKTOP_CONTRACT_VERSION +0 -1
  105. package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
  106. package/dist/assets/e57-source-CQHxE8n3.js +0 -1
  107. package/dist/assets/event-B0kAzHa-.js +0 -1
  108. package/dist/assets/geometry.worker-BdH-E6NB.js +0 -1
  109. package/dist/assets/index-ajK6D32J.css +0 -1
  110. package/dist/assets/lens-PYsLu_MA.js +0 -1
  111. package/dist/assets/parser.worker-D591Zu_-.js +0 -182
  112. package/dist/assets/raw-D9iw0tmc.js +0 -1
  113. package/dist/assets/server-client-Cjwnm7il.js +0 -706
  114. package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
  115. package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
  116. package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
  117. package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
  118. package/src/components/viewer/SettingsPage.tsx +0 -581
  119. package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
  120. package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
  121. package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
  122. package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
  123. package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
  124. package/src/lib/desktop-entitlement.ts +0 -43
  125. package/src/lib/desktop-product.ts +0 -130
  126. package/src/lib/platform.ts +0 -23
  127. package/src/services/desktop-cache.ts +0 -186
  128. package/src/services/desktop-harness.ts +0 -196
  129. package/src/services/desktop-logger.ts +0 -20
  130. package/src/services/desktop-native-metadata.ts +0 -230
  131. package/src/services/desktop-panel-actions.ts +0 -43
  132. package/src/services/desktop-preferences.ts +0 -44
  133. package/src/services/fs-cache.ts +0 -212
  134. package/src/services/tauri-core-stub.ts +0 -7
  135. package/src/services/tauri-dialog-stub.ts +0 -7
  136. package/src/services/tauri-fs-stub.ts +0 -7
  137. package/src/services/tauri-modules.d.ts +0 -50
  138. package/src/store/slices/desktopEntitlementSlice.ts +0 -86
  139. package/src/utils/desktopModelSnapshot.ts +0 -358
  140. package/src/utils/nativeSpatialDataStore.ts +0 -277
  141. package/src-tauri/Cargo.toml +0 -29
  142. package/src-tauri/build.rs +0 -7
  143. package/src-tauri/capabilities/default.json +0 -18
  144. package/src-tauri/icons/128x128.png +0 -0
  145. package/src-tauri/icons/128x128@2x.png +0 -0
  146. package/src-tauri/icons/32x32.png +0 -0
  147. package/src-tauri/icons/Square107x107Logo.png +0 -0
  148. package/src-tauri/icons/Square142x142Logo.png +0 -0
  149. package/src-tauri/icons/Square150x150Logo.png +0 -0
  150. package/src-tauri/icons/Square284x284Logo.png +0 -0
  151. package/src-tauri/icons/Square30x30Logo.png +0 -0
  152. package/src-tauri/icons/Square310x310Logo.png +0 -0
  153. package/src-tauri/icons/Square44x44Logo.png +0 -0
  154. package/src-tauri/icons/Square71x71Logo.png +0 -0
  155. package/src-tauri/icons/Square89x89Logo.png +0 -0
  156. package/src-tauri/icons/StoreLogo.png +0 -0
  157. package/src-tauri/icons/icon.icns +0 -0
  158. package/src-tauri/icons/icon.ico +0 -0
  159. package/src-tauri/icons/icon.png +0 -0
  160. package/src-tauri/src/lib.rs +0 -21
  161. package/src-tauri/src/main.rs +0 -10
  162. package/src-tauri/tauri.conf.json +0 -39
@@ -32,11 +32,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
32
32
  import { useViewerStore } from '@/store';
33
33
  import { toGlobalIdFromModels } from '@/store/globalId';
34
34
  import { useIfc } from '@/hooks/useIfc';
35
- import { getNativeEntityDetails } from '@/services/desktop-native-metadata';
36
35
  import { configureMutationView } from '@/utils/configureMutationView';
37
36
  import { IfcQuery } from '@ifc-lite/query';
38
37
  import { MutablePropertyView } from '@ifc-lite/mutations';
39
- import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, getAttributeNames, type IfcDataStore } from '@ifc-lite/parser';
38
+ import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractMaterialPropertiesOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, getAttributeNames, type IfcDataStore, type MaterialPsetGroup } from '@ifc-lite/parser';
40
39
  import type { NewEntity } from '@ifc-lite/mutations';
41
40
  import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
42
41
  import type { EntityRef, FederatedModel } from '@/store/types';
@@ -47,6 +46,7 @@ import { QuantitySetCard } from './properties/QuantitySetCard';
47
46
  import { ModelMetadataPanel } from './properties/ModelMetadataPanel';
48
47
  import { ClassificationCard } from './properties/ClassificationCard';
49
48
  import { MaterialCard } from './properties/MaterialCard';
49
+ import { MaterialTotalsPanel } from './properties/MaterialTotalsPanel';
50
50
  import { ScheduleCard } from './properties/ScheduleCard';
51
51
  import { TaskEditCard } from './properties/TaskEditCard';
52
52
  import { DocumentCard } from './properties/DocumentCard';
@@ -56,6 +56,17 @@ import { BsddCard } from './properties/BsddCard';
56
56
  import { GeoreferencingPanel } from './properties/GeoreferencingPanel';
57
57
  import { RawStepCard } from './properties/RawStepCard';
58
58
 
59
+ /** IFC material *definition* classes selectable from the Materials tab. */
60
+ const MATERIAL_DEF_TYPES = new Set([
61
+ 'IFCMATERIAL',
62
+ 'IFCMATERIALLAYERSET',
63
+ 'IFCMATERIALLAYERSETUSAGE',
64
+ 'IFCMATERIALPROFILESET',
65
+ 'IFCMATERIALPROFILESETUSAGE',
66
+ 'IFCMATERIALCONSTITUENTSET',
67
+ 'IFCMATERIALLIST',
68
+ ]);
69
+
59
70
  type DisplayProperty = { name: string; value: unknown; isMutated: boolean };
60
71
  type DisplayPropertySet = {
61
72
  name: string;
@@ -150,7 +161,7 @@ export function PropertiesPanel() {
150
161
  const m = models.get(selectedEntity.modelId);
151
162
  if (m) {
152
163
  return {
153
- modelQuery: m.nativeMetadata ? null : (m.ifcDataStore ? new IfcQuery(m.ifcDataStore) : null),
164
+ modelQuery: m.ifcDataStore ? new IfcQuery(m.ifcDataStore) : null,
154
165
  model: m,
155
166
  };
156
167
  }
@@ -189,8 +200,6 @@ export function PropertiesPanel() {
189
200
  const [copied, setCopied] = useState(false);
190
201
  const [coordCopied, setCoordCopied] = useState<string | null>(null);
191
202
  const [coordOpen, setCoordOpen] = useState(false);
192
- const [nativeDetails, setNativeDetails] = useState<import('@/store/types').NativeMetadataEntityDetails | null>(null);
193
- const [nativeDetailsState, setNativeDetailsState] = useState<'idle' | 'loading' | 'error'>('idle');
194
203
 
195
204
  // Inline property editing is gated by the global edit-mode pill in
196
205
  // the main toolbar (see `uiSlice.editEnabled`). Reading it from the
@@ -199,32 +208,6 @@ export function PropertiesPanel() {
199
208
  // tools — behind a single switch.
200
209
  const editMode = useViewerStore((s) => s.editEnabled);
201
210
 
202
- useEffect(() => {
203
- if (!selectedEntity || !model?.nativeMetadata) {
204
- setNativeDetails(null);
205
- setNativeDetailsState('idle');
206
- return;
207
- }
208
- let cancelled = false;
209
- setNativeDetailsState('loading');
210
- void getNativeEntityDetails(model.nativeMetadata.cacheKey, selectedEntity.expressId)
211
- .then((details) => {
212
- if (!cancelled) {
213
- setNativeDetails(details);
214
- setNativeDetailsState('idle');
215
- }
216
- })
217
- .catch(() => {
218
- if (!cancelled) {
219
- setNativeDetails(null);
220
- setNativeDetailsState('error');
221
- }
222
- });
223
- return () => {
224
- cancelled = true;
225
- };
226
- }, [selectedEntity, model?.nativeMetadata]);
227
-
228
211
  const copyToClipboard = useCallback((text: string) => {
229
212
  navigator.clipboard.writeText(text);
230
213
  setCopied(true);
@@ -450,6 +433,16 @@ export function PropertiesPanel() {
450
433
  return typeName.endsWith('Type');
451
434
  }, [selectedEntity, model, ifcDataStore]);
452
435
 
436
+ // Detect a material definition selected from the "Materials" hierarchy tab.
437
+ // Materials aren't products, so the EntityTable's getTypeName doesn't cover
438
+ // them — read the raw class from the entity index instead.
439
+ const selectedMaterialId = useMemo(() => {
440
+ if (!selectedEntity) return null;
441
+ const dataStore = model?.ifcDataStore ?? ifcDataStore;
442
+ const rawType = (dataStore as IfcDataStore | null)?.entityIndex?.byId?.get(selectedEntity.expressId)?.type;
443
+ return rawType && MATERIAL_DEF_TYPES.has(rawType.toUpperCase()) ? selectedEntity.expressId : null;
444
+ }, [selectedEntity, model, ifcDataStore]);
445
+
453
446
  // Unified property/quantity access - EntityNode handles on-demand extraction automatically
454
447
  // These hooks must be called before any early return to maintain hook order
455
448
  // Use MutablePropertyView as primary source when available (it handles base + mutations)
@@ -627,6 +620,17 @@ export function PropertiesPanel() {
627
620
  return extractMaterialsOnDemand(dataStore as IfcDataStore, lookupExpressId);
628
621
  }, [selectedEntity, lookupExpressId, model, ifcDataStore]);
629
622
 
623
+ // Property sets attached to the selected entity's material(s) via
624
+ // IfcMaterialProperties (e.g. Pset_MaterialConcrete). These live on the
625
+ // IfcMaterial — not on the object — so they never surface through the
626
+ // occurrence/type pset paths; resolve them through the material association.
627
+ const materialProperties: MaterialPsetGroup[] = useMemo(() => {
628
+ if (!selectedEntity || lookupExpressId === null) return [];
629
+ const dataStore = model?.ifcDataStore ?? ifcDataStore;
630
+ if (!dataStore) return [];
631
+ return extractMaterialPropertiesOnDemand(dataStore as IfcDataStore, lookupExpressId);
632
+ }, [selectedEntity, lookupExpressId, model, ifcDataStore]);
633
+
630
634
  // Extract documents for the selected entity from the IFC data store
631
635
  const documents = useMemo(() => {
632
636
  if (!selectedEntity || lookupExpressId === null) return [];
@@ -925,51 +929,6 @@ export function PropertiesPanel() {
925
929
  return names;
926
930
  }, [attributes]);
927
931
 
928
- const isNativeLazySelection = Boolean(selectedEntity && model?.nativeMetadata);
929
-
930
- // Native-lazy entities (server-streamed without full STEP data) can
931
- // never be edited; the per-row editors below self-guard via
932
- // `enableEditing={editMode && !isNativeLazySelection}`. The old
933
- // panel-local `editMode` would flip itself off when a native-lazy
934
- // entity was selected; now that the flag is global it stays on, and
935
- // the field-level guards do the work.
936
-
937
- const nativeSpatialInfo = useMemo(() => {
938
- if (!nativeDetails?.spatial?.storeyName) return null;
939
- return {
940
- storeyId: nativeDetails.spatial.storeyId ?? undefined,
941
- storeyName: nativeDetails.spatial.storeyName,
942
- elevation: nativeDetails.spatial.elevation ?? undefined,
943
- height: nativeDetails.spatial.height ?? undefined,
944
- };
945
- }, [nativeDetails]);
946
-
947
- const nativeOccurrenceProperties = useMemo<PropertySet[]>(() => {
948
- if (!nativeDetails) return [];
949
- return nativeDetails.properties.map((pset) => ({
950
- name: pset.name,
951
- properties: pset.properties.map((property) => ({
952
- name: property.name,
953
- value: property.value,
954
- isMutated: false,
955
- })),
956
- isNewPset: false,
957
- source: 'instance' as const,
958
- }));
959
- }, [nativeDetails]);
960
-
961
- const nativeQuantities = useMemo<QuantitySet[]>(() => {
962
- if (!nativeDetails) return [];
963
- return nativeDetails.quantities.map((qset) => ({
964
- name: qset.name,
965
- quantities: qset.quantities.map((quantity) => ({
966
- name: quantity.name,
967
- value: quantity.value,
968
- type: quantity.type ?? 0,
969
- })),
970
- }));
971
- }, [nativeDetails]);
972
-
973
932
  // Overlay (authored) entities — split halves, duplicates, scripted
974
933
  // adds — live only in the StoreEditor overlay, NOT the parsed store.
975
934
  // `modelQuery.entity()` always returns a node, and its getters fall
@@ -977,48 +936,27 @@ export function PropertiesPanel() {
977
936
  // table (entity-table.ts#getTypeName). Those non-null sentinels would
978
937
  // shadow the overlay record in an `entityNode ?? overlay` chain, so
979
938
  // when an overlay record exists it MUST take precedence.
980
- const renderedEntityType = isNativeLazySelection
981
- ? (nativeDetails?.summary.type ?? 'Loading...')
982
- : (overlayEntity?.type ?? entityNode?.type ?? 'Unknown');
983
- const renderedEntityName = isNativeLazySelection
984
- ? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
985
- : (overlayAttr(2) ?? entityNode?.name ?? undefined);
986
- const renderedEntityGlobalId = isNativeLazySelection
987
- ? (nativeDetails?.summary.globalId ?? null)
988
- : (overlayAttr(0) ?? entityNode?.globalId);
989
- const renderedEntityDescription = isNativeLazySelection
990
- ? undefined
991
- : (overlayAttr(3) ?? entityNode?.description ?? undefined);
992
- const renderedEntityObjectType = isNativeLazySelection
993
- ? undefined
994
- : (overlayAttr(4) ?? entityNode?.objectType ?? undefined);
995
- const renderedSpatialInfo = isNativeLazySelection ? nativeSpatialInfo : spatialInfo;
996
- const renderedOccurrenceProperties = isNativeLazySelection ? nativeOccurrenceProperties : occurrenceProperties;
997
- const renderedInheritedTypeProperties = isNativeLazySelection ? [] : inheritedTypeProperties;
998
- const renderedMergedProperties = isNativeLazySelection
999
- ? nativeOccurrenceProperties
1000
- : mergedProperties;
1001
- const renderedQuantities = isNativeLazySelection ? nativeQuantities : quantities;
1002
- const renderedAttributes = isNativeLazySelection ? [] : attributes;
1003
- const renderedClassifications = isNativeLazySelection ? [] : classifications;
1004
- const renderedMaterialInfo = isNativeLazySelection ? null : materialInfo;
1005
- const renderedDocuments = isNativeLazySelection ? [] : documents;
1006
- const renderedEntityRelationships = isNativeLazySelection ? null : entityRelationships;
1007
- const renderedGeoref = isNativeLazySelection ? null : georef;
1008
- const renderedSpatialContainment = isNativeLazySelection ? null : spatialContainment;
1009
- const renderedTypeProperties = isNativeLazySelection
1010
- ? (nativeDetails?.typeSummary
1011
- ? {
1012
- typeName: nativeDetails.typeSummary.name,
1013
- typeId: nativeDetails.typeSummary.expressId,
1014
- psets: [] as PropertySet[],
1015
- }
1016
- : null)
1017
- : typeProperties;
1018
- const renderedTypeEditImpact = isNativeLazySelection ? null : typeEditImpact;
1019
- const renderedIsTypeEntity = isNativeLazySelection
1020
- ? ((nativeDetails?.summary.type ?? '').endsWith('Type'))
1021
- : isTypeEntity;
939
+ const renderedEntityType = overlayEntity?.type ?? entityNode?.type ?? 'Unknown';
940
+ const renderedEntityName = overlayAttr(2) ?? entityNode?.name ?? undefined;
941
+ const renderedEntityGlobalId = overlayAttr(0) ?? entityNode?.globalId;
942
+ const renderedEntityDescription = overlayAttr(3) ?? entityNode?.description ?? undefined;
943
+ const renderedEntityObjectType = overlayAttr(4) ?? entityNode?.objectType ?? undefined;
944
+ const renderedSpatialInfo = spatialInfo;
945
+ const renderedOccurrenceProperties = occurrenceProperties;
946
+ const renderedInheritedTypeProperties = inheritedTypeProperties;
947
+ const renderedMergedProperties = mergedProperties;
948
+ const renderedQuantities = quantities;
949
+ const renderedAttributes = attributes;
950
+ const renderedClassifications = classifications;
951
+ const renderedMaterialInfo = materialInfo;
952
+ const renderedMaterialProperties = materialProperties;
953
+ const renderedDocuments = documents;
954
+ const renderedEntityRelationships = entityRelationships;
955
+ const renderedGeoref = georef;
956
+ const renderedSpatialContainment = spatialContainment;
957
+ const renderedTypeProperties = typeProperties;
958
+ const renderedTypeEditImpact = typeEditImpact;
959
+ const renderedIsTypeEntity = isTypeEntity;
1022
960
  const renderedExistingProps = useMemo(() => {
1023
961
  const keys = new Set<string>();
1024
962
  for (const pset of renderedMergedProperties) {
@@ -1053,6 +991,12 @@ export function PropertiesPanel() {
1053
991
  }
1054
992
  }
1055
993
 
994
+ // Material selected from the "Materials" hierarchy tab — show the material's
995
+ // own property sets plus quantities aggregated across all using elements.
996
+ if (selectedMaterialId !== null && selectedEntity) {
997
+ return <MaterialTotalsPanel materialId={selectedMaterialId} modelId={selectedEntity.modelId} />;
998
+ }
999
+
1056
1000
  // Multi-entity selection (unified storeys) - render combined view
1057
1001
  if (selectedEntities.length > 1) {
1058
1002
  return (
@@ -1069,7 +1013,7 @@ export function PropertiesPanel() {
1069
1013
  // `overlayEntity` when `entityNode` is empty. Without including
1070
1014
  // `overlayEntity` here the panel collapses to the model-metadata
1071
1015
  // view the moment a fresh add lands.
1072
- if (!selectedEntityId || (!isNativeLazySelection && (!modelQuery || (!entityNode && !overlayEntity)))) {
1016
+ if (!selectedEntityId || !modelQuery || (!entityNode && !overlayEntity)) {
1073
1017
  // Show model metadata when a single model is loaded and nothing selected.
1074
1018
  // Handles both federated models (models.size >= 1) and legacy single-model path (models.size === 0).
1075
1019
  if (models.size === 1) {
@@ -1461,7 +1405,7 @@ export function PropertiesPanel() {
1461
1405
  </div>
1462
1406
  )}
1463
1407
  {/* Edit toolbar - only shown when edit mode is active */}
1464
- {editMode && selectedEntity && !isNativeLazySelection && (
1408
+ {editMode && selectedEntity && (
1465
1409
  <>
1466
1410
  <GeometryEditCard
1467
1411
  modelId={selectedEntity.modelId}
@@ -1481,6 +1425,7 @@ export function PropertiesPanel() {
1481
1425
  {renderedMergedProperties.length === 0
1482
1426
  && renderedClassifications.length === 0
1483
1427
  && !renderedMaterialInfo
1428
+ && renderedMaterialProperties.length === 0
1484
1429
  && renderedDocuments.length === 0
1485
1430
  && !renderedEntityRelationships
1486
1431
  && !hasScheduleForSelection ? (
@@ -1506,7 +1451,7 @@ export function PropertiesPanel() {
1506
1451
  pset={pset}
1507
1452
  modelId={selectedEntity?.modelId}
1508
1453
  entityId={selectedEntity?.expressId}
1509
- enableEditing={editMode && !isNativeLazySelection}
1454
+ enableEditing={editMode}
1510
1455
  isTypeProperty={renderedIsTypeEntity}
1511
1456
  typeEditScope={renderedIsTypeEntity ? renderedTypeEditImpact ?? undefined : undefined}
1512
1457
  />
@@ -1530,7 +1475,7 @@ export function PropertiesPanel() {
1530
1475
  pset={pset}
1531
1476
  modelId={selectedEntity?.modelId}
1532
1477
  entityId={renderedTypeProperties.typeId}
1533
- enableEditing={editMode && !isNativeLazySelection}
1478
+ enableEditing={editMode}
1534
1479
  isTypeProperty
1535
1480
  typeEditScope={renderedTypeEditImpact?.mode === 'inherited' ? renderedTypeEditImpact : undefined}
1536
1481
  />
@@ -1560,10 +1505,38 @@ export function PropertiesPanel() {
1560
1505
  </>
1561
1506
  )}
1562
1507
 
1508
+ {/* Material Property Sets (Pset_Material* attached to the
1509
+ IfcMaterial via IfcMaterialProperties). Grouped per material,
1510
+ mirroring the Type Properties block. */}
1511
+ {renderedMaterialProperties.length > 0 && (
1512
+ <>
1513
+ {(renderedMergedProperties.length > 0 || renderedClassifications.length > 0 || renderedMaterialInfo) && (
1514
+ <div className="border-t border-amber-200 dark:border-amber-800/50 pt-2 mt-2" />
1515
+ )}
1516
+ {renderedMaterialProperties.map((group) => (
1517
+ <div key={`matpset-${group.materialId}`} className="space-y-3">
1518
+ <div className="flex items-center gap-2 px-1 pb-0.5 text-[11px] text-amber-600/70 dark:text-amber-400/60 uppercase tracking-wider font-semibold">
1519
+ <Layers className="h-3 w-3 shrink-0" />
1520
+ <span className="truncate">Material Properties ({group.materialName})</span>
1521
+ </div>
1522
+ {group.psets.map((pset) => (
1523
+ <PropertySetCard
1524
+ key={`matpset-${group.materialId}-${pset.name}`}
1525
+ pset={{
1526
+ name: pset.name,
1527
+ properties: pset.properties.map((p) => ({ name: p.name, value: p.value, isMutated: false })),
1528
+ }}
1529
+ />
1530
+ ))}
1531
+ </div>
1532
+ ))}
1533
+ </>
1534
+ )}
1535
+
1563
1536
  {/* Documents */}
1564
1537
  {renderedDocuments.length > 0 && (
1565
1538
  <>
1566
- {(renderedMergedProperties.length > 0 || renderedClassifications.length > 0 || renderedMaterialInfo) && (
1539
+ {(renderedMergedProperties.length > 0 || renderedClassifications.length > 0 || renderedMaterialInfo || renderedMaterialProperties.length > 0) && (
1567
1540
  <div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
1568
1541
  )}
1569
1542
  {renderedDocuments.map((doc, i) => (
@@ -1626,7 +1599,7 @@ export function PropertiesPanel() {
1626
1599
  </TabsContent>
1627
1600
 
1628
1601
  <TabsContent value="raw-step" className="m-0 p-3 overflow-hidden">
1629
- {selectedEntity && !isNativeLazySelection ? (
1602
+ {selectedEntity ? (
1630
1603
  <RawStepCard
1631
1604
  modelId={selectedEntity.modelId === 'legacy' ? '__legacy__' : selectedEntity.modelId}
1632
1605
  entityId={selectedEntity.expressId}
@@ -1636,9 +1609,7 @@ export function PropertiesPanel() {
1636
1609
  />
1637
1610
  ) : (
1638
1611
  <p className="text-sm text-zinc-500 dark:text-zinc-500 text-center py-8 font-mono">
1639
- {isNativeLazySelection
1640
- ? 'Raw STEP is not available for native-metadata selections'
1641
- : 'Select an entity to inspect raw STEP arguments'}
1612
+ Select an entity to inspect raw STEP arguments
1642
1613
  </p>
1643
1614
  )}
1644
1615
  </TabsContent>
@@ -49,13 +49,10 @@ import {
49
49
  DialogDescription,
50
50
  } from '@/components/ui/dialog';
51
51
  import { ScrollArea } from '@/components/ui/scroll-area';
52
- import { toast } from '@/components/ui/toast';
53
- import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess } from '@/lib/desktop-product';
54
52
  import { cn, formatDuration } from '@/lib/utils';
55
53
  import { useViewerStore } from '@/store';
56
54
  import { useSandbox } from '@/hooks/useSandbox';
57
55
  import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
58
- import { navigateToPath } from '@/services/app-navigation';
59
56
  import { CodeEditor } from './CodeEditor';
60
57
  import { ChatPanel } from './ChatPanel';
61
58
  import { PromoteToolDialog } from '@/components/extensions/PromoteToolDialog';
@@ -154,8 +151,6 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
154
151
  const [outputCollapsed, setOutputCollapsed] = useState(false);
155
152
  const chatPanelVisible = useViewerStore((s) => s.chatPanelVisible);
156
153
  const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible);
157
- const desktopEntitlement = useViewerStore((s) => s.desktopEntitlement);
158
- const canUseAiAssistant = hasDesktopFeatureAccess(desktopEntitlement, 'ai_assistant');
159
154
 
160
155
  // Chat panel width (px) — resizable via drag handle
161
156
  const [chatWidth, setChatWidth] = useState(380);
@@ -165,27 +160,14 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
165
160
  // Open chat by default when script panel mounts
166
161
  useEffect(() => {
167
162
  try {
168
- if (canUseAiAssistant && localStorage.getItem('ifc-lite-chat-panel-visible') === null) {
163
+ if (localStorage.getItem('ifc-lite-chat-panel-visible') === null) {
169
164
  setChatPanelVisible(true);
170
165
  }
171
166
  } catch {
172
- if (canUseAiAssistant) {
173
- setChatPanelVisible(true);
174
- }
167
+ setChatPanelVisible(true);
175
168
  }
176
169
  return () => { cleanupChatDragRef.current?.(); };
177
- }, [canUseAiAssistant, setChatPanelVisible]);
178
-
179
- useEffect(() => {
180
- if (!canUseAiAssistant && chatPanelVisible) {
181
- setChatPanelVisible(false);
182
- }
183
- }, [canUseAiAssistant, chatPanelVisible, setChatPanelVisible]);
184
-
185
- const promptAiUpgrade = useCallback(() => {
186
- toast.info('AI assistant is available with Desktop Pro');
187
- navigateToPath(buildDesktopUpgradeUrl());
188
- }, []);
170
+ }, [setChatPanelVisible]);
189
171
 
190
172
  const handleChatResizeStart = useCallback((e: React.MouseEvent) => {
191
173
  e.preventDefault();
@@ -259,10 +241,6 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
259
241
 
260
242
  const handleFixWithLlm = useCallback(() => {
261
243
  if (!lastError) return;
262
- if (!canUseAiAssistant) {
263
- promptAiUpgrade();
264
- return;
265
- }
266
244
  setChatPanelVisible(true);
267
245
  const state = useViewerStore.getState();
268
246
  queueChatRepairRequest({
@@ -270,15 +248,11 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
270
248
  diagnostics: state.scriptLastDiagnostics,
271
249
  reason: lastError.startsWith('Preflight validation failed:') ? 'preflight' : 'runtime',
272
250
  });
273
- }, [canUseAiAssistant, lastError, promptAiUpgrade, queueChatRepairRequest, setChatPanelVisible]);
251
+ }, [lastError, queueChatRepairRequest, setChatPanelVisible]);
274
252
 
275
253
  const toggleChat = useCallback(() => {
276
- if (!canUseAiAssistant) {
277
- promptAiUpgrade();
278
- return;
279
- }
280
254
  setChatPanelVisible(!chatPanelVisible);
281
- }, [canUseAiAssistant, chatPanelVisible, promptAiUpgrade, setChatPanelVisible]);
255
+ }, [chatPanelVisible, setChatPanelVisible]);
282
256
 
283
257
  return (
284
258
  <div className="h-full flex bg-background">
@@ -333,12 +307,12 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
333
307
  variant={chatPanelVisible ? 'default' : 'ghost'}
334
308
  size="icon-xs"
335
309
  onClick={toggleChat}
336
- className={cn(canUseAiAssistant && chatPanelVisible && 'bg-blue-500 hover:bg-blue-600 text-white')}
310
+ className={cn(chatPanelVisible && 'bg-blue-500 hover:bg-blue-600 text-white')}
337
311
  >
338
312
  <Bot className="h-3.5 w-3.5" />
339
313
  </Button>
340
314
  </TooltipTrigger>
341
- <TooltipContent>{canUseAiAssistant ? (chatPanelVisible ? 'Hide AI Chat' : 'Show AI Chat') : 'Desktop Pro required for AI Chat'}</TooltipContent>
315
+ <TooltipContent>{chatPanelVisible ? 'Hide AI Chat' : 'Show AI Chat'}</TooltipContent>
342
316
  </Tooltip>
343
317
 
344
318
  {onClose && (
@@ -578,7 +552,7 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
578
552
  className="h-6 px-2 text-xs border-destructive/40 text-destructive bg-transparent hover:bg-destructive/10"
579
553
  onClick={handleFixWithLlm}
580
554
  >
581
- {canUseAiAssistant ? 'Fix with LLM' : 'Upgrade for AI Fix'}
555
+ Fix with LLM
582
556
  </Button>
583
557
  </div>
584
558
  </div>
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';
15
- import { X, Download, Eye, EyeOff, Maximize2, ZoomIn, ZoomOut, Loader2, Printer, GripVertical, MoreHorizontal, RefreshCw, Pin, PinOff, Palette, Ruler, Trash2, FileText, Shapes, Box, PenTool, Hexagon, Type, Cloud, MousePointer2, Tag } from 'lucide-react';
15
+ import { X, Download, Eye, EyeOff, Maximize2, ZoomIn, ZoomOut, Loader2, Printer, GripVertical, MoreHorizontal, RefreshCw, Pin, PinOff, Palette, Ruler, Trash2, FileText, Shapes, Box, BoxSelect, PenTool, Hexagon, Type, Cloud, MousePointer2, Tag } from 'lucide-react';
16
16
  import { Button } from '@/components/ui/button';
17
17
  import {
18
18
  DropdownMenu,
@@ -321,6 +321,15 @@ export function Section2DPanel({
321
321
  updateDisplayOptions({ showIfcAnnotations: !displayOptions.showIfcAnnotations });
322
322
  }, [displayOptions.showIfcAnnotations, updateDisplayOptions]);
323
323
 
324
+ // Construction projection (issue #979): toggling changes which geometry the
325
+ // generator emits, so clear the current drawing to force a regenerate —
326
+ // same pattern as the symbolic/section-cut toggle.
327
+ const toggleConstructionProjection = useCallback(() => {
328
+ updateDisplayOptions({ showConstructionProjection: !displayOptions.showConstructionProjection });
329
+ setDrawing(null);
330
+ setDrawingStatus('idle');
331
+ }, [displayOptions.showConstructionProjection, updateDisplayOptions, setDrawing, setDrawingStatus]);
332
+
324
333
  const annotationHandlers = useAnnotation2D({
325
334
  drawing, viewTransform, sectionAxis: sectionPlane.axis, containerRef,
326
335
  activeTool: annotation2DActiveTool, setActiveTool: setAnnotation2DActiveTool,
@@ -589,6 +598,21 @@ export function Section2DPanel({
589
598
  <Tag className="h-4 w-4" />
590
599
  </Button>
591
600
 
601
+ {/* Construction projection toggle (issue #979) — plan only */}
602
+ <Button
603
+ variant={displayOptions.showConstructionProjection ? 'default' : 'ghost'}
604
+ size="icon-sm"
605
+ onClick={toggleConstructionProjection}
606
+ title={
607
+ displayOptions.showConstructionProjection
608
+ ? 'Hide construction projection (overhead & visible reference lines)'
609
+ : 'Show construction projection (overhead & visible reference lines)'
610
+ }
611
+ disabled={sectionPlane.axis !== 'down' || sectionPlane.custom !== undefined}
612
+ >
613
+ <BoxSelect className="h-4 w-4" />
614
+ </Button>
615
+
592
616
  {/* Annotation Tools Dropdown */}
593
617
  <DropdownMenu>
594
618
  <DropdownMenuTrigger asChild>
@@ -760,6 +784,13 @@ export function Section2DPanel({
760
784
  <Tag className="h-4 w-4 mr-2" />
761
785
  IFC Annotations {displayOptions.showIfcAnnotations ? 'On' : 'Off'}
762
786
  </DropdownMenuItem>
787
+ <DropdownMenuItem
788
+ onClick={toggleConstructionProjection}
789
+ disabled={sectionPlane.axis !== 'down' || sectionPlane.custom !== undefined}
790
+ >
791
+ <BoxSelect className="h-4 w-4 mr-2" />
792
+ Construction Projection {displayOptions.showConstructionProjection ? 'On' : 'Off'}
793
+ </DropdownMenuItem>
763
794
  <DropdownMenuItem onClick={() => setAnnotation2DActiveTool('none')}>
764
795
  <MousePointer2 className="h-4 w-4 mr-2" />
765
796
  Select / Pan {annotation2DActiveTool === 'none' ? '(On)' : ''}
@@ -29,13 +29,13 @@ import { BCFPanel } from './BCFPanel';
29
29
  import { IDSPanel } from './IDSPanel';
30
30
  import { LensPanel } from './LensPanel';
31
31
  import { ClashPanel } from './ClashPanel';
32
+ import { ComparePanel } from './ComparePanel';
32
33
  import { ListPanel } from './lists/ListPanel';
33
34
  import { ScriptPanel } from './ScriptPanel';
34
35
  import { GanttPanel } from './schedule/GanttPanel';
35
36
  import { ExtensionsPanel } from '@/components/extensions/ExtensionsPanel';
36
37
  import { CommandPalette } from './CommandPalette';
37
38
  import { SearchModal } from './SearchModal';
38
- import { DesktopEntitlementBanner } from './DesktopEntitlementBanner';
39
39
  import {
40
40
  closeActiveAnalysisExtension,
41
41
  getAnalysisExtensionById,
@@ -135,6 +135,8 @@ export function ViewerLayout() {
135
135
  const setLensPanelVisible = useViewerStore((s) => s.setLensPanelVisible);
136
136
  const clashPanelVisible = useViewerStore((s) => s.clashPanelVisible);
137
137
  const setClashPanelVisible = useViewerStore((s) => s.setClashPanelVisible);
138
+ const comparePanelVisible = useViewerStore((s) => s.comparePanelVisible);
139
+ const setComparePanelVisible = useViewerStore((s) => s.setComparePanelVisible);
138
140
  const scriptPanelVisible = useViewerStore((s) => s.scriptPanelVisible);
139
141
  const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible);
140
142
  const ganttPanelVisible = useViewerStore((s) => s.ganttPanelVisible);
@@ -283,7 +285,6 @@ export function ViewerLayout() {
283
285
 
284
286
  {/* Main Toolbar — use compact MobileToolbar on mobile */}
285
287
  {isMobile ? <MobileToolbar /> : <MainToolbar onShowShortcuts={shortcutsDialog.toggle} />}
286
- {!isMobile && <DesktopEntitlementBanner />}
287
288
 
288
289
  {/* Main Content Area - Desktop Layout */}
289
290
  {!isMobile && (
@@ -347,6 +348,8 @@ export function ViewerLayout() {
347
348
  <LensPanel onClose={() => setLensPanelVisible(false)} />
348
349
  ) : clashPanelVisible ? (
349
350
  <ClashPanel onClose={() => setClashPanelVisible(false)} />
351
+ ) : comparePanelVisible ? (
352
+ <ComparePanel onClose={() => setComparePanelVisible(false)} />
350
353
  ) : idsPanelVisible ? (
351
354
  <IDSPanel onClose={() => setIdsPanelVisible(false)} />
352
355
  ) : bcfPanelVisible ? (
@@ -808,6 +808,9 @@ export function Viewport({
808
808
  }
809
809
  setIsInitialized(false);
810
810
  rendererRef.current = null;
811
+ // Free all WebGPU resources held by this renderer instance.
812
+ // destroy() is idempotent, so this is safe even if init() rejected.
813
+ renderer.destroy();
811
814
  // Clear BCF global refs to prevent memory leaks
812
815
  clearGlobalRefs();
813
816
  };