@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
package/src/App.tsx CHANGED
@@ -9,12 +9,10 @@
9
9
  * at boot, react to popstate, and switch by prefix. The handful of routes:
10
10
  *
11
11
  * / → main WebGL viewer (default)
12
- * /settings → desktop-shell account / API-key management (Tauri)
13
12
  * /mcp[/...] → @ifc-lite/mcp marketing surface
14
13
  */
15
14
 
16
15
  import { ViewerLayout } from './components/viewer/ViewerLayout';
17
- import { SettingsPage } from './components/viewer/SettingsPage';
18
16
  import { McpLanding } from './components/mcp/McpLanding';
19
17
  import { McpPlayground } from './components/mcp/McpPlayground';
20
18
  import { BimProvider } from './sdk/BimProvider';
@@ -66,7 +64,7 @@ export function App() {
66
64
  return (
67
65
  <BimProvider>
68
66
  <ExtensionHostProvider>
69
- {pathname === '/settings' ? <SettingsPage /> : <ViewerLayout />}
67
+ <ViewerLayout />
70
68
  <Toaster />
71
69
  <Analytics />
72
70
  </ExtensionHostProvider>
@@ -13,7 +13,7 @@
13
13
  * - Import/export BCF files
14
14
  */
15
15
 
16
- import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
16
+ import React, { useCallback, useState, useMemo, useRef } from 'react';
17
17
  import {
18
18
  X,
19
19
  MessageSquare,
@@ -39,7 +39,6 @@ import { BCFTopicList } from './bcf/BCFTopicList';
39
39
  import { BCFTopicDetail } from './bcf/BCFTopicDetail';
40
40
  import { BCFCreateTopicForm } from './bcf/BCFCreateTopicForm';
41
41
  import { openGenericFileDialog } from '@/services/file-dialog';
42
- import { claimNextDesktopPanelAction, subscribeDesktopPanelActions } from '@/services/desktop-panel-actions';
43
42
 
44
43
  // ============================================================================
45
44
  // Main BCF Panel Component
@@ -298,20 +297,6 @@ export function BCFPanel({ onClose }: BCFPanelProps) {
298
297
  setShowAuthorDialog(false);
299
298
  }, [tempAuthor, setBcfAuthor]);
300
299
 
301
- useEffect(() => {
302
- const drainDesktopActions = () => {
303
- if (claimNextDesktopPanelAction('bcf-import')) {
304
- void importFromDialog();
305
- }
306
- if (claimNextDesktopPanelAction('bcf-export')) {
307
- void handleExport();
308
- }
309
- };
310
-
311
- drainDesktopActions();
312
- return subscribeDesktopPanelActions(drainDesktopActions);
313
- }, [handleExport, importFromDialog]);
314
-
315
300
  return (
316
301
  <div className="flex flex-col h-full bg-background">
317
302
  {/* Header */}
@@ -52,7 +52,6 @@ import { buildRepairSessionKey, getEscalatedRepairScope, pruneMessagesForRepair
52
52
  import type { ChatMessage, ChatRepairRequest, FileAttachment } from '@/lib/llm/types';
53
53
  import { canUsePlainCodeBlockFallback, type ScriptMutationIntent } from '@/lib/llm/script-preservation';
54
54
  import { Check, Image as ImageIcon, KeyRound } from 'lucide-react';
55
- import { hasDesktopFeatureAccess } from '@/lib/desktop-product';
56
55
  import { getModelById } from '@/lib/llm/models';
57
56
  import { resolveStreamRoute } from '@/lib/llm/byok-guard';
58
57
  import { getApiKeys, hasAnthropicKey, hasOpenaiKey, subscribeApiKeys } from '@/services/api-keys';
@@ -300,9 +299,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
300
299
  const setChatHasByokKey = useViewerStore((s) => s.setChatHasByokKey);
301
300
  const usage = useViewerStore((s) => s.chatUsage);
302
301
  const setChatUsage = useViewerStore((s) => s.setChatUsage);
303
- const desktopEntitlement = useViewerStore((s) => s.desktopEntitlement);
304
302
  const { execute } = useSandbox();
305
- const canUseAiAssistant = hasDesktopFeatureAccess(desktopEntitlement, 'ai_assistant');
306
303
 
307
304
  // Sync BYOK key availability into the store and track per-provider state
308
305
  const [keyStateAnthropic, setKeyStateAnthropic] = useState(hasAnthropicKey);
@@ -349,10 +346,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
349
346
  const [showScrollBtn, setShowScrollBtn] = useState(false);
350
347
  const [userScrolledUp, setUserScrolledUp] = useState(false);
351
348
  const [lastFinishReason, setLastFinishReason] = useState<string | null>(null);
352
- const promptAiUpgrade = useCallback(() => {
353
- setChatError('AI assistant is available with Desktop Pro.');
354
- toast.info('AI assistant is available with Desktop Pro');
355
- }, [setChatError]);
356
349
 
357
350
  const inputRef = useRef<HTMLTextAreaElement>(null);
358
351
  const scrollRef = useRef<HTMLDivElement>(null);
@@ -505,10 +498,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
505
498
  // ── Core send logic ──
506
499
  const doSend = useCallback(async (text: string, options?: ChatSendOptions) => {
507
500
  if (!text.trim() || status === 'streaming' || status === 'sending') return;
508
- if (!canUseAiAssistant) {
509
- setChatError('AI assistant is available with Desktop Pro.');
510
- return;
511
- }
512
501
  // Clear any stale post-authoring CTA — this turn re-establishes it
513
502
  // on completion if it's another authoring turn.
514
503
  setChatToolReady(null);
@@ -1056,7 +1045,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1056
1045
  }
1057
1046
  }
1058
1047
  }, [
1059
- canUseAiAssistant, status, activeModel, attachments,
1048
+ status, activeModel, attachments,
1060
1049
  addMessage, setChatStatus, updateStreaming, finalizeAssistant,
1061
1050
  setChatError, setChatAbortController, clearAttachments, setChatUsage, resizeInput,
1062
1051
  buildRepairPromptFromLiveState, triggerAutoRepair, execute, extensionHost,
@@ -1064,12 +1053,8 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1064
1053
  ]);
1065
1054
 
1066
1055
  const handleSend = useCallback(() => {
1067
- if (!canUseAiAssistant) {
1068
- promptAiUpgrade();
1069
- return;
1070
- }
1071
1056
  doSend(inputText);
1072
- }, [canUseAiAssistant, doSend, inputText, promptAiUpgrade]);
1057
+ }, [doSend, inputText]);
1073
1058
 
