@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
@@ -44,13 +44,14 @@ import {
44
44
  CalendarClock,
45
45
  Globe2,
46
46
  Move,
47
- Settings,
48
47
  PenLine,
49
48
  Layers3,
50
49
  SquareStack,
51
50
  ChevronsUpDown,
52
51
  Undo2,
53
52
  Redo2,
53
+ Boxes,
54
+ Shapes,
54
55
  } from 'lucide-react';
55
56
  import { Button } from '@/components/ui/button';
56
57
  import { Switch } from '@/components/ui/switch';
@@ -83,16 +84,10 @@ import { DataConnector } from './DataConnector';
83
84
  import { ExportChangesButton } from './ExportChangesButton';
84
85
  import { SearchInline } from './SearchInline';
85
86
  import { useFloorplanView } from '@/hooks/useFloorplanView';
86
- import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess, type DesktopFeature } from '@/lib/desktop-product';
87
87
  import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
88
88
  import { ThemeSwitch } from './ThemeSwitch';
89
89
  import { ExtensionToolbarSlot } from '@/components/extensions/ExtensionToolbarSlot';
90
90
  import { toast } from '@/components/ui/toast';
91
- import { navigateToPath } from '@/services/app-navigation';
92
- import { getStartupHarnessRequest, setActiveHarnessRequest, tryClaimStartupHarnessRequest } from '@/services/desktop-harness';
93
- import { logToDesktopTerminal } from '@/services/desktop-logger';
94
- import { openIfcFileDialog, type NativeFileHandle } from '@/services/file-dialog';
95
- import { isTauri } from '@/lib/platform';
96
91
  import {
97
92
  closeActiveAnalysisExtension,
98
93
  getAnalysisExtensionsSnapshot,
@@ -103,10 +98,6 @@ import {
103
98
  type Tool = 'select' | 'walk' | 'measure' | 'section' | 'annotate' | 'addElement' | 'split';
104
99
  type WorkspacePanel = 'script' | 'list' | 'bcf' | 'ids' | 'lens' | 'addElement' | string;
105
100
 
106
- function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
107
- return typeof (file as NativeFileHandle).path === 'string';
108
- }
109
-
110
101
  // #region FIX: Move ToolButton OUTSIDE MainToolbar to prevent recreation on every render
111
102
  // This fixes Radix UI Tooltip's asChild prop becoming stale during re-renders
112
103
  interface ToolButtonProps {
@@ -438,11 +429,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
438
429
  // Listen for programmatic file-load requests (from command palette recent files)
439
430
  useEffect(() => {
440
431
  const handler = (e: Event) => {
441
- const file = (e as CustomEvent<File | NativeFileHandle>).detail;
432
+ const file = (e as CustomEvent<File>).detail;
442
433
  if (file) {
443
- recordRecentFiles([isNativeFileHandle(file)
444
- ? { name: file.name, size: file.size, path: file.path, modifiedMs: file.modifiedMs ?? null }
445
- : { name: file.name, size: file.size }]);
434
+ recordRecentFiles([{ name: file.name, size: file.size }]);
446
435
  void loadFile(file);
447
436
  }
448
437
  };
@@ -450,78 +439,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
450
439
  return () => window.removeEventListener('ifc-lite:load-file', handler);
451
440
  }, [loadFile]);
452
441
 
453
- useEffect(() => {
454
- let cancelled = false;
455
- const sleep = (ms: number) => new Promise((resolve) => globalThis.setTimeout(resolve, ms));
456
-
457
- const waitForViewerToSettle = async (label: string) => {
458
- const timeoutMs = 120_000;
459
- const pollMs = 100;
460
- const start = performance.now();
461
- while (!cancelled) {
462
- const state = useViewerStore.getState();
463
- const meshCount = state.geometryResult?.meshes.length ?? 0;
464
- if (!state.loading && meshCount > 0) {
465
- void logToDesktopTerminal(
466
- 'info',
467
- `[DesktopHarness] ${label} settled: loading=${state.loading} meshes=${meshCount} progress=${state.progress?.phase ?? 'none'}`
468
- );
469
- return;
470
- }
471
- if (performance.now() - start >= timeoutMs) {
472
- throw new Error(`[DesktopHarness] Timed out waiting for ${label} to settle`);
473
- }
474
- await sleep(pollMs);
475
- }
476
- };
477
-
478
- void (async () => {
479
- void logToDesktopTerminal('info', '[DesktopHarness] MainToolbar startup harness effect running');
480
- const request = await getStartupHarnessRequest();
481
- if (!request || cancelled) {
482
- void logToDesktopTerminal(
483
- 'info',
484
- `[DesktopHarness] No startup harness request available (cancelled=${cancelled})`
485
- );
486
- return;
487
- }
488
- if (!tryClaimStartupHarnessRequest(request)) {
489
- void logToDesktopTerminal('info', `[DesktopHarness] Startup harness request already claimed for ${request.file.path}`);
490
- return;
491
- }
492
- void logToDesktopTerminal('info', `[DesktopHarness] Claimed startup harness request for ${request.file.path}`);
493
- console.log(`[DesktopHarness] Auto-loading startup file: ${request.file.path}`);
494
- if (!request.replaceFile) {
495
- void logToDesktopTerminal('info', `[DesktopHarness] Calling loadFile for ${request.file.path}`);
496
- await loadFile(request.file);
497
- return;
498
- }
499
-
500
- void logToDesktopTerminal(
501
- 'info',
502
- `[DesktopHarness] Running replacement sequence first=${request.file.path} second=${request.replaceFile.path}`
503
- );
504
- setActiveHarnessRequest(null);
505
- await loadFile(request.file);
506
- await waitForViewerToSettle(`first load ${request.file.name}`);
507
- if (cancelled) {
508
- return;
509
- }
510
-
511
- setActiveHarnessRequest({
512
- ...request,
513
- file: request.replaceFile,
514
- replaceFile: undefined,
515
- });
516
- void logToDesktopTerminal('info', `[DesktopHarness] Calling replacement loadFile for ${request.replaceFile.path}`);
517
- await loadFile(request.replaceFile);
518
- })();
519
-
520
- return () => {
521
- cancelled = true;
522
- };
523
- }, [loadFile]);
524
-
525
442
  // Floorplan view
526
443
  const { availableStoreys, activateFloorplan } = useFloorplanView();
527
444
 
@@ -541,6 +458,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
541
458
  const typeVisibility = useViewerStore((state) => state.typeVisibility);
542
459
  const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
543
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);
544
465
  // How many of the five class toggles are on — surfaced in the menu
545
466
  // header so the user sees scene state at a glance.
546
467
  const visibleClassCount = [
@@ -591,7 +512,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
591
512
  const cesiumPlacementEditMode = useViewerStore((state) => state.cesiumPlacementEditMode);
592
513
  const setCesiumPlacementEditMode = useViewerStore((state) => state.setCesiumPlacementEditMode);
593
514
  const storeModels = useViewerStore((state) => state.models);
594
- const desktopEntitlement = useViewerStore((state) => state.desktopEntitlement);
595
515
  const analysisExtensionState = useSyncExternalStore(
596
516
  subscribeAnalysisExtensions,
597
517
  getAnalysisExtensionsSnapshot,
@@ -609,7 +529,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
609
529
  () => analysisExtensionState.extensions.filter((extension) => (extension.placement ?? 'right') === 'bottom'),
610
530
  [analysisExtensionState.extensions],
611
531
  );
612
- const desktopShell = isTauri();
613
532
 
614
533
  // NOTE: The Class Visibility dropdown used to gate each toggle on whether
615
534
  // the loaded model actually contained that class (scanning meshes for
@@ -725,19 +644,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
725
644
  goHomeFromStore();
726
645
  }, []);
