@ifc-lite/viewer 1.28.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 (110) hide show
  1. package/.turbo/turbo-build.log +34 -41
  2. package/CHANGELOG.md +10 -0
  3. package/dist/assets/{basketViewActivator-BNRDNuUJ.js → basketViewActivator-Ce38DhXd.js} +7 -7
  4. package/dist/assets/{bcf-DCwCuP7n.js → bcf-Cv_O3JfD.js} +1 -1
  5. package/dist/assets/{deflate-DNGgs8Ur.js → deflate-HbyMq59o.js} +1 -1
  6. package/dist/assets/drawing-2d-DW98umlt.js +257 -0
  7. package/dist/assets/{exporters-B9v81gi9.js → exporters-BuD3XRzB.js} +463 -416
  8. package/dist/assets/geometry.worker-TH3fCCoY.js +1 -0
  9. package/dist/assets/{geotiff-D-YCLS4g.js → geotiff-B2HA8Bwm.js} +10 -10
  10. package/dist/assets/{ids-CCpq-5d3.js → ids-DYUFMd5f.js} +4 -4
  11. package/dist/assets/{ifc-lite_bg-DbgS5EUA.wasm → ifc-lite_bg-BEA5DLmg.wasm} +0 -0
  12. package/dist/assets/index-E9wB0zWt.css +1 -0
  13. package/dist/assets/{index-Bgb3_Pu_.js → index-n5O1QJMM.js} +36808 -39415
  14. package/dist/assets/{index.es-CWfqZyyr.js → index.es-BKVIpZgL.js} +8 -8
  15. package/dist/assets/{jpeg-DGOAeUqU.js → jpeg-C7hjKjPX.js} +1 -1
  16. package/dist/assets/{jspdf.es.min-XPLU2Wkq.js → jspdf.es.min-oWlFc42Y.js} +4 -4
  17. package/dist/assets/{lerc-1PMSCHwX.js → lerc-BfIOGhQz.js} +1 -1
  18. package/dist/assets/{lzw-C65U9lNM.js → lzw-B0jRuuW5.js} +1 -1
  19. package/dist/assets/{native-bridge-XxXos6yI.js → native-bridge-DpB-dtEn.js} +5 -2
  20. package/dist/assets/{packbits-BdMWXC3m.js → packbits-DVvBTC39.js} +1 -1
  21. package/dist/assets/{parser.worker-Ddwo3_06.js → parser.worker-BDsWQ6rc.js} +1 -1
  22. package/dist/assets/{pdf-CRwaZf3s.js → pdf-dVIqI5ac.js} +9 -9
  23. package/dist/assets/raw-C0ZJYGmN.js +1 -0
  24. package/dist/assets/{sandbox-0sDo3g3m.js → sandbox-qpJlrNN0.js} +8 -8
  25. package/dist/assets/{server-client-cTCJ-853.js → server-client-DVZ2huNS.js} +1 -1
  26. package/dist/assets/{webimage-BtakWX7W.js → webimage-B394g0Tw.js} +1 -1
  27. package/dist/assets/{xlsx-B1YOg2QB.js → xlsx-D-oHO76J.js} +7 -7
  28. package/dist/assets/{zstd-CmwsbxmM.js → zstd-Bf38MwV2.js} +1 -1
  29. package/dist/index.html +8 -8
  30. package/package.json +5 -5
  31. package/src/App.tsx +1 -3
  32. package/src/components/viewer/BCFPanel.tsx +1 -16
  33. package/src/components/viewer/ChatPanel.tsx +11 -46
  34. package/src/components/viewer/HierarchyPanel.tsx +2 -176
  35. package/src/components/viewer/IDSPanel.tsx +1 -26
  36. package/src/components/viewer/MainToolbar.tsx +75 -185
  37. package/src/components/viewer/MobileToolbar.tsx +1 -9
  38. package/src/components/viewer/PropertiesPanel.tsx +28 -126
  39. package/src/components/viewer/ScriptPanel.tsx +8 -34
  40. package/src/components/viewer/Section2DPanel.tsx +32 -1
  41. package/src/components/viewer/ViewerLayout.tsx +0 -2
  42. package/src/components/viewer/ViewportContainer.tsx +24 -42
  43. package/src/components/viewer/ViewportOverlays.tsx +1 -4
  44. package/src/components/viewer/useGeometryStreaming.ts +0 -2
  45. package/src/hooks/ingest/federationAlign.ts +7 -0
  46. package/src/hooks/useDrawingGeneration.ts +211 -13
  47. package/src/hooks/useIfcCache.ts +94 -41
  48. package/src/hooks/useIfcFederation.ts +2 -3
  49. package/src/hooks/useIfcLoader.ts +10 -1051
  50. package/src/services/cacheService.ts +9 -25
  51. package/src/services/desktop-export.ts +2 -59
  52. package/src/services/file-dialog.ts +8 -142
  53. package/src/store/constants.ts +23 -0
  54. package/src/store/index.ts +3 -5
  55. package/src/store/slices/drawing2DSlice.ts +8 -0
  56. package/src/store/slices/visibilitySlice.ts +22 -1
  57. package/src/store/types.ts +1 -71
  58. package/src/utils/ifcConfig.ts +0 -12
  59. package/vite.config.ts +6 -3
  60. package/DESKTOP_CONTRACT_VERSION +0 -1
  61. package/dist/assets/drawing-2d-D0dDf6Lh.js +0 -257
  62. package/dist/assets/event-B0kAzHa-.js +0 -1
  63. package/dist/assets/geometry.worker-Bpa3115V.js +0 -1
  64. package/dist/assets/index-BtbXFKsX.css +0 -1
  65. package/dist/assets/raw-CJgQdyuZ.js +0 -1
  66. package/dist/assets/tauri-core-stub-D8Fa-u43.js +0 -1
  67. package/dist/assets/tauri-dialog-stub-r7Wksg7o.js +0 -1
  68. package/dist/assets/tauri-fs-stub-BdeRC7aK.js +0 -1
  69. package/src/components/viewer/DesktopEntitlementBanner.tsx +0 -74
  70. package/src/components/viewer/SettingsPage.tsx +0 -581
  71. package/src/lib/desktop/desktopEntitlementEvents.ts +0 -39
  72. package/src/lib/desktop-entitlement.ts +0 -43
  73. package/src/lib/desktop-product.ts +0 -130
  74. package/src/lib/platform.ts +0 -23
  75. package/src/services/desktop-cache.ts +0 -186
  76. package/src/services/desktop-harness.ts +0 -196
  77. package/src/services/desktop-logger.ts +0 -20
  78. package/src/services/desktop-native-metadata.ts +0 -230
  79. package/src/services/desktop-panel-actions.ts +0 -43
  80. package/src/services/desktop-preferences.ts +0 -44
  81. package/src/services/fs-cache.ts +0 -212
  82. package/src/services/tauri-core-stub.ts +0 -7
  83. package/src/services/tauri-dialog-stub.ts +0 -7
  84. package/src/services/tauri-fs-stub.ts +0 -7
  85. package/src/services/tauri-modules.d.ts +0 -50
  86. package/src/store/slices/desktopEntitlementSlice.ts +0 -86
  87. package/src/utils/desktopModelSnapshot.ts +0 -359
  88. package/src/utils/nativeSpatialDataStore.ts +0 -277
  89. package/src-tauri/Cargo.toml +0 -29
  90. package/src-tauri/build.rs +0 -7
  91. package/src-tauri/capabilities/default.json +0 -18
  92. package/src-tauri/icons/128x128.png +0 -0
  93. package/src-tauri/icons/128x128@2x.png +0 -0
  94. package/src-tauri/icons/32x32.png +0 -0
  95. package/src-tauri/icons/Square107x107Logo.png +0 -0
  96. package/src-tauri/icons/Square142x142Logo.png +0 -0
  97. package/src-tauri/icons/Square150x150Logo.png +0 -0
  98. package/src-tauri/icons/Square284x284Logo.png +0 -0
  99. package/src-tauri/icons/Square30x30Logo.png +0 -0
  100. package/src-tauri/icons/Square310x310Logo.png +0 -0
  101. package/src-tauri/icons/Square44x44Logo.png +0 -0
  102. package/src-tauri/icons/Square71x71Logo.png +0 -0
  103. package/src-tauri/icons/Square89x89Logo.png +0 -0
  104. package/src-tauri/icons/StoreLogo.png +0 -0
  105. package/src-tauri/icons/icon.icns +0 -0
  106. package/src-tauri/icons/icon.ico +0 -0
  107. package/src-tauri/icons/icon.png +0 -0
  108. package/src-tauri/src/lib.rs +0 -21
  109. package/src-tauri/src/main.rs +0 -10
  110. package/src-tauri/tauri.conf.json +0 -39