1074
1059
  // Allow other panels (e.g. ScriptPanel errors) to trigger a chat repair turn.
1075
1060
  useEffect(() => {
@@ -1098,10 +1083,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1098
1083
  }, [pendingRepairRequest, status, consumePendingRepairRequest, buildRepairPromptFromLiveState, doSend]);
1099
1084
 
1100
1085
  const handleContinue = useCallback(() => {
1101
- if (!canUseAiAssistant) {
1102
- promptAiUpgrade();
1103
- return;
1104
- }
1105
1086
  const state = useViewerStore.getState();
1106
1087
  const partial = state.chatStreamingContent.trim();
1107
1088
  const lastAssistant = [...state.chatMessages].reverse().find((m) => m.role === 'assistant');
@@ -1114,7 +1095,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1114
1095
  }
1115
1096
  setChatError(null);
1116
1097
  doSend(CONTINUE_PROMPT, { continuationBase });
1117
- }, [canUseAiAssistant, doSend, finalizeAssistant, promptAiUpgrade, setChatError]);
1098
+ }, [doSend, finalizeAssistant, setChatError]);
1118
1099
 
1119
1100
  const handleStop = useCallback(() => {
1120
1101
  const controller = useViewerStore.getState().chatAbortController;
@@ -1139,10 +1120,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1139
1120
 
1140
1121
  // ── Error feedback (Fix this) ──
1141
1122
  const handleFixError = useCallback((code: string, errorMsg: string) => {
1142
- if (!canUseAiAssistant) {
1143
- promptAiUpgrade();
1144
- return;
1145
- }
1146
1123
  const diagnostics = useViewerStore.getState().scriptLastDiagnostics;
1147
1124
  const liveCode = useViewerStore.getState().scriptEditorContent;
1148
1125
  const staleCode = code.trim() !== liveCode.trim() ? code : undefined;
@@ -1157,7 +1134,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1157
1134
  requestedRepairScope: getPrimaryRootCause(diagnostics)?.repairScope,
1158
1135
  rootCauseKey: getPrimaryRootCause(diagnostics)?.rootCauseKey,
1159
1136
  });
1160
- }, [buildRepairPromptFromLiveState, canUseAiAssistant, doSend, promptAiUpgrade]);
1137
+ }, [buildRepairPromptFromLiveState, doSend]);
1161
1138
 
