@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
@@ -15,6 +15,7 @@ import {
15
15
  EyeOff,
16
16
  Equal,
17
17
  Crosshair,
18
+ GitCompareArrows,
18
19
  Home,
19
20
  Maximize2,
20
21
  Grid3x3,
@@ -43,13 +44,14 @@ import {
43
44
  CalendarClock,
44
45
  Globe2,
45
46
  Move,
46
- Settings,
47
47
  PenLine,
48
48
  Layers3,
49
49
  SquareStack,
50
50
  ChevronsUpDown,
51
51
  Undo2,
52
52
  Redo2,
53
+ Boxes,
54
+ Shapes,
53
55
  } from 'lucide-react';
54
56
  import { Button } from '@/components/ui/button';
55
57
  import { Switch } from '@/components/ui/switch';
@@ -82,16 +84,10 @@ import { DataConnector } from './DataConnector';
82
84
  import { ExportChangesButton } from './ExportChangesButton';
83
85
  import { SearchInline } from './SearchInline';
84
86
  import { useFloorplanView } from '@/hooks/useFloorplanView';
85
- import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess, type DesktopFeature } from '@/lib/desktop-product';
86
87
  import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
87
88
  import { ThemeSwitch } from './ThemeSwitch';
88
89
  import { ExtensionToolbarSlot } from '@/components/extensions/ExtensionToolbarSlot';
89
90
  import { toast } from '@/components/ui/toast';
