@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
@@ -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,
@@ -11,6 +11,7 @@ import {
11
11
  LayoutTemplate,
12
12
  FileBox,
13
13
  GripHorizontal,
14
+ Palette,
14
15
  } from 'lucide-react';
15
16
  import { Input } from '@/components/ui/input';
16
17
  import { Button } from '@/components/ui/button';
@@ -18,7 +19,6 @@ import { cn } from '@/lib/utils';
18
19
  import { useViewerStore, resolveEntityRef } from '@/store';
19
20
  import { toGlobalIdFromModels } from '@/store/globalId';
20
21
  import { useIfc } from '@/hooks/useIfc';
21
- import { getNativeMetadataChildren, searchNativeMetadataEntities } from '@/services/desktop-native-metadata';
22
22
 
23
23
  import type { TreeNode } from './hierarchy/types';
24
24
  import { isSpatialContainer } from './hierarchy/types';
@@ -37,7 +37,6 @@ export function HierarchyPanel() {
37
37
  removeModel,
38
38
  } = useIfc();
39
39
  const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
40
- const selectedEntity = useViewerStore((s) => s.selectedEntity);
41
40
  const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
42
41
  const setSelectedEntityIds = useViewerStore((s) => s.setSelectedEntityIds);
43
42
  const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
@@ -91,33 +90,6 @@ export function HierarchyPanel() {
91
90
 
92
91
  // Check if we have multiple models loaded
93
92
  const isMultiModel = models.size > 1;
94
- const nativeLazyModel = useMemo(() => {
95
- if (models.size !== 1) return null;
96
- const [, model] = Array.from(models.entries())[0];
97
- if (!model.nativeMetadata) return null;
98
- return model.ifcDataStore?.spatialHierarchy ? null : model;
99
- }, [models]);
100
- const [nativeChildren, setNativeChildren] = useState<Record<number, Array<{
101
- expressId: number;
102
- type: string;
103
- name: string;
104
- globalId?: string | null;
105
- kind: 'spatial' | 'element';
106
- hasChildren: boolean;
107
- elementCount?: number;
108
- elevation?: number | null;
109
- }>>>({});
110
- const [nativeExpanded, setNativeExpanded] = useState<Set<number>>(new Set());
111
- const [nativeSearchResults, setNativeSearchResults] = useState<Array<{
112
- expressId: number;
113
- type: string;
114
- name: string;
115
- globalId?: string | null;
116
- kind: 'spatial' | 'element';
117
- hasChildren: boolean;
118
- elementCount?: number;
119
- elevation?: number | null;
120
- }>>([]);
121
93
 
122
94
  // Use extracted hook for tree data management
123
95
  const {
@@ -221,29 +193,6 @@ export function HierarchyPanel() {
221
193
  };
222
194
  }, [isDragging]);
223
195
 
224
- useEffect(() => {
225
- if (!nativeLazyModel?.nativeMetadata) {
226
- setNativeSearchResults([]);
227
- return;
228
- }
229
- const query = searchQuery.trim();
230
- if (!query) {
231
- setNativeSearchResults([]);
232
- return;
233
- }
234
- let cancelled = false;
235
- void searchNativeMetadataEntities(nativeLazyModel.nativeMetadata.cacheKey, query, 200)
236
- .then((results) => {
237
- if (!cancelled) setNativeSearchResults(results);
238
- })
239
- .catch(() => {
240
- if (!cancelled) setNativeSearchResults([]);
241
- });
242
- return () => {
243
- cancelled = true;
244
- };
245
- }, [nativeLazyModel, searchQuery]);
246
-
247
196
  // Toggle visibility for a node