727
646
 
728
- const promptDesktopUpgrade = useCallback((featureLabel: string) => {
729
- toast.info(`${featureLabel} is available with Desktop Pro`);
730
- navigateToPath(buildDesktopUpgradeUrl());
731
- }, []);
732
-
733
- const requireDesktopFeature = useCallback((feature: DesktopFeature, label: string) => {
734
- if (hasDesktopFeatureAccess(desktopEntitlement, feature)) {
735
- return true;
736
- }
737
- promptDesktopUpgrade(label);
738
- return false;
739
- }, [desktopEntitlement, promptDesktopUpgrade]);
740
-
741
647
  const handleToggleBottomPanel = useCallback((panel: 'script' | 'list' | 'gantt') => {
742
648
  if (activeAnalysisExtension?.placement === 'bottom') {
743
649
  closeActiveAnalysisExtension();
@@ -768,15 +674,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
768
674
  if (activeAnalysisExtension?.placement !== 'bottom') {
769
675
  closeActiveAnalysisExtension();
770
676
  }
771
- if (panel === 'bcf' && !requireDesktopFeature('bcf_issue_management', 'BCF issue management')) {
772
- return;
773
- }
774
- if (panel === 'ids' && !requireDesktopFeature('ids_validation', 'IDS validation')) {
775
- return;
776
- }
777
- if (panel === 'extensions' && !requireDesktopFeature('extensions', 'Extensions')) {
778
- return;
779
- }
780
677
 
781
678
  const nextBcfVisible = panel === 'bcf' ? !bcfPanelVisible : false;
782
679
  const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
@@ -812,7 +709,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
812
709
  extensionsPanelVisible,
813
710
  idsPanelVisible,
814
711
  lensPanelVisible,
815
- requireDesktopFeature,
816
712
  setActiveTool,
817
713
  setBcfPanelVisible,
818
714
  setClashPanelVisible,
@@ -920,7 +816,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
920
816
  }, [activeAnalysisExtension?.label, activeWorkspacePanels]);
921
817
 
922
818
  const handleScreenshot = useCallback(() => {
923
- if (!requireDesktopFeature('exports', 'Exports')) return;
924
819
  const canvas = document.querySelector('canvas');
925
820
  if (!canvas) return;
926
821
  try {
@@ -934,10 +829,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
934
829
  console.error('Screenshot failed:', err);
935
830
  toast.error('Screenshot failed');
936
831
  }
937
- }, [requireDesktopFeature]);
832
+ }, []);
938
833
 