90
- import { navigateToPath } from '@/services/app-navigation';
91
- import { getStartupHarnessRequest, setActiveHarnessRequest, tryClaimStartupHarnessRequest } from '@/services/desktop-harness';
92
- import { logToDesktopTerminal } from '@/services/desktop-logger';
93
- import { openIfcFileDialog, type NativeFileHandle } from '@/services/file-dialog';
94
- import { isTauri } from '@/lib/platform';
95
91
  import {
96
92
  closeActiveAnalysisExtension,
97
93
  getAnalysisExtensionsSnapshot,
@@ -102,10 +98,6 @@ import {
102
98
  type Tool = 'select' | 'walk' | 'measure' | 'section' | 'annotate' | 'addElement' | 'split';
103
99
  type WorkspacePanel = 'script' | 'list' | 'bcf' | 'ids' | 'lens' | 'addElement' | string;
104
100
 
105
- function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
106
- return typeof (file as NativeFileHandle).path === 'string';
107
- }
108
-
109
101
  // #region FIX: Move ToolButton OUTSIDE MainToolbar to prevent recreation on every render
110
102
  // This fixes Radix UI Tooltip's asChild prop becoming stale during re-renders
111
103
  interface ToolButtonProps {
@@ -437,11 +429,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
437
429
  // Listen for programmatic file-load requests (from command palette recent files)
438
430
  useEffect(() => {
439
431
  const handler = (e: Event) => {
440
- const file = (e as CustomEvent<File | NativeFileHandle>).detail;
432
+ const file = (e as CustomEvent<File>).detail;
441
433
  if (file) {
442
- recordRecentFiles([isNativeFileHandle(file)
443
- ? { name: file.name, size: file.size, path: file.path, modifiedMs: file.modifiedMs ?? null }
444
- : { name: file.name, size: file.size }]);
434
+ recordRecentFiles([{ name: file.name, size: file.size }]);
445
435
  void loadFile(file);
446
436
  }
447
437
  };
@@ -449,78 +439,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
449
439
  return () => window.removeEventListener('ifc-lite:load-file', handler);
450
440
  }, [loadFile]);
451
441
 
452
- useEffect(() => {
453
- let cancelled = false;
454
- const sleep = (ms: number) => new Promise((resolve) => globalThis.setTimeout(resolve, ms));
455
-
456
- const waitForViewerToSettle = async (label: string) => {
457
- const timeoutMs = 120_000;
458
- const pollMs = 100;
459
- const start = performance.now();
460
- while (!cancelled) {
461
- const state = useViewerStore.getState();
462
- const meshCount = state.geometryResult?.meshes.length ?? 0;
463
- if (!state.loading && meshCount > 0) {
464
- void logToDesktopTerminal(
465
- 'info',
466
- `[DesktopHarness] ${label} settled: loading=${state.loading} meshes=${meshCount} progress=${state.progress?.phase ?? 'none'}`
467
- );
468
- return;
469
- }
470
- if (performance.now() - start >= timeoutMs) {
471
- throw new Error(`[DesktopHarness] Timed out waiting for ${label} to settle`);
472
- }
473
- await sleep(pollMs);
474
- }
475
- };
476
-
477
- void (async () => {
478
- void logToDesktopTerminal('info', '[DesktopHarness] MainToolbar startup harness effect running');
479
- const request = await getStartupHarnessRequest();
480
- if (!request || cancelled) {
481
- void logToDesktopTerminal(
482
- 'info',
483
- `[DesktopHarness] No startup harness request available (cancelled=${cancelled})`
484
- );
485
- return;
486
- }
487
- if (!tryClaimStartupHarnessRequest(request)) {
488
- void logToDesktopTerminal('info', `[DesktopHarness] Startup harness request already claimed for ${request.file.path}`);
489
- return;
490
- }
491
- void logToDesktopTerminal('info', `[DesktopHarness] Claimed startup harness request for ${request.file.path}`);
492
- console.log(`[DesktopHarness] Auto-loading startup file: ${request.file.path}`);
493
- if (!request.replaceFile) {
494
- void logToDesktopTerminal('info', `[DesktopHarness] Calling loadFile for ${request.file.path}`);
495
- await loadFile(request.file);
496
- return;
497
- }
498
-
499
- void logToDesktopTerminal(
500
- 'info',
501
- `[DesktopHarness] Running replacement sequence first=${request.file.path} second=${request.replaceFile.path}`
502
- );
503
- setActiveHarnessRequest(null);
504
- await loadFile(request.file);
505
- await waitForViewerToSettle(`first load ${request.file.name}`);
506
- if (cancelled) {
507
- return;
508
- }
509
-
510
- setActiveHarnessRequest({
511
- ...request,
512
- file: request.replaceFile,
513
- replaceFile: undefined,
514
- });
515
- void logToDesktopTerminal('info', `[DesktopHarness] Calling replacement loadFile for ${request.replaceFile.path}`);
516
- await loadFile(request.replaceFile);
517
- })();
518
-
519
- return () => {
520
- cancelled = true;
521
- };
522
- }, [loadFile]);
523
-
524
442
  // Floorplan view
525
443
  const { availableStoreys, activateFloorplan } = useFloorplanView();
526
444
 
@@ -540,6 +458,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
540
458
  const typeVisibility = useViewerStore((state) => state.typeVisibility);
541
459
  const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
542
460
  const resetTypeVisibility = useViewerStore((state) => state.resetTypeVisibility);
461
+ // #957 follow-up: Model/Types 3D view switch — 'model' shows placed
462
+ // occurrences (default), 'types' shows the type-library shapes.
463
+ const typeViewMode = useViewerStore((state) => state.typeViewMode);
464
+ const setTypeViewMode = useViewerStore((state) => state.setTypeViewMode);
543
465
  // How many of the five class toggles are on — surfaced in the menu
544
466
  // header so the user sees scene state at a glance.
545
467
  const visibleClassCount = [
@@ -562,6 +484,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
562
484
  const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
563
485
  const clashPanelVisible = useViewerStore((state) => state.clashPanelVisible);
564
486
  const setClashPanelVisible = useViewerStore((state) => state.setClashPanelVisible);
487
+ const comparePanelVisible = useViewerStore((state) => state.comparePanelVisible);
488
+ const setComparePanelVisible = useViewerStore((state) => state.setComparePanelVisible);
565
489
  const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
566
490
  const setListPanelVisible = useViewerStore((state) => state.setListPanelVisible);
567
491
  const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
@@ -588,7 +512,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
588
512
  const cesiumPlacementEditMode = useViewerStore((state) => state.cesiumPlacementEditMode);
589
513
  const setCesiumPlacementEditMode = useViewerStore((state) => state.setCesiumPlacementEditMode);
590
514
  const storeModels = useViewerStore((state) => state.models);
591
- const desktopEntitlement = useViewerStore((state) => state.desktopEntitlement);
592
515
  const analysisExtensionState = useSyncExternalStore(
593
516
  subscribeAnalysisExtensions,
594
517
  getAnalysisExtensionsSnapshot,
@@ -606,7 +529,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
606
529
  () => analysisExtensionState.extensions.filter((extension) => (extension.placement ?? 'right') === 'bottom'),
607
530
  [analysisExtensionState.extensions],
608
531
  );
609
- const desktopShell = isTauri();
610
532
 
611
533
  // NOTE: The Class Visibility dropdown used to gate each toggle on whether
612
534
  // the loaded model actually contained that class (scanning meshes for
@@ -722,19 +644,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
722
644
  goHomeFromStore();
723
645
  }, []);
724
646
 
725
- const promptDesktopUpgrade = useCallback((featureLabel: string) => {
726
- toast.info(`${featureLabel} is available with Desktop Pro`);
727
- navigateToPath(buildDesktopUpgradeUrl());
728
- }, []);
729
-
730
- const requireDesktopFeature = useCallback((feature: DesktopFeature, label: string) => {
731
- if (hasDesktopFeatureAccess(desktopEntitlement, feature)) {
732
- return true;
733
- }
734
- promptDesktopUpgrade(label);
735
- return false;
736
- }, [desktopEntitlement, promptDesktopUpgrade]);
737
-
738
647
  const handleToggleBottomPanel = useCallback((panel: 'script' | 'list' | 'gantt') => {
739
648
  if (activeAnalysisExtension?.placement === 'bottom') {
740
649
  closeActiveAnalysisExtension();
@@ -761,24 +670,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
761
670
  setScriptPanelVisible,
762
671
  ]);
763
672
 
764
- const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'addElement' | 'extensions') => {
673
+ const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'compare' | 'addElement' | 'extensions') => {
765
674
  if (activeAnalysisExtension?.placement !== 'bottom') {
766
675
  closeActiveAnalysisExtension();
767
676
  }
768
- if (panel === 'bcf' && !requireDesktopFeature('bcf_issue_management', 'BCF issue management')) {
769
- return;
770
- }
771
- if (panel === 'ids' && !requireDesktopFeature('ids_validation', 'IDS validation')) {
772
- return;
773
- }
774
- if (panel === 'extensions' && !requireDesktopFeature('extensions', 'Extensions')) {
775
- return;
776
- }
777
677
 
778
678
  const nextBcfVisible = panel === 'bcf' ? !bcfPanelVisible : false;
779
679
  const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
780
680
  const nextLensVisible = panel === 'lens' ? !lensPanelVisible : false;
781
681
  const nextClashVisible = panel === 'clash' ? !clashPanelVisible : false;
682
+ const nextCompareVisible = panel === 'compare' ? !comparePanelVisible : false;
782
683
  const nextExtensionsVisible = panel === 'extensions' ? !extensionsPanelVisible : false;
783
684
  const isAddElementActive = activeTool === 'addElement';
784
685
  const nextAddElementActive = panel === 'addElement' ? !isAddElementActive : false;
@@ -787,6 +688,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
787
688
  setIdsPanelVisible(nextIdsVisible);
788
689
  setLensPanelVisible(nextLensVisible);
789
690
  setClashPanelVisible(nextClashVisible);
691
+ setComparePanelVisible(nextCompareVisible);
790
692
  setExtensionsPanelVisible(nextExtensionsVisible);
791
693
 
792
694
  if (panel === 'addElement') {
@@ -795,7 +697,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
795
697
  setActiveTool('select');
796
698
  }
797
699
 
798
- if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextClashVisible || nextExtensionsVisible || nextAddElementActive) {
700
+ if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextClashVisible || nextCompareVisible || nextExtensionsVisible || nextAddElementActive) {
799
701
  setRightPanelCollapsed(false);
800
702
  }
801
703
  }, [
@@ -803,13 +705,14 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
803
705
  activeTool,
804
706
  bcfPanelVisible,
805
707
  clashPanelVisible,
708
+ comparePanelVisible,
806
709
  extensionsPanelVisible,
807
710
  idsPanelVisible,
808
711
  lensPanelVisible,
809
- requireDesktopFeature,
810
712
  setActiveTool,
811
713
  setBcfPanelVisible,
812
714
  setClashPanelVisible,
715
+ setComparePanelVisible,
813
716
  setExtensionsPanelVisible,
814
717
  setIdsPanelVisible,
815
718
  setLensPanelVisible,
@@ -877,6 +780,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
877
780
  if (idsPanelVisible) panels.add('ids');
878
781
  if (lensPanelVisible) panels.add('lens');
879
782
  if (clashPanelVisible) panels.add('clash');
783
+ if (comparePanelVisible) panels.add('compare');
880
784
  if (extensionsPanelVisible) panels.add('extensions');
881
785
  if (activeTool === 'addElement') panels.add('addElement');
882
786
  if (analysisExtensionState.activeId) panels.add(analysisExtensionState.activeId);
@@ -886,6 +790,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
886
790
  analysisExtensionState.activeId,
887
791
  bcfPanelVisible,
888
792
  clashPanelVisible,
793
+ comparePanelVisible,
889
794
  extensionsPanelVisible,
890
795
  ganttPanelVisible,
891
796
  idsPanelVisible,
@@ -904,13 +809,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
904
809
  if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
905
810
  if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
906
811
  if (activeWorkspacePanels.has('clash')) return 'Clash Detection';
812
+ if (activeWorkspacePanels.has('compare')) return 'Compare Models';
907
813
  if (activeWorkspacePanels.has('extensions')) return 'Extensions';
908
814
  if (activeWorkspacePanels.has('addElement')) return 'Add Element';
909
815
  return activeAnalysisExtension?.label ?? 'Analysis';
910
816
  }, [activeAnalysisExtension?.label, activeWorkspacePanels]);