248
197
  const handleVisibilityToggle = useCallback((node: TreeNode) => {
249
198
  const elements = getNodeElements(node);
@@ -322,6 +271,34 @@ export function HierarchyPanel() {
322
271
  return;
323
272
  }
324
273
 
274
+ // Material group nodes (Materials tab) - select the material entity for the
275
+ // totals panel + isolate the elements that use it.
276
+ if (node.type === 'material-group') {
277
+ const modelId = node.modelIds[0];
278
+ const materialExpressId = node.entityExpressId;
279
+
280
+ // Clear multi-selection first (setSelectedEntityIds([]) resets selectedEntityId)
281
+ setSelectedEntityIds([]);
282
+
283
+ if (materialExpressId !== undefined) {
284
+ if (modelId && modelId !== 'legacy') {
285
+ setSelectedEntityId(toGlobalId(modelId, materialExpressId));
286
+ setSelectedEntity({ modelId, expressId: materialExpressId });
287
+ setActiveModel(modelId);
288
+ } else {
289
+ setSelectedEntityId(materialExpressId);
290
+ setSelectedEntity({ modelId: 'legacy', expressId: materialExpressId });
291
+ }
292
+ }
293
+
294
+ // Isolate the elements using this material
295
+ const elements = getNodeElements(node);
296
+ if (elements.length > 0) {
297
+ isolateEntities(elements);
298
+ }
299
+ return;
300
+ }
301
+
325
302
  // IFC type entity nodes (e.g. IfcWallType/W01) - select type entity for property panel + isolate instances
326
303
  if (node.type === 'ifc-type') {
327
304
  const modelId = node.modelIds[0];
@@ -478,14 +455,14 @@ export function HierarchyPanel() {
478
455
  ? selectedStoreys.has(node.expressIds[0])
479
456
  : node.type === 'IfcSpace' || node.type === 'element'
480
457
  ? selectedEntityId === (node.globalIds[0] ?? node.expressIds[0])
481
- : node.type === 'ifc-type'
458
+ : node.type === 'ifc-type' || node.type === 'material-group'
482
459
  ? (() => {
483
- const typeExpressId = node.entityExpressId;
484
- if (!typeExpressId) return false;
460
+ const entityExpressId = node.entityExpressId;
461
+ if (!entityExpressId) return false;
485
462
  const mId = node.modelIds[0];
486
463
  const gId = mId && mId !== 'legacy'
487
- ? toGlobalId(mId, typeExpressId)
488
- : typeExpressId;
464
+ ? toGlobalId(mId, entityExpressId)
465
+ : entityExpressId;
489
466
  return selectedEntityId === gId;
490
467
  })()
491
468
  : false;
@@ -495,7 +472,7 @@ export function HierarchyPanel() {
495
472
  if (node.type === 'element') {
496
473
  nodeHidden = hiddenEntities.has(node.globalIds[0] ?? node.expressIds[0]);
497
474
  } else if (node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' ||
498
- node.type === 'type-group' || node.type === 'ifc-type' ||
475
+ node.type === 'type-group' || node.type === 'ifc-type' || node.type === 'material-group' ||
499
476
  (node.type === 'model-header' && node.id.startsWith('contrib-'))) {
500
477
  const elements = getNodeElements(node);
501
478
  nodeHidden = elements.length > 0 && elements.every(id => hiddenEntities.has(id));
@@ -531,7 +508,7 @@ export function HierarchyPanel() {
531
508
  }
532
509
 
533
510
  const singleModel = models.size === 1 ? Array.from(models.values())[0] : null;
534
- if (!ifcDataStore && singleModel && !nativeLazyModel) {
511
+ if (!ifcDataStore && singleModel) {
535
512
  const metadataState = singleModel.metadataLoadState;
536
513
  const message = metadataState === 'error'
537
514
  ? (singleModel.loadError || 'Native metadata failed to load.')
@@ -552,128 +529,6 @@ export function HierarchyPanel() {
552
529
  );
553
530
  }
554
531
 
555
- if (nativeLazyModel?.nativeMetadata) {
556
- const nativeMetadata = nativeLazyModel.nativeMetadata;
557
- const nativeSelectedGlobalId =
558
- selectedEntity?.modelId === nativeLazyModel.id
559
- ? toGlobalId(nativeLazyModel.id, selectedEntity.expressId)
560
- : null;
561
-
562
- const selectNativeEntity = (expressId: number) => {
563
- const globalId = toGlobalId(nativeLazyModel.id, expressId);
564
- setSelectedEntityIds([]);
565
- setSelectedEntityId(globalId);
566
- setSelectedEntity({
567
- modelId: nativeLazyModel.id,
568
- expressId,
569
- });
570
- setActiveModel(nativeLazyModel.id);
571
- };
572
-
573
- const toggleNativeNode = async (expressId: number) => {
574
- setNativeExpanded((prev) => {
575
- const next = new Set(prev);
576
- if (next.has(expressId)) {
577
- next.delete(expressId);
578
- } else {
579
- next.add(expressId);
580
- }
581
- return next;
582
- });
583
- if (nativeChildren[expressId]) return;
584
- try {
585
- const children = await getNativeMetadataChildren(nativeMetadata.cacheKey, expressId);
586
- setNativeChildren((prev) => ({ ...prev, [expressId]: children }));
587
- } catch {
588
- setNativeChildren((prev) => ({ ...prev, [expressId]: [] }));
589
- }
590
- };
591
-
592
- const renderNativeSummary = (
593
- summary: {
594
- expressId: number;
595
- type: string;
596
- name: string;
597
- kind: 'spatial' | 'element';
598
- hasChildren: boolean;
599
- elementCount?: number;
600
- },
601
- depth: number,
602
- ): ReactElement => {
603
- const expanded = nativeExpanded.has(summary.expressId);
604
- return (
605
- <div key={`${summary.kind}-${summary.expressId}`}>
606
- <button
607
- type="button"
608
- className={cn(
609
- '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',
610
- nativeSelectedGlobalId === toGlobalId(nativeLazyModel.id, summary.expressId) && 'bg-primary/10 text-primary'
611
- )}
612
- style={{ paddingLeft: `${12 + depth * 16}px` }}
613
- onClick={() => selectNativeEntity(summary.expressId)}
614
- >
615
- {summary.hasChildren ? (
616
- <span
617
- className="w-4 text-center text-xs text-zinc-500"
618
- onClick={(event) => {
619
- event.stopPropagation();
620
- void toggleNativeNode(summary.expressId);
621
- }}
622
- >
623
- {expanded ? 'v' : '>'}
624
- </span>
625
- ) : (
626
- <span className="w-4" />
627
- )}
628
- <span className="truncate flex-1 text-sm">{summary.name || `${summary.type} #${summary.expressId}`}</span>
629
- <span className="text-[10px] uppercase tracking-wide text-zinc-500">{summary.type}</span>
630
- {typeof summary.elementCount === 'number' && summary.elementCount > 0 && (
631
- <span className="text-[10px] text-zinc-400">{summary.elementCount}</span>
632
- )}
633
- </button>
634
- {expanded && (nativeChildren[summary.expressId] ?? []).map((child) => renderNativeSummary(child, depth + 1))}
635
- </div>
636
- );
637
- };
638
-
639
- return (
640
- <div className="h-full flex flex-col border-r-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
641
- <div className="p-3 border-b-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black">
642
- <Input
643
- placeholder="Search..."
644
- value={searchQuery}
645
- onChange={(e) => setSearchQuery(e.target.value)}
646
- leftIcon={<Search className="h-4 w-4" />}
647
- 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"
648
- />
649
- </div>
650
- <SectionHeader
651
- icon={Building2}
652
- title={searchQuery.trim() ? 'Search Results' : 'Hierarchy'}
653
- count={searchQuery.trim() ? nativeSearchResults.length : 1}
654
- />
655
- <div className="flex-1 overflow-auto scrollbar-thin bg-white dark:bg-black">
656
- {searchQuery.trim()
657
- ? nativeSearchResults.map((result) => renderNativeSummary(result, 0))
658
- : nativeMetadata.spatialTree
659
- ? renderNativeSummary(nativeMetadata.spatialTree, 0)
660
- : (
661
- <div className="p-4 text-xs text-zinc-500">
662
- {nativeLazyModel.metadataLoadState === 'error'
663
- ? (nativeLazyModel.loadError || 'Native spatial metadata is unavailable for this model.')
664
- : nativeLazyModel.metadataLoadState === 'bootstrapping'
665
- ? 'Native spatial metadata is still loading.'
666
- : 'Native spatial metadata tree is unavailable for this model.'}
667
- </div>
668
- )}
669
- </div>
670
- <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">
671
- On-demand desktop metadata
672
- </div>
673
- </div>
674
- );
675
- }
676
-
677
532
  // Helper to render a node via the extracted HierarchyNode component
678
533
  const renderNode = (node: TreeNode, virtualRow: { index: number; size: number; start: number }) => {
679
534
  const { isSelected, nodeHidden, modelVisible } = computeNodeState(node);
@@ -732,6 +587,16 @@ export function HierarchyPanel() {
732
587
  <FileBox className="h-3 w-3 shrink-0 panel-compact-icon" />
733
588
  <span className="panel-compact-text">Type</span>
734
589
  </Button>
590
+ <Button
591
+ variant={groupingMode === 'material' ? 'default' : 'outline'}
592
+ size="sm"
593
+ className="h-6 text-[10px] flex-1 min-w-0 rounded-none uppercase tracking-wider"
594
+ onClick={() => setGroupingMode('material')}
595
+ title="Materials"
596
+ >
597
+ <Palette className="h-3 w-3 shrink-0 panel-compact-icon" />
598
+ <span className="panel-compact-text">Material</span>
599
+ </Button>
735
600
  </div>
736
601
  );
737
602
 
@@ -871,7 +736,7 @@ export function HierarchyPanel() {
871
736
  </div>
872
737
 
873
738
  {/* Section Header */}
874
- <SectionHeader icon={groupingMode === 'spatial' ? Building2 : groupingMode === 'type' ? Layers : FileBox} title={groupingMode === 'spatial' ? 'Hierarchy' : groupingMode === 'type' ? 'By Class' : 'By Type'} count={filteredNodes.length} />
739
+ <SectionHeader icon={groupingMode === 'spatial' ? Building2 : groupingMode === 'type' ? Layers : groupingMode === 'material' ? Palette : FileBox} title={groupingMode === 'spatial' ? 'Hierarchy' : groupingMode === 'type' ? 'By Class' : groupingMode === 'material' ? 'By Material' : 'By Type'} count={filteredNodes.length} />
875
740
 
876
741
  {/* Tree */}
877
742
  <div ref={parentRef} className="flex-1 overflow-auto scrollbar-thin bg-white dark:bg-black">
@@ -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);