939
834
  const handleExportCSV = useCallback((type: 'entities' | 'properties' | 'quantities' | 'spatial') => {
940
- if (!requireDesktopFeature('exports', 'Exports')) return;
941
835
  if (!ifcDataStore) return;
942
836
  try {
943
837
  const exporter = new CSVExporter(ifcDataStore);
@@ -975,10 +869,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
975
869
  console.error('CSV export failed:', err);
976
870
  toast.error(`CSV export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
977
871
  }
978
- }, [ifcDataStore, requireDesktopFeature]);
872
+ }, [ifcDataStore]);
979
873
 
980
874
  const handleExportJSON = useCallback(() => {
981
- if (!requireDesktopFeature('exports', 'Exports')) return;
982
875
  if (!ifcDataStore) return;
983
876
  try {
984
877
  const entities: Record<string, unknown>[] = [];
@@ -1006,7 +899,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1006
899
  console.error('JSON export failed:', err);
1007
900
  toast.error(`JSON export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
1008
901
  }
1009
- }, [ifcDataStore, requireDesktopFeature]);
902
+ }, [ifcDataStore]);
1010
903
 
1011
904
  return (
1012
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">
@@ -1034,25 +927,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1034
927
  <Button
1035
928
  variant="ghost"
1036
929
  size="icon-sm"
1037
- onClick={async (e) => {
930
+ onClick={(e) => {
1038
931
  // Blur button to close tooltip before opening file dialog
1039
932
  (e.currentTarget as HTMLButtonElement).blur();
1040
-
1041
- void logToDesktopTerminal('info', '[MainToolbar] Open file button clicked');
1042
- const file = await openIfcFileDialog();
1043
- if (file) {
1044
- void logToDesktopTerminal('info', `[MainToolbar] Native dialog selected ${file.path}`);
1045
- recordRecentFiles([{
1046
- name: file.name,
1047
- size: file.size,
1048
- path: file.path,
1049
- modifiedMs: file.modifiedMs ?? null,
1050
- }]);
1051
- void loadFile(file);
1052
- return;
1053
- }
1054
-
1055
- void logToDesktopTerminal('info', '[MainToolbar] Falling back to browser file input');
1056
933
  fileInputRef.current?.click();
1057
934
  }}
1058
935
  disabled={loading}
@@ -1095,37 +972,23 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1095
972
  </Button>
1096
973
  </DropdownMenuTrigger>
1097
974
  <DropdownMenuContent>
1098
- {hasDesktopFeatureAccess(desktopEntitlement, 'exports') ? (
1099
- <ExportDialog
1100
- trigger={
1101
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
1102
- <FileText className="h-4 w-4 mr-2" />
1103
- Export IFC (with changes)
1104
- </DropdownMenuItem>
1105
- }
1106
- />
1107
- ) : (
1108
- <DropdownMenuItem onClick={() => promptDesktopUpgrade('Exports')}>
1109
- <FileText className="h-4 w-4 mr-2" />
1110
- Export IFC (with changes)
1111
- </DropdownMenuItem>
1112
- )}
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
+ />
1113
983
  <DropdownMenuSeparator />
1114
- {hasDesktopFeatureAccess(desktopEntitlement, 'exports') ? (
1115
- <GLBExportDialog
1116
- trigger={
1117
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
1118
- <Download className="h-4 w-4 mr-2" />
1119
- Export GLB (3D Model)
1120
- </DropdownMenuItem>
1121
- }
1122
- />
1123
- ) : (
1124
- <DropdownMenuItem onClick={() => promptDesktopUpgrade('Exports')}>
1125
- <Download className="h-4 w-4 mr-2" />
1126
- Export GLB (3D Model)
1127
- </DropdownMenuItem>
1128
- )}
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
+ />
1129
992
  <DropdownMenuSeparator />
1130
993
  <DropdownMenuSub>
1131
994
  <DropdownMenuSubTrigger disabled={!ifcDataStore}>
@@ -1197,9 +1060,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1197
1060
  </DropdownMenu>
1198
1061
 
1199
1062
  {/* Export Changes Button - shows when there are pending mutations */}
1200
- {hasDesktopFeatureAccess(desktopEntitlement, 'exports') ? (
1201
- <ExportChangesButton />
1202
- ) : null}
1063
+ <ExportChangesButton />
1203
1064
 
1204
1065
  {/* ── Panels ── */}
1205
1066
  <DropdownMenu>
@@ -1549,6 +1410,51 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1549
1410
  model lacks is a no-op.
1550
1411
  */}
1551
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
+
1552
1458
  <div className="flex items-center justify-between gap-2 px-1.5 pb-1 pt-0.5">
1553
1459
  <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
1554
1460
  Visibility
@@ -1638,7 +1544,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1638
1544
  appears beside it (its amber tint signals a modal pose whose
1639
1545
  exit affordance must stay visible).
1640
1546
  */}
1641
- {cesiumAvailable && !desktopShell && (
1547
+ {cesiumAvailable && (
1642
1548
  <>
1643
1549
  <Tooltip>
1644
1550
  <TooltipTrigger asChild>
@@ -1800,22 +1706,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1800
1706
  the toolbar's meta cluster stays focused on shell chrome
1801
1707
  (Settings · Theme · Help). */}
1802
1708
  <div className="flex items-center gap-2 ml-2 pl-2 border-l border-zinc-200 dark:border-zinc-700/60">
1803
- {desktopShell ? (
1804
- <Tooltip>
1805
- <TooltipTrigger asChild>
1806
- <Button
1807
- variant="ghost"
1808
- size="icon"
1809
- className="rounded-full"
1810
- onClick={() => navigateToPath('/settings')}
1811
- >
1812
- <Settings className="!h-[20px] !w-[20px]" />
1813
- </Button>
1814
- </TooltipTrigger>
1815
- <TooltipContent>Settings</TooltipContent>
1816
- </Tooltip>
1817
- ) : null}
1818
-
1819
1709
  <Tooltip>
1820
1710
  <TooltipTrigger asChild>
1821
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}