911
817
 
912
818
  const handleScreenshot = useCallback(() => {
913
- if (!requireDesktopFeature('exports', 'Exports')) return;
914
819
  const canvas = document.querySelector('canvas');
915
820
  if (!canvas) return;
916
821
  try {
@@ -924,10 +829,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
924
829
  console.error('Screenshot failed:', err);
925
830
  toast.error('Screenshot failed');
926
831
  }
927
- }, [requireDesktopFeature]);
832
+ }, []);
928
833
 
929
834
  const handleExportCSV = useCallback((type: 'entities' | 'properties' | 'quantities' | 'spatial') => {
930
- if (!requireDesktopFeature('exports', 'Exports')) return;
931
835
  if (!ifcDataStore) return;
932
836
  try {
933
837
  const exporter = new CSVExporter(ifcDataStore);
@@ -965,10 +869,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
965
869
  console.error('CSV export failed:', err);
966
870
  toast.error(`CSV export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
967
871
  }
968
- }, [ifcDataStore, requireDesktopFeature]);
872
+ }, [ifcDataStore]);
969
873
 
970
874
  const handleExportJSON = useCallback(() => {
971
- if (!requireDesktopFeature('exports', 'Exports')) return;
972
875
  if (!ifcDataStore) return;
973
876
  try {
974
877
  const entities: Record<string, unknown>[] = [];
@@ -996,7 +899,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
996
899
  console.error('JSON export failed:', err);
997
900
  toast.error(`JSON export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
998
901
  }
999
- }, [ifcDataStore, requireDesktopFeature]);
902
+ }, [ifcDataStore]);
1000
903
 