1162
1139
  // ── Clickable example prompts ──
1163
1140
  const handleExampleClick = useCallback((prompt: string) => {
@@ -1191,10 +1168,6 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1191
1168
 
1192
1169
  // ── File upload (button + drag-drop + paste) ──
1193
1170
  const processFiles = useCallback(async (files: FileList | File[]) => {
1194
- if (!canUseAiAssistant) {
1195
- promptAiUpgrade();
1196
- return;
1197
- }
1198
1171
  const model = getModelById(activeModel);
1199
1172
  const supportsImages = model?.supportsImages ?? false;
1200
1173
  const supportsFileAttachments = model?.supportsFileAttachments ?? true;
@@ -1309,7 +1282,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1309
1282
  setChatError(`Could not read ${file.name}: ${error instanceof Error ? error.message : String(error)}`);
1310
1283
  }
1311
1284
  }
1312
- }, [activeModel, addAttachment, attachments.length, canUseAiAssistant, promptAiUpgrade, setChatError]);
1285
+ }, [activeModel, addAttachment, attachments.length, setChatError]);
1313
1286
 
1314
1287
  const handleFileUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
1315
1288
  const files = e.target.files;
@@ -1490,15 +1463,9 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1490
1463
  )}
1491
1464
  </div>
1492
1465
 
1493
- {!canUseAiAssistant && (
1494
- <div className="border-b bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
1495
- AI assistant requires Desktop Pro. Core viewing and scripting stay available without it.
1496
- </div>
1497
- )}
1498
-
1499
1466
  {/* Slim CTA banner — appears when the modal has been dismissed but the
1500
1467
  selected model still needs a key. Re-opens the modal on click. */}