@@ -32,7 +32,6 @@ 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';
@@ -162,7 +161,7 @@ export function PropertiesPanel() {
162
161
  const m = models.get(selectedEntity.modelId);
163
162
  if (m) {
164
163
  return {
165
- modelQuery: m.nativeMetadata ? null : (m.ifcDataStore ? new IfcQuery(m.ifcDataStore) : null),
164
+ modelQuery: m.ifcDataStore ? new IfcQuery(m.ifcDataStore) : null,
166
165
  model: m,
167
166
  };
168
167
  }
@@ -201,8 +200,6 @@ export function PropertiesPanel() {
201
200
  const [copied, setCopied] = useState(false);
202
201
  const [coordCopied, setCoordCopied] = useState<string | null>(null);
203
202
  const [coordOpen, setCoordOpen] = useState(false);
204
- const [nativeDetails, setNativeDetails] = useState<import('@/store/types').NativeMetadataEntityDetails | null>(null);
205
- const [nativeDetailsState, setNativeDetailsState] = useState<'idle' | 'loading' | 'error'>('idle');
206
203
 
207
204
  // Inline property editing is gated by the global edit-mode pill in
208
205
  // the main toolbar (see `uiSlice.editEnabled`). Reading it from the
@@ -211,32 +208,6 @@ export function PropertiesPanel() {
211
208
  // tools — behind a single switch.
212
209
  const editMode = useViewerStore((s) => s.editEnabled);
213
210
 
214
- useEffect(() => {
215
- if (!selectedEntity || !model?.nativeMetadata) {
216
- setNativeDetails(null);
217
- setNativeDetailsState('idle');
218
- return;
219
- }
220
- let cancelled = false;
221
- setNativeDetailsState('loading');
222
- void getNativeEntityDetails(model.nativeMetadata.cacheKey, selectedEntity.expressId)
223
- .then((details) => {
224
- if (!cancelled) {
225
- setNativeDetails(details);
226
- setNativeDetailsState('idle');
227
- }
228
- })
229
- .catch(() => {
230
- if (!cancelled) {
231
- setNativeDetails(null);
232
- setNativeDetailsState('error');
233
- }
234
- });
235
- return () => {
236
- cancelled = true;
237
- };
238
- }, [selectedEntity, model?.nativeMetadata]);
239
-
240
211
  const copyToClipboard = useCallback((text: string) => {
241
212
  navigator.clipboard.writeText(text);
242
213
  setCopied(true);
@@ -958,51 +929,6 @@ export function PropertiesPanel() {
958
929
  return names;
959
930
  }, [attributes]);
960
931
 
961
- const isNativeLazySelection = Boolean(selectedEntity && model?.nativeMetadata);
962
-
963
- // Native-lazy entities (server-streamed without full STEP data) can
964
- // never be edited; the per-row editors below self-guard via
965
- // `enableEditing={editMode && !isNativeLazySelection}`. The old
966
- // panel-local `editMode` would flip itself off when a native-lazy
967
- // entity was selected; now that the flag is global it stays on, and
968
- // the field-level guards do the work.
969
-
970
- const nativeSpatialInfo = useMemo(() => {
971
- if (!nativeDetails?.spatial?.storeyName) return null;
972
- return {
973
- storeyId: nativeDetails.spatial.storeyId ?? undefined,
974
- storeyName: nativeDetails.spatial.storeyName,
975
- elevation: nativeDetails.spatial.elevation ?? undefined,
976
- height: nativeDetails.spatial.height ?? undefined,
977
- };
978
- }, [nativeDetails]);
979
-
980
- const nativeOccurrenceProperties = useMemo<PropertySet[]>(() => {
981
- if (!nativeDetails) return [];
982
- return nativeDetails.properties.map((pset) => ({
983
- name: pset.name,
984
- properties: pset.properties.map((property) => ({
985
- name: property.name,
986
- value: property.value,
987
- isMutated: false,
988
- })),
989
- isNewPset: false,
990
- source: 'instance' as const,
991
- }));
992
- }, [nativeDetails]);
993
-
994
- const nativeQuantities = useMemo<QuantitySet[]>(() => {
995
- if (!nativeDetails) return [];
996
- return nativeDetails.quantities.map((qset) => ({
997
- name: qset.name,
998
- quantities: qset.quantities.map((quantity) => ({
999
- name: quantity.name,
1000
- value: quantity.value,
1001
- type: quantity.type ?? 0,
1002
- })),
1003
- }));
1004
- }, [nativeDetails]);
1005
-
1006
932
  // Overlay (authored) entities — split halves, duplicates, scripted
1007
933
  // adds — live only in the StoreEditor overlay, NOT the parsed store.
1008
934
  // `modelQuery.entity()` always returns a node, and its getters fall
@@ -1010,49 +936,27 @@ export function PropertiesPanel() {
1010
936
  // table (entity-table.ts#getTypeName). Those non-null sentinels would
1011
937
  // shadow the overlay record in an `entityNode ?? overlay` chain, so
1012
938
  // when an overlay record exists it MUST take precedence.
1013
- const renderedEntityType = isNativeLazySelection
1014
- ? (nativeDetails?.summary.type ?? 'Loading...')
1015
- : (overlayEntity?.type ?? entityNode?.type ?? 'Unknown');
1016
- const renderedEntityName = isNativeLazySelection
1017
- ? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
1018
- : (overlayAttr(2) ?? entityNode?.name ?? undefined);
1019
- const renderedEntityGlobalId = isNativeLazySelection
1020
- ? (nativeDetails?.summary.globalId ?? null)
1021
- : (overlayAttr(0) ?? entityNode?.globalId);
1022
- const renderedEntityDescription = isNativeLazySelection
1023
- ? undefined
1024
- : (overlayAttr(3) ?? entityNode?.description ?? undefined);
1025
- const renderedEntityObjectType = isNativeLazySelection
1026
- ? undefined
1027
- : (overlayAttr(4) ?? entityNode?.objectType ?? undefined);
1028
- const renderedSpatialInfo = isNativeLazySelection ? nativeSpatialInfo : spatialInfo;
1029
- const renderedOccurrenceProperties = isNativeLazySelection ? nativeOccurrenceProperties : occurrenceProperties;
1030
- const renderedInheritedTypeProperties = isNativeLazySelection ? [] : inheritedTypeProperties;
1031
- const renderedMergedProperties = isNativeLazySelection
1032
- ? nativeOccurrenceProperties
1033
- : mergedProperties;
1034
- const renderedQuantities = isNativeLazySelection ? nativeQuantities : quantities;
1035
- const renderedAttributes = isNativeLazySelection ? [] : attributes;
1036
- const renderedClassifications = isNativeLazySelection ? [] : classifications;
1037
- const renderedMaterialInfo = isNativeLazySelection ? null : materialInfo;
1038
- const renderedMaterialProperties = isNativeLazySelection ? [] : materialProperties;
1039
- const renderedDocuments = isNativeLazySelection ? [] : documents;
1040
- const renderedEntityRelationships = isNativeLazySelection ? null : entityRelationships;
1041
- const renderedGeoref = isNativeLazySelection ? null : georef;
1042
- const renderedSpatialContainment = isNativeLazySelection ? null : spatialContainment;
1043
- const renderedTypeProperties = isNativeLazySelection
1044
- ? (nativeDetails?.typeSummary
1045
- ? {
1046
- typeName: nativeDetails.typeSummary.name,
1047
- typeId: nativeDetails.typeSummary.expressId,
1048
- psets: [] as PropertySet[],
1049
- }
1050
- : null)
1051
- : typeProperties;
1052
- const renderedTypeEditImpact = isNativeLazySelection ? null : typeEditImpact;
1053
- const renderedIsTypeEntity = isNativeLazySelection
1054
- ? ((nativeDetails?.summary.type ?? '').endsWith('Type'))
1055
- : 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;
1056
960
  const renderedExistingProps = useMemo(() => {
1057
961
  const keys = new Set<string>();
1058
962
  for (const pset of renderedMergedProperties) {
@@ -1109,7 +1013,7 @@ export function PropertiesPanel() {
1109
1013
  // `overlayEntity` when `entityNode` is empty. Without including
1110
1014
  // `overlayEntity` here the panel collapses to the model-metadata
1111
1015
  // view the moment a fresh add lands.
1112
- if (!selectedEntityId || (!isNativeLazySelection && (!modelQuery || (!entityNode && !overlayEntity)))) {
1016
+ if (!selectedEntityId || !modelQuery || (!entityNode && !overlayEntity)) {
1113
1017
  // Show model metadata when a single model is loaded and nothing selected.
1114
1018
  // Handles both federated models (models.size >= 1) and legacy single-model path (models.size === 0).
1115
1019
  if (models.size === 1) {
@@ -1501,7 +1405,7 @@ export function PropertiesPanel() {
1501
1405
  </div>
1502
1406
  )}
1503
1407
  {/* Edit toolbar - only shown when edit mode is active */}
1504
- {editMode && selectedEntity && !isNativeLazySelection && (
1408
+ {editMode && selectedEntity && (
1505
1409
  <>
1506
1410
  <GeometryEditCard
1507
1411
  modelId={selectedEntity.modelId}
@@ -1547,7 +1451,7 @@ export function PropertiesPanel() {
1547
1451
  pset={pset}
1548
1452
  modelId={selectedEntity?.modelId}
1549
1453
  entityId={selectedEntity?.expressId}
1550
- enableEditing={editMode && !isNativeLazySelection}
1454
+ enableEditing={editMode}
1551
1455
  isTypeProperty={renderedIsTypeEntity}
1552
1456
  typeEditScope={renderedIsTypeEntity ? renderedTypeEditImpact ?? undefined : undefined}
1553
1457
  />
@@ -1571,7 +1475,7 @@ export function PropertiesPanel() {
1571
1475
  pset={pset}
1572
1476
  modelId={selectedEntity?.modelId}
1573
1477
  entityId={renderedTypeProperties.typeId}
1574
- enableEditing={editMode && !isNativeLazySelection}
1478
+ enableEditing={editMode}
1575
1479
  isTypeProperty
1576
1480
  typeEditScope={renderedTypeEditImpact?.mode === 'inherited' ? renderedTypeEditImpact : undefined}
1577
1481
  />
@@ -1695,7 +1599,7 @@ export function PropertiesPanel() {
1695
1599
  </TabsContent>
1696
1600
 
1697
1601
  <TabsContent value="raw-step" className="m-0 p-3 overflow-hidden">
1698
- {selectedEntity && !isNativeLazySelection ? (
1602
+ {selectedEntity ? (
1699
1603
  <RawStepCard
1700
1604
  modelId={selectedEntity.modelId === 'legacy' ? '__legacy__' : selectedEntity.modelId}
1701
1605
  entityId={selectedEntity.expressId}
@@ -1705,9 +1609,7 @@ export function PropertiesPanel() {
1705
1609
  />
1706
1610
  ) : (
1707
1611
  <p className="text-sm text-zinc-500 dark:text-zinc-500 text-center py-8 font-mono">
1708
- {isNativeLazySelection
1709
- ? 'Raw STEP is not available for native-metadata selections'
1710
- : 'Select an entity to inspect raw STEP arguments'}
1612
+ Select an entity to inspect raw STEP arguments
1711
1613
  </p>
1712
1614
  )}
1713
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)' : ''}
@@ -36,7 +36,6 @@ import { GanttPanel } from './schedule/GanttPanel';
36
36
  import { ExtensionsPanel } from '@/components/extensions/ExtensionsPanel';
37
37
  import { CommandPalette } from './CommandPalette';
38
38
  import { SearchModal } from './SearchModal';
39
- import { DesktopEntitlementBanner } from './DesktopEntitlementBanner';
40
39
  import {
41
40
  closeActiveAnalysisExtension,
42
41
  getAnalysisExtensionById,
@@ -286,7 +285,6 @@ export function ViewerLayout() {
286
285
 
287
286
  {/* Main Toolbar — use compact MobileToolbar on mobile */}
288
287
  {isMobile ? <MobileToolbar /> : <MainToolbar onShowShortcuts={shortcutsDialog.toggle} />}
289
- {!isMobile && <DesktopEntitlementBanner />}
290
288
 
291
289
  {/* Main Content Area - Desktop Layout */}
292
290
  {!isMobile && (
@@ -19,10 +19,7 @@ import { toGlobalIdFromModels } from '@/store/globalId';
19
19
  import { collectIfcBuildingStoreyElementsWithIfcSpace } from '@/store/basketVisibleSet';
20
20
  import { useIfc } from '@/hooks/useIfc';
21
21
  import { useWebGPU } from '@/hooks/useWebGPU';
22
- import { openIfcFileDialog } from '@/services/file-dialog';
23
- import { logToDesktopTerminal } from '@/services/desktop-logger';
24
22
  import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRecentFiles, type RecentFileEntry } from '@/lib/recent-files';
25
- import { isTauri } from '@/lib/platform';
26
23
  import { toast } from '@/components/ui/toast';
27
24
  import { describeUnsupportedFormat } from '@/hooks/ingest/pointCloudIngest';
28
25
  import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3, Sparkles, ArrowUpRight, PackagePlus } from 'lucide-react';
@@ -69,6 +66,7 @@ export function ViewportContainer() {
69
66
  const releaseGeometryMemory = useViewerStore((s) => s.releaseGeometryMemory);
70
67
  const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
71
68
  const typeVisibility = useViewerStore((s) => s.typeVisibility);
69
+ const typeViewMode = useViewerStore((s) => s.typeViewMode);
72
70
  const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
73
71
  const classFilter = useViewerStore((s) => s.classFilter);
74
72
  const resetViewerState = useViewerStore((s) => s.resetViewerState);
@@ -392,20 +390,6 @@ export function ViewportContainer() {
392
390
  setCesiumSourceModelId(georef?.sourceModelId ?? null);
393
391
  }, [georef?.sourceModelId, setCesiumSourceModelId]);
394
392
 
395
- useEffect(() => {
396
- // Recent files are a desktop-only feature — the web viewer should not
397
- // show previously opened files in the landing page empty state.
398
- if (!isTauri()) return;
399
-
400
- const refreshRecentFiles = () => {
401
- setRecentFiles(getRecentFiles().slice(0, 3));
402
- };
403
-
404
- refreshRecentFiles();
405
- window.addEventListener('focus', refreshRecentFiles);
406
- return () => window.removeEventListener('focus', refreshRecentFiles);
407
- }, []);
408
-
409
393
  const handleDragOver = useCallback((e: React.DragEvent) => {
410
394
  e.preventDefault();
411
395
  e.stopPropagation();
@@ -504,7 +488,6 @@ export function ViewportContainer() {
504
488
 
505
489
  const handleStartBlank = useCallback(async () => {
506
490
  if (!webgpu.supported) return;
507
- void logToDesktopTerminal('info', '[ViewportContainer] Start blank IFC clicked');
508
491
  const file = createBlankIfcFile();
509
492
  // Must await: loadFile() calls resetViewerState() internally which
510
493
  // resets activeTool back to 'select'. Setting addElement before that
@@ -526,6 +509,7 @@ export function ViewportContainer() {
526
509
  const filteredSourceLenRef = useRef(0);
527
510
  const filteredSourceRef = useRef<MeshData[] | null>(null);
528
511
  const filteredTypeVisRef = useRef(typeVisibility);
512
+ const filteredTypeModeRef = useRef(typeViewMode);
529
513
  const filteredVersionRef = useRef(0);
530
514
 
531
515
  const filteredGeometry = useMemo(() => {
@@ -540,18 +524,21 @@ export function ViewportContainer() {
540
524
  const allMeshes = mergedGeometryResult.meshes;
541
525
  const cache = filteredCacheRef.current;
542
526
 
543
- // Full rebuild if: type visibility changed, source shrunk (new file), or empty cache
527
+ // Full rebuild if: type visibility changed, view mode changed, source shrunk
528
+ // (new file), or empty cache
544
529
  const prevVis = filteredTypeVisRef.current;
545
530
  const typeVisChanged =
546
531
  prevVis.spaces !== typeVisibility.spaces ||
547
532
  prevVis.openings !== typeVisibility.openings ||
548
- prevVis.site !== typeVisibility.site;
533
+ prevVis.site !== typeVisibility.site ||
534
+ filteredTypeModeRef.current !== typeViewMode;
549
535
  const sourceChanged = filteredSourceRef.current !== allMeshes;
550
536
  if (typeVisChanged || sourceChanged || allMeshes.length < filteredSourceLenRef.current) {
551
537
  cache.length = 0;
552
538
  filteredSourceLenRef.current = 0;
553
539
  filteredSourceRef.current = allMeshes;
554
540
  filteredTypeVisRef.current = typeVisibility;
541
+ filteredTypeModeRef.current = typeViewMode;
555
542
  }
556
543
 
557
544
  const needsFilter = !typeVisibility.spaces || !typeVisibility.openings || !typeVisibility.site;
@@ -562,6 +549,18 @@ export function ViewportContainer() {
562
549
  const mesh = allMeshes[i];
563
550
  const ifcType = mesh.ifcType;
564
551
 
552
+ // Model/Types view switch (#957 follow-up). geometryClass: 0 = occurrence,
553
+ // 1 = orphan type (no occurrence — shown in BOTH modes since it's the only
554
+ // geometry), 2 = instanced type-library shape. In 'model' mode hide class 2
555
+ // (else the AC20 duplicate boxes at the MappingOrigin reappear); in 'types'
556
+ // mode hide occurrences (class 0) so only the type library shows.
557
+ const geometryClass = mesh.geometryClass ?? 0;
558
+ if (typeViewMode === 'types') {
559
+ if (geometryClass === 0) continue;
560
+ } else if (geometryClass === 2) {
561
+ continue;
562
+ }
563
+
565
564
  if (needsFilter) {
566
565
  if (ifcType === 'IfcSpace' && !typeVisibility.spaces) continue;
567
566
  if (ifcType === 'IfcOpeningElement' && !typeVisibility.openings) continue;
@@ -588,7 +587,7 @@ export function ViewportContainer() {
588
587
  // Return the same array reference — downstream change detection uses
589
588
  // geometryVersion (which increments each batch) instead of array identity.
590
589
  return cache;
591
- }, [mergedGeometryResult, typeVisibility]);
590
+ }, [mergedGeometryResult, typeVisibility, typeViewMode]);
592
591
 
593
592
  // Version counter that changes every batch — triggers useGeometryStreaming
594
593
  // without requiring a new geometry array reference.
@@ -882,27 +881,10 @@ export function ViewportContainer() {
882
881
  */}
883
882
  {/* Track 1 — open / drag */}
884
883
  <button
885
- onClick={async () => {
884
+ onClick={() => {
886
885
  if (!webgpu.supported) {
887
886
  return;
888
887
  }
889
-
890
- void logToDesktopTerminal('info', '[ViewportContainer] Empty-state open button clicked');
891
- const file = await openIfcFileDialog();
892
- if (file) {
893
- void logToDesktopTerminal('info', `[ViewportContainer] Native dialog selected ${file.path}`);
894
- recordRecentFiles([{
895
- name: file.name,
896
- size: file.size,
897
- path: file.path,
898
- modifiedMs: file.modifiedMs ?? null,
899
- }]);
900
- setRecentFiles(getRecentFiles().slice(0, 3));
901
- loadFile(file);
902
- return;
903
- }
904
-
905
- void logToDesktopTerminal('info', '[ViewportContainer] Falling back to browser file input');
906
888
  fileInputRef.current?.click();
907
889
  }}
908
890
  disabled={!webgpu.supported || webgpu.checking}
@@ -1064,7 +1046,7 @@ export function ViewportContainer() {
1064
1046
  )}
1065
1047
 
1066
1048
  {/* Cesium 3D world context overlay — rendered behind the WebGPU canvas (web only) */}
1067
- {cesiumEnabled && georef && !isTauri() && (
1049
+ {cesiumEnabled && georef && (
1068
1050
  <CesiumOverlay
1069
1051
  mapConversion={georef.mapConversion}
1070
1052
  cameraMapConversion={georef.baseMapConversion}
@@ -1075,7 +1057,7 @@ export function ViewportContainer() {
1075
1057
  storeyElevations={georef.storeyElevations}
1076
1058
  />
1077
1059
  )}
1078
- {cesiumEnabled && georef?.mapConversion && !isTauri() && georef.baseMapConversion && (
1060
+ {cesiumEnabled && georef?.mapConversion && georef.baseMapConversion && (
1079
1061
  <CesiumPlacementEditor
1080
1062
  modelId={georef.sourceModelId}
1081
1063
  mapConversion={georef.mapConversion}
@@ -1094,7 +1076,7 @@ export function ViewportContainer() {
1094
1076
  coordinateInfo={mergedGeometryResult?.coordinateInfo}
1095
1077
  computedIsolatedIds={computedIsolatedIds}
1096
1078
  modelIdToIndex={modelIdToIndex}
1097
- cesiumActive={cesiumEnabled && georef !== null && !isTauri()}
1079
+ cesiumActive={cesiumEnabled && georef !== null}
1098
1080
  releaseGeometryAfterStream={false}
1099
1081
  onGeometryReleased={releaseGeometryMemory}
1100
1082
  />
@@ -15,15 +15,12 @@ import { useViewerStore } from '@/store';
15
15
  import { goHomeFromStore } from '@/store/homeView';
16
16
  import { useIfc } from '@/hooks/useIfc';
17
17
  import { cn } from '@/lib/utils';
18
- import { isTauri } from '@/lib/platform';
19
18
  import { ViewCube, type ViewCubeRef } from './ViewCube';
20
19
  import { AxisHelper, type AxisHelperRef } from './AxisHelper';
21
20
  import { BasepointOverlay } from './BasepointOverlay';
22
21
  import { PointCloudPanel } from './PointCloudPanel';
23
22
  import { Crosshair } from 'lucide-react';
24
23
 
25
- const isDesktop = isTauri();
26
-
27
24
  export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: boolean } = {}) {
28
25
  const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
29
26
  const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
@@ -144,7 +141,7 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
144
141
  <>
145
142
  <PointCloudPanelMount />
146
143
  {/* Bottom-right: Navigation controls (hidden when Cesium active — Cesium is web-only) */}
147
- {!(cesiumEnabled && !isDesktop) && (
144
+ {!cesiumEnabled && (
148
145
  <div
149
146
  className={cn(
150
147
  'absolute flex flex-col gap-1 bg-background/90 backdrop-blur-sm border p-1',
@@ -21,7 +21,6 @@
21
21
  import { useEffect, useRef, type MutableRefObject } from 'react';
22
22
  import type { Renderer } from '@ifc-lite/renderer';
23
23
  import type { MeshData, CoordinateInfo } from '@ifc-lite/geometry';
24
- import { logToDesktopTerminal } from '@/services/desktop-logger';
25
24
  import { toast } from '../ui/toast.js';
26
25
 
27
26
  // Session-scoped flag so the linear-infrastructure hint fires at most once
@@ -87,7 +86,6 @@ const MAX_VALID_COORD = 10000;
87
86
 
88
87
  function traceGeometrySync(message: string): void {
89
88
  console.log(`[GeomSync] ${message}`);
90
- void logToDesktopTerminal('info', `[GeomSync] ${message}`);
91
89
  }
92
90
 
93
91
  export function useGeometryStreaming(params: UseGeometryStreamingParams): void {
@@ -172,6 +172,13 @@ function buildGeorefAlignmentTransform(source: ModelGeoref, reference: ModelGeor
172
172
 
173
173
  const yVx = (-refAxis.o * eVx + refAxis.a * nVx) * invRefDenom;
174
174
  const yVz = (-refAxis.o * eVz + refAxis.a * nVz) * invRefDenom;
175
+ // NOTE: the refOffset handling is intentionally asymmetric between X and Z and
176
+ // must NOT be "symmetrised". refOffset is subtracted from the FINAL viewer
177
+ // coordinate on every axis. X maps positively (`tx = +xC`), so its offset is
178
+ // folded into xC above. Z maps to the NEGATED north axis (`tz = -yC`), so its
179
+ // offset is applied after the negation, leaving yC offset-free here. This
180
+ // matches alignGeometryAcrossCrs: alignedZ = refWorldZ - refOffset.z with
181
+ // refWorldZ = -ifcYr. Folding -refOffset.z into yC would flip its sign.
175
182
  const yC = (-refAxis.o * eC + refAxis.a * nC) * invRefDenom;
176
183
 
177
184
  return {