1001
904
  return (
1002
905
  <div className="flex items-center gap-1 px-2 h-12 border-b bg-white dark:bg-black border-zinc-200 dark:border-zinc-800 relative z-50">
@@ -1024,25 +927,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1024
927
  <Button
1025
928
  variant="ghost"
1026
929
  size="icon-sm"
1027
- onClick={async (e) => {
930
+ onClick={(e) => {
1028
931
  // Blur button to close tooltip before opening file dialog
1029
932
  (e.currentTarget as HTMLButtonElement).blur();
1030
-
1031
- void logToDesktopTerminal('info', '[MainToolbar] Open file button clicked');
1032
- const file = await openIfcFileDialog();
1033
- if (file) {
1034
- void logToDesktopTerminal('info', `[MainToolbar] Native dialog selected ${file.path}`);
1035
- recordRecentFiles([{
1036
- name: file.name,
1037
- size: file.size,
1038
- path: file.path,
1039
- modifiedMs: file.modifiedMs ?? null,
1040
- }]);
1041
- void loadFile(file);
1042
- return;
1043
- }
1044
-
1045
- void logToDesktopTerminal('info', '[MainToolbar] Falling back to browser file input');
1046
933
  fileInputRef.current?.click();
1047
934
  }}
1048
935
  disabled={loading}
@@ -1085,37 +972,23 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1085
972
  </Button>
1086
973
  </DropdownMenuTrigger>
1087
974
  <DropdownMenuContent>
1088
- {hasDesktopFeatureAccess(desktopEntitlement, 'exports') ? (
1089
- <ExportDialog
1090
- trigger={
1091
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
1092
- <FileText className="h-4 w-4 mr-2" />
1093
- Export IFC (with changes)
1094
- </DropdownMenuItem>
1095
- }
1096
- />
1097
- ) : (
1098
- <DropdownMenuItem onClick={() => promptDesktopUpgrade('Exports')}>
1099
- <FileText className="h-4 w-4 mr-2" />
1100
- Export IFC (with changes)
1101
- </DropdownMenuItem>
1102
- )}
975
+ <ExportDialog
976
+ trigger={
977
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
978
+ <FileText className="h-4 w-4 mr-2" />
979
+ Export IFC (with changes)
980
+ </DropdownMenuItem>
981
+ }
982
+ />
1103
983
  <DropdownMenuSeparator />
1104
- {hasDesktopFeatureAccess(desktopEntitlement, 'exports') ? (
1105
- <GLBExportDialog
1106
- trigger={
1107
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
1108
- <Download className="h-4 w-4 mr-2" />
1109
- Export GLB (3D Model)
1110
- </DropdownMenuItem>
1111
- }
1112
- />
1113
- ) : (
1114
- <DropdownMenuItem onClick={() => promptDesktopUpgrade('Exports')}>
1115
- <Download className="h-4 w-4 mr-2" />
1116
- Export GLB (3D Model)
1117
- </DropdownMenuItem>
1118
- )}
984
+ <GLBExportDialog
985
+ trigger={
986
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
987
+ <Download className="h-4 w-4 mr-2" />
988
+ Export GLB (3D Model)
989
+ </DropdownMenuItem>
990
+ }
991
+ />
1119
992
  <DropdownMenuSeparator />
1120
993
  <DropdownMenuSub>
1121
994
  <DropdownMenuSubTrigger disabled={!ifcDataStore}>
@@ -1187,9 +1060,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1187
1060
  </DropdownMenu>
1188
1061
 
1189
1062
  {/* Export Changes Button - shows when there are pending mutations */}
1190
- {hasDesktopFeatureAccess(desktopEntitlement, 'exports') ? (
1191
- <ExportChangesButton />
1192
- ) : null}
1063
+ <ExportChangesButton />
1193
1064
 
1194
1065
  {/* ── Panels ── */}
1195
1066
  <DropdownMenu>
@@ -1265,6 +1136,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1265
1136
  <Crosshair className="h-4 w-4 mr-2" />
1266
1137
  Clash Detection
1267
1138
  </DropdownMenuCheckboxItem>
1139
+ <DropdownMenuCheckboxItem
1140
+ checked={activeWorkspacePanels.has('compare')}
1141
+ onCheckedChange={() => handleToggleRightPanel('compare')}
1142
+ >
1143
+ <GitCompareArrows className="h-4 w-4 mr-2" />
1144
+ Compare Models
1145
+ </DropdownMenuCheckboxItem>
1268
1146
  <DropdownMenuSeparator />
1269
1147
  <DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
1270
1148
  Author
@@ -1532,6 +1410,51 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1532
1410
  model lacks is a no-op.
1533
1411
  */}
1534
1412
  <DropdownMenuContent align="start" className="w-[300px] p-1.5">
1413
+ {/* Model / Types 3D view switch (#957 follow-up). A type carries a
1414
+ RepresentationMap whose shape is drawn at its MappingOrigin; "Types"
1415
+ shows that type library, "Model" shows the placed occurrences. The
1416
+ two are mutually exclusive — toggling re-filters the cached mesh set
1417
+ instantly (no reload). */}
1418
+ <div className="px-1.5 pb-1 pt-0.5">
1419
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
1420
+ 3D View
1421
+ </span>
1422
+ </div>
1423
+ <div className="flex gap-1 px-1.5 pb-1.5" role="radiogroup" aria-label="3D view mode">
1424
+ <button
1425
+ type="button"
1426
+ role="radio"
1427
+ aria-checked={typeViewMode === 'model'}
1428
+ onClick={() => setTypeViewMode('model')}
1429
+ className={cn(
1430
+ 'flex flex-1 items-center justify-center gap-1.5 rounded-md border px-2 py-1.5 text-xs font-medium transition-colors',
1431
+ typeViewMode === 'model'
1432
+ ? 'border-primary/40 bg-primary/10 text-foreground'
1433
+ : 'border-transparent text-muted-foreground hover:bg-muted/50',
1434
+ )}
1435
+ >
1436
+ <Boxes className="h-3.5 w-3.5 shrink-0" />
1437
+ Model
1438
+ </button>
1439
+ <button
1440
+ type="button"
1441
+ role="radio"
1442
+ aria-checked={typeViewMode === 'types'}
1443
+ onClick={() => setTypeViewMode('types')}
1444
+ className={cn(
1445
+ 'flex flex-1 items-center justify-center gap-1.5 rounded-md border px-2 py-1.5 text-xs font-medium transition-colors',
1446
+ typeViewMode === 'types'
1447
+ ? 'border-primary/40 bg-primary/10 text-foreground'
1448
+ : 'border-transparent text-muted-foreground hover:bg-muted/50',
1449
+ )}
1450
+ >
1451
+ <Shapes className="h-3.5 w-3.5 shrink-0" />
1452
+ Types
1453
+ </button>
1454
+ </div>
1455
+
1456
+ <DropdownMenuSeparator className="my-1" />
1457
+
1535
1458
  <div className="flex items-center justify-between gap-2 px-1.5 pb-1 pt-0.5">
1536
1459
  <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
1537
1460
  Visibility
@@ -1621,7 +1544,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1621
1544
  appears beside it (its amber tint signals a modal pose whose
1622
1545
  exit affordance must stay visible).
1623
1546
  */}
1624
- {cesiumAvailable && !desktopShell && (
1547
+ {cesiumAvailable && (
1625
1548
  <>
1626
1549
  <Tooltip>
1627
1550
  <TooltipTrigger asChild>
@@ -1783,22 +1706,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1783
1706
  the toolbar's meta cluster stays focused on shell chrome
1784
1707
  (Settings · Theme · Help). */}
1785
1708
  <div className="flex items-center gap-2 ml-2 pl-2 border-l border-zinc-200 dark:border-zinc-700/60">
1786
- {desktopShell ? (
1787
- <Tooltip>
1788
- <TooltipTrigger asChild>
1789
- <Button
1790
- variant="ghost"
1791
- size="icon"
1792
- className="rounded-full"
1793
- onClick={() => navigateToPath('/settings')}
1794
- >
1795
- <Settings className="!h-[20px] !w-[20px]" />
1796
- </Button>
1797
- </TooltipTrigger>
1798
- <TooltipContent>Settings</TooltipContent>
1799
- </Tooltip>
1800
- ) : null}
1801
-
1802
1709
  <Tooltip>
1803
1710
  <TooltipTrigger asChild>
1804
1711
  <div>
@@ -44,8 +44,6 @@ import { executeBasketIsolate } from '@/store/basket/basketCommands';
44
44
  import { useIfc } from '@/hooks/useIfc';
45
45
  import { cn } from '@/lib/utils';
46
46
  import { GLTFExporter } from '@ifc-lite/export';
47
- import { openIfcFileDialog } from '@/services/file-dialog';
48
- import { logToDesktopTerminal } from '@/services/desktop-logger';
49
47
  import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
50
48
  import { toast } from '@/components/ui/toast';
51
49
 
@@ -181,13 +179,7 @@ export function MobileToolbar() {
181
179
  variant="ghost"
182
180
  size="icon-sm"
183
181
  className="h-9 w-9 flex-shrink-0"
184
- onClick={async () => {
185
- const file = await openIfcFileDialog();
186
- if (file) {
187
- recordRecentFiles([{ name: file.name, size: file.size, path: file.path, modifiedMs: file.modifiedMs ?? null }]);
188
- void loadFile(file);
189
- return;
190
- }
182
+ onClick={() => {
191
183
  fileInputRef.current?.click();
192
184
  }}
193
185
  disabled={loading}