1501
- {needsByokKey && canUseAiAssistant && !byokModal.open && (
1468
+ {needsByokKey && !byokModal.open && (
1502
1469
  <button
1503
1470
  type="button"
1504
1471
  onClick={() => openByokModal(needsAnthropicKey ? 'anthropic' : 'openai')}
@@ -1734,16 +1701,14 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1734
1701
  variant="ghost"
1735
1702
  size="icon-xs"
1736
1703
  onClick={() => fileInputRef.current?.click()}
1737
- disabled={!canAttachInput || !canUseAiAssistant}
1704
+ disabled={!canAttachInput}
1738
1705
  className="shrink-0 mb-0.5"
1739
1706
  >
1740
1707
  <Paperclip className="h-3.5 w-3.5" />
1741
1708
  </Button>
1742
1709
  </TooltipTrigger>
1743
1710
  <TooltipContent>
1744
- {!canUseAiAssistant
1745
- ? 'AI assistant not available'
1746
- : canAttachInput
1711
+ {canAttachInput
1747
1712
  ? 'Attach file or image (paste, drag & drop)'
1748
1713
  : 'Selected model does not support attachments'}
1749
1714
  </TooltipContent>
@@ -1758,11 +1723,11 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1758
1723
  }}
1759
1724
  onKeyDown={handleKeyDown}
1760
1725
  onPaste={handlePaste}
1761
- placeholder={!canUseAiAssistant ? 'AI assistant not available' : needsByokKey ? `Add your ${needsAnthropicKey ? 'Anthropic' : 'OpenAI'} key to chat with this model` : 'Ask anything...'}
1726
+ placeholder={needsByokKey ? `Add your ${needsAnthropicKey ? 'Anthropic' : 'OpenAI'} key to chat with this model` : 'Ask anything...'}
1762
1727
  rows={1}
1763
1728
  className="flex-1 resize-none rounded-md border border-input bg-background text-foreground placeholder:text-muted-foreground px-3 py-1.5 text-sm min-h-[32px] max-h-[120px] focus:outline-none focus:ring-1 focus:ring-ring"
1764
1729
  style={{ height: 'auto', overflow: 'hidden' }}
1765
- disabled={!canUseAiAssistant || needsByokKey}
1730
+ disabled={needsByokKey}
1766
1731
  />
1767
1732
 
1768
1733
  {isActive ? (
@@ -1786,7 +1751,7 @@ export function ChatPanel({ onClose }: ChatPanelProps) {
1786
1751
  variant="default"
1787
1752
  size="icon-xs"
1788
1753
  onClick={handleSend}
1789
- disabled={!inputText.trim() || !canUseAiAssistant || needsByokKey}
1754
+ disabled={!inputText.trim() || needsByokKey}
1790
1755
  className="shrink-0 mb-0.5"
1791
1756
  >
1792
1757
  <Send className="h-3.5 w-3.5" />
@@ -2,7 +2,7 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
- import { useState, useCallback, useRef, useEffect, useMemo, type ReactElement } from 'react';
5
+ import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
6
6
  import { useVirtualizer } from '@tanstack/react-virtual';
7
7
  import {
8
8
  Search,
@@ -19,7 +19,6 @@ import { cn } from '@/lib/utils';
19
19
  import { useViewerStore, resolveEntityRef } from '@/store';
20
20
  import { toGlobalIdFromModels } from '@/store/globalId';
21
21
  import { useIfc } from '@/hooks/useIfc';
22
- import { getNativeMetadataChildren, searchNativeMetadataEntities } from '@/services/desktop-native-metadata';
23
22
 
24
23
  import type { TreeNode } from './hierarchy/types';
25
24
  import { isSpatialContainer } from './hierarchy/types';
@@ -38,7 +37,6 @@ export function HierarchyPanel() {
38
37
  removeModel,
39
38
  } = useIfc();
40
39
  const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
41
- const selectedEntity = useViewerStore((s) => s.selectedEntity);
42
40
  const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
43
41
  const setSelectedEntityIds = useViewerStore((s) => s.setSelectedEntityIds);
44
42
  const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
@@ -92,33 +90,6 @@ export function HierarchyPanel() {
92
90
 
93
91
  // Check if we have multiple models loaded
94
92
  const isMultiModel = models.size > 1;
95
- const nativeLazyModel = useMemo(() => {
96
- if (models.size !== 1) return null;
97
- const [, model] = Array.from(models.entries())[0];
98
- if (!model.nativeMetadata) return null;
99
- return model.ifcDataStore?.spatialHierarchy ? null : model;
100
- }, [models]);
101
- const [nativeChildren, setNativeChildren] = useState<Record<number, Array<{
102
- expressId: number;
103
- type: string;
104
- name: string;
105
- globalId?: string | null;
106
- kind: 'spatial' | 'element';
107
- hasChildren: boolean;
108
- elementCount?: number;
109
- elevation?: number | null;
110
- }>>>({});
111
- const [nativeExpanded, setNativeExpanded] = useState<Set<number>>(new Set());
112
- const [nativeSearchResults, setNativeSearchResults] = useState<Array<{
113
- expressId: number;
114
- type: string;
115
- name: string;
116
- globalId?: string | null;
117
- kind: 'spatial' | 'element';
118
- hasChildren: boolean;
119
- elementCount?: number;
120
- elevation?: number | null;
121
- }>>([]);
122
93
 
123
94
  // Use extracted hook for tree data management
124
95
  const {
@@ -222,29 +193,6 @@ export function HierarchyPanel() {
222
193
  };
223
194
  }, [isDragging]);
224
195
 
225
- useEffect(() => {
226
- if (!nativeLazyModel?.nativeMetadata) {
227
- setNativeSearchResults([]);
228
- return;
229
- }
230
- const query = searchQuery.trim();
231
- if (!query) {
232
- setNativeSearchResults([]);
233
- return;
234
- }
235
- let cancelled = false;
236
- void searchNativeMetadataEntities(nativeLazyModel.nativeMetadata.cacheKey, query, 200)
237
- .then((results) => {
238
- if (!cancelled) setNativeSearchResults(results);
239
- })
240
- .catch(() => {
241
- if (!cancelled) setNativeSearchResults([]);
242
- });
243
- return () => {
244
- cancelled = true;
245
- };
246
- }, [nativeLazyModel, searchQuery]);
247
-
248
196
  // Toggle visibility for a node
249
197
  const handleVisibilityToggle = useCallback((node: TreeNode) => {
250
198
  const elements = getNodeElements(node);
@@ -560,7 +508,7 @@ export function HierarchyPanel() {
560
508
  }
561
509
 
562
510
  const singleModel = models.size === 1 ? Array.from(models.values())[0] : null;
563
- if (!ifcDataStore && singleModel && !nativeLazyModel) {
511
+ if (!ifcDataStore && singleModel) {
564
512
  const metadataState = singleModel.metadataLoadState;
565
513
  const message = metadataState === 'error'
566
514
  ? (singleModel.loadError || 'Native metadata failed to load.')
@@ -581,128 +529,6 @@ export function HierarchyPanel() {
581
529
  );
582
530
  }
583
531
 
584
- if (nativeLazyModel?.nativeMetadata) {
585
- const nativeMetadata = nativeLazyModel.nativeMetadata;
586
- const nativeSelectedGlobalId =
587
- selectedEntity?.modelId === nativeLazyModel.id
588
- ? toGlobalId(nativeLazyModel.id, selectedEntity.expressId)
589
- : null;
590
-
591
- const selectNativeEntity = (expressId: number) => {
592
- const globalId = toGlobalId(nativeLazyModel.id, expressId);
593
- setSelectedEntityIds([]);
594
- setSelectedEntityId(globalId);
595
- setSelectedEntity({
596
- modelId: nativeLazyModel.id,
597
- expressId,
598
- });
599
- setActiveModel(nativeLazyModel.id);
600
- };
601
-
602
- const toggleNativeNode = async (expressId: number) => {
603
- setNativeExpanded((prev) => {
604
- const next = new Set(prev);
605
- if (next.has(expressId)) {
606
- next.delete(expressId);
607
- } else {
608
- next.add(expressId);
609
- }
610
- return next;
611
- });
612
- if (nativeChildren[expressId]) return;
613
- try {
614
- const children = await getNativeMetadataChildren(nativeMetadata.cacheKey, expressId);
615
- setNativeChildren((prev) => ({ ...prev, [expressId]: children }));
616
- } catch {
617
- setNativeChildren((prev) => ({ ...prev, [expressId]: [] }));
618
- }
619
- };
620
-
621
- const renderNativeSummary = (
622
- summary: {
623
- expressId: number;
624
- type: string;
625
- name: string;
626
- kind: 'spatial' | 'element';
627
- hasChildren: boolean;
628
- elementCount?: number;
629
- },
630
- depth: number,
631
- ): ReactElement => {
632
- const expanded = nativeExpanded.has(summary.expressId);
633
- return (
634
- <div key={`${summary.kind}-${summary.expressId}`}>
635
- <button
636
- type="button"
637
- className={cn(
638
- 'w-full flex items-center gap-2 px-3 py-2 text-left border-b border-zinc-100 dark:border-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-950',
639
- nativeSelectedGlobalId === toGlobalId(nativeLazyModel.id, summary.expressId) && 'bg-primary/10 text-primary'
640
- )}
641
- style={{ paddingLeft: `${12 + depth * 16}px` }}
642
- onClick={() => selectNativeEntity(summary.expressId)}
643
- >
644
- {summary.hasChildren ? (
645
- <span
646
- className="w-4 text-center text-xs text-zinc-500"
647
- onClick={(event) => {
648
- event.stopPropagation();
649
- void toggleNativeNode(summary.expressId);
650
- }}
651
- >
652
- {expanded ? 'v' : '>'}
653
- </span>
654
- ) : (
655
- <span className="w-4" />
656
- )}
657
- <span className="truncate flex-1 text-sm">{summary.name || `${summary.type} #${summary.expressId}`}</span>
658
- <span className="text-[10px] uppercase tracking-wide text-zinc-500">{summary.type}</span>
659
- {typeof summary.elementCount === 'number' && summary.elementCount > 0 && (
660
- <span className="text-[10px] text-zinc-400">{summary.elementCount}</span>
661
- )}
662
- </button>
663
- {expanded && (nativeChildren[summary.expressId] ?? []).map((child) => renderNativeSummary(child, depth + 1))}
664
- </div>
665
- );
666
- };
667
-
668
- return (
669
- <div className="h-full flex flex-col border-r-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
670
- <div className="p-3 border-b-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black">
671
- <Input
672
- placeholder="Search..."
673
- value={searchQuery}
674
- onChange={(e) => setSearchQuery(e.target.value)}
675
- leftIcon={<Search className="h-4 w-4" />}
676
- className="h-9 text-sm rounded-none border-2 border-zinc-200 dark:border-zinc-800 focus:border-primary focus:ring-0 bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-600"
677
- />
678
- </div>
679
- <SectionHeader
680
- icon={Building2}
681
- title={searchQuery.trim() ? 'Search Results' : 'Hierarchy'}
682
- count={searchQuery.trim() ? nativeSearchResults.length : 1}
683
- />
684
- <div className="flex-1 overflow-auto scrollbar-thin bg-white dark:bg-black">
685
- {searchQuery.trim()
686
- ? nativeSearchResults.map((result) => renderNativeSummary(result, 0))
687
- : nativeMetadata.spatialTree
688
- ? renderNativeSummary(nativeMetadata.spatialTree, 0)
689
- : (
690
- <div className="p-4 text-xs text-zinc-500">
691
- {nativeLazyModel.metadataLoadState === 'error'
692
- ? (nativeLazyModel.loadError || 'Native spatial metadata is unavailable for this model.')
693
- : nativeLazyModel.metadataLoadState === 'bootstrapping'
694
- ? 'Native spatial metadata is still loading.'
695
- : 'Native spatial metadata tree is unavailable for this model.'}
696
- </div>
697
- )}
698
- </div>
699
- <div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 text-[10px] uppercase tracking-wide text-zinc-500 dark:text-zinc-500 text-center bg-zinc-50 dark:bg-black font-mono">
700
- On-demand desktop metadata
701
- </div>
702
- </div>
703
- );
704
- }
705
-
706
532
  // Helper to render a node via the extracted HierarchyNode component
707
533
  const renderNode = (node: TreeNode, virtualRow: { index: number; size: number; start: number }) => {
708
534
  const { isSelected, nodeHidden, modelVisible } = computeNodeState(node);
@@ -15,7 +15,7 @@
15
15
  * - Multi-language support (EN/DE/FR)
16
16
  */
17
17
 
18
- import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
18
+ import React, { useCallback, useState, useMemo, useRef } from 'react';
19
19
  import {
20
20
  X,
21
21
  Upload,
@@ -75,7 +75,6 @@ import { cn } from '@/lib/utils';
75
75
  import { IDSAuditSummary } from './IDSAuditSummary';
76
76
  import { IDSExportDialog } from './IDSExportDialog';
77
77
  import type { IDSBCFExportSettings, IDSExportProgress } from './IDSExportDialog';
78
- import { claimNextDesktopPanelAction, subscribeDesktopPanelActions } from '@/services/desktop-panel-actions';
79
78
 
80
79
  // ============================================================================
81
80
  // Types
@@ -513,30 +512,6 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
513
512
  fileInputRef.current?.click();
514
513
  }, [loadIdsFromDialog]);
515
514
 
516
- const handleDesktopRunValidation = useCallback(async () => {
517
- if (!document) {
518
- const loaded = await loadIdsFromDialog();
519
- if (!loaded) {
520
- return;
521
- }
522
- }
523
- await runValidation();
524
- }, [document, loadIdsFromDialog, runValidation]);
525
-
526
- useEffect(() => {
527
- const drainDesktopActions = () => {
528
- if (claimNextDesktopPanelAction('ids-open')) {
529
- void loadIdsFromDialog();
530
- }
531
- if (claimNextDesktopPanelAction('ids-run-validation')) {
532
- void handleDesktopRunValidation();
533
- }
534
- };
535
-
536
- drainDesktopActions();
537
- return subscribeDesktopPanelActions(drainDesktopActions);
538
- }, [handleDesktopRunValidation, loadIdsFromDialog]);
539
-
540
515
  // Handle entity click
541
516
  const handleEntityClick = useCallback((modelId: string, expressId: number) => {
542
517
  selectEntity(modelId, expressId);