@ifc-lite/viewer 1.17.6 → 1.19.0

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 (156) hide show
  1. package/.turbo/turbo-build.log +20 -15
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +949 -0
  4. package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
  5. package/dist/assets/decode-worker-Collf_X_.js +1320 -0
  6. package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
  7. package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
  8. package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
  9. package/dist/assets/index-0XpVr_S5.css +1 -0
  10. package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
  11. package/dist/assets/index-XwKzDuw6.js +22 -0
  12. package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
  13. package/dist/assets/sandbox-Baez7n-t.js +9682 -0
  14. package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
  15. package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
  16. package/dist/index.html +6 -6
  17. package/package.json +11 -10
  18. package/src/apache-arrow.d.ts +30 -0
  19. package/src/components/viewer/AddElementPanel.tsx +758 -0
  20. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  21. package/src/components/viewer/ChatPanel.tsx +64 -2
  22. package/src/components/viewer/CommandPalette.tsx +56 -7
  23. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  24. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  25. package/src/components/viewer/ExportDialog.tsx +19 -1
  26. package/src/components/viewer/MainToolbar.tsx +73 -12
  27. package/src/components/viewer/PointCloudPanel.tsx +174 -0
  28. package/src/components/viewer/PropertiesPanel.tsx +222 -22
  29. package/src/components/viewer/SearchInline.tsx +669 -0
  30. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  31. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  32. package/src/components/viewer/SearchModal.text.tsx +388 -0
  33. package/src/components/viewer/SearchModal.tsx +235 -0
  34. package/src/components/viewer/ToolOverlays.tsx +5 -0
  35. package/src/components/viewer/ViewerLayout.tsx +24 -4
  36. package/src/components/viewer/Viewport.tsx +29 -2
  37. package/src/components/viewer/ViewportContainer.tsx +45 -5
  38. package/src/components/viewer/ViewportOverlays.tsx +13 -2
  39. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  40. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  41. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  42. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  43. package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
  44. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  45. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  46. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  47. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  48. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  49. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  50. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  51. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  52. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  53. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  54. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  55. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  56. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  57. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  58. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  59. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  60. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  61. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  62. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  63. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  64. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  65. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  66. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  67. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  68. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  69. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  70. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  71. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  72. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  73. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  74. package/src/components/viewer/selectionHandlers.ts +446 -0
  75. package/src/components/viewer/tools/AddElementOverlay.tsx +581 -0
  76. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  77. package/src/components/viewer/useMouseControls.ts +9 -1
  78. package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
  79. package/src/components/viewer/usePointCloudSync.ts +98 -0
  80. package/src/hooks/ingest/pointCloudIngest.ts +391 -0
  81. package/src/hooks/ingest/viewerModelIngest.ts +32 -3
  82. package/src/hooks/useIfcFederation.ts +72 -3
  83. package/src/hooks/useIfcLoader.ts +89 -13
  84. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  85. package/src/hooks/useSandbox.ts +1 -1
  86. package/src/hooks/useSearchIndex.ts +125 -0
  87. package/src/index.css +66 -0
  88. package/src/lib/llm/system-prompt.test.ts +14 -0
  89. package/src/lib/llm/system-prompt.ts +102 -1
  90. package/src/lib/llm/types.ts +6 -0
  91. package/src/lib/recent-files.ts +38 -4
  92. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  93. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  94. package/src/lib/scripts/templates.ts +7 -0
  95. package/src/lib/search/common-ifc-types.ts +36 -0
  96. package/src/lib/search/filter-evaluate.test.ts +537 -0
  97. package/src/lib/search/filter-evaluate.ts +610 -0
  98. package/src/lib/search/filter-rules.test.ts +119 -0
  99. package/src/lib/search/filter-rules.ts +198 -0
  100. package/src/lib/search/filter-schema.test.ts +233 -0
  101. package/src/lib/search/filter-schema.ts +146 -0
  102. package/src/lib/search/recent-searches.test.ts +116 -0
  103. package/src/lib/search/recent-searches.ts +93 -0
  104. package/src/lib/search/result-export.test.ts +101 -0
  105. package/src/lib/search/result-export.ts +104 -0
  106. package/src/lib/search/saved-filters.test.ts +118 -0
  107. package/src/lib/search/saved-filters.ts +154 -0
  108. package/src/lib/search/tier0-scan.test.ts +196 -0
  109. package/src/lib/search/tier0-scan.ts +237 -0
  110. package/src/lib/search/tier1-index.test.ts +242 -0
  111. package/src/lib/search/tier1-index.ts +448 -0
  112. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  113. package/src/sdk/adapters/export-adapter.ts +404 -1
  114. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  115. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  116. package/src/sdk/adapters/model-compat.ts +8 -2
  117. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  118. package/src/sdk/adapters/store-adapter.ts +201 -0
  119. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  120. package/src/sdk/local-backend.ts +16 -8
  121. package/src/services/desktop-export.ts +3 -1
  122. package/src/services/desktop-native-metadata.ts +41 -18
  123. package/src/services/file-dialog.ts +8 -3
  124. package/src/services/tauri-modules.d.ts +25 -0
  125. package/src/store/basketVisibleSet.ts +3 -0
  126. package/src/store/globalId.ts +4 -1
  127. package/src/store/index.ts +79 -1
  128. package/src/store/slices/addElementMeshes.ts +365 -0
  129. package/src/store/slices/addElementSlice.ts +275 -0
  130. package/src/store/slices/annotationsSlice.test.ts +133 -0
  131. package/src/store/slices/annotationsSlice.ts +251 -0
  132. package/src/store/slices/dataSlice.test.ts +23 -4
  133. package/src/store/slices/dataSlice.ts +1 -1
  134. package/src/store/slices/modelSlice.test.ts +67 -9
  135. package/src/store/slices/modelSlice.ts +39 -7
  136. package/src/store/slices/mutationSlice.ts +964 -3
  137. package/src/store/slices/overlayCompositor.test.ts +164 -0
  138. package/src/store/slices/overlaySlice.test.ts +93 -0
  139. package/src/store/slices/overlaySlice.ts +151 -0
  140. package/src/store/slices/pinboardSlice.test.ts +6 -1
  141. package/src/store/slices/playbackSlice.ts +128 -0
  142. package/src/store/slices/pointCloudSlice.ts +102 -0
  143. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  144. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  145. package/src/store/slices/scheduleSlice.test.ts +694 -0
  146. package/src/store/slices/scheduleSlice.ts +1330 -0
  147. package/src/store/slices/searchSlice.test.ts +342 -0
  148. package/src/store/slices/searchSlice.ts +341 -0
  149. package/src/store/slices/selectionSlice.test.ts +46 -0
  150. package/src/store/slices/selectionSlice.ts +20 -0
  151. package/src/store/types.ts +7 -0
  152. package/src/store.ts +14 -0
  153. package/vite.config.ts +1 -0
  154. package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
  155. package/dist/assets/index-_bfZsDCC.css +0 -1
  156. package/dist/assets/sandbox-C8575tul.js +0 -5951
@@ -10,6 +10,7 @@ import {
10
10
  PersonStanding,
11
11
  Ruler,
12
12
  Scissors,
13
+ MapPin,
13
14
  Eye,
14
15
  EyeOff,
15
16
  Equal,
@@ -37,6 +38,7 @@ import {
37
38
  Layout,
38
39
  LayoutTemplate,
39
40
  FileCode2,
41
+ CalendarClock,
40
42
  Globe2,
41
43
  Settings,
42
44
  } from 'lucide-react';
@@ -66,6 +68,7 @@ import { ExportDialog } from './ExportDialog';
66
68
  import { BulkPropertyEditor } from './BulkPropertyEditor';
67
69
  import { DataConnector } from './DataConnector';
68
70
  import { ExportChangesButton } from './ExportChangesButton';
71
+ import { SearchInline } from './SearchInline';
69
72
  // CesiumSettingsDialog removed — settings now shown as overlay on Cesium viewer
70
73
  import { useFloorplanView } from '@/hooks/useFloorplanView';
71
74
  import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess, type DesktopFeature } from '@/lib/desktop-product';
@@ -84,7 +87,7 @@ import {
84
87
  subscribeAnalysisExtensions,
85
88
  } from '@/services/analysis-extensions';
86
89
 
87
- type Tool = 'select' | 'walk' | 'measure' | 'section';
90
+ type Tool = 'select' | 'walk' | 'measure' | 'section' | 'annotate';
88
91
  type WorkspacePanel = 'script' | 'list' | 'bcf' | 'ids' | 'lens' | string;
89
92
 
90
93
  function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
@@ -100,21 +103,39 @@ interface ToolButtonProps {
100
103
  shortcut?: string;
101
104
  activeTool: string;
102
105
  onToolChange: (tool: Tool) => void;
106
+ /**
107
+ * Tailwind classes applied when this tool is active. Defaults to the
108
+ * shared `bg-primary text-primary-foreground` shape; pass a per-tool
109
+ * accent (e.g. amber for Annotate) to set tools apart visually
110
+ * without breaking the toolbar's tool-button rhythm.
111
+ */
112
+ activeAccentClass?: string;
103
113
  }
104
114
 
105
- function ToolButton({ tool, icon: Icon, label, shortcut, activeTool, onToolChange }: ToolButtonProps) {
115
+ function ToolButton({
116
+ tool,
117
+ icon: Icon,
118
+ label,
119
+ shortcut,
120
+ activeTool,
121
+ onToolChange,
122
+ activeAccentClass,
123
+ }: ToolButtonProps) {
124
+ const isActive = activeTool === tool;
106
125
  return (
107
126
  <Tooltip>
108
127
  <TooltipTrigger asChild>
109
128
  <Button
110
- variant={activeTool === tool ? 'default' : 'ghost'}
129
+ variant={isActive ? 'default' : 'ghost'}
111
130
  size="icon-sm"
112
131
  onClick={(e) => {
113
132
  // Blur button to close tooltip after click
114
133
  (e.currentTarget as HTMLButtonElement).blur();
115
134
  onToolChange(tool);
116
135
  }}
117
- className={cn(activeTool === tool && 'bg-primary text-primary-foreground')}
136
+ className={cn(
137
+ isActive && (activeAccentClass ?? 'bg-primary text-primary-foreground'),
138
+ )}
118
139
  >
119
140
  <Icon className="h-4 w-4" />
120
141
  </Button>
@@ -305,6 +326,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
305
326
  const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
306
327
  const scriptPanelVisible = useViewerStore((state) => state.scriptPanelVisible);
307
328
  const setScriptPanelVisible = useViewerStore((state) => state.setScriptPanelVisible);
329
+ const ganttPanelVisible = useViewerStore((state) => state.ganttPanelVisible);
330
+ const setGanttPanelVisible = useViewerStore((state) => state.setGanttPanelVisible);
308
331
  // Cesium 3D overlay state
309
332
  const cesiumAvailable = useViewerStore((state) => state.cesiumAvailable);
310
333
  const cesiumEnabled = useViewerStore((state) => state.cesiumEnabled);
@@ -402,6 +425,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
402
425
  // Filter to supported files (IFC, IFCX, GLB)
403
426
  const supportedFiles = Array.from(files).filter(
404
427
  f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
428
+ || f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
405
429
  );
406
430
 
407
431
  if (supportedFiles.length === 0) return;
@@ -442,6 +466,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
442
466
  // Filter to supported files (IFC, IFCX, GLB)
443
467
  const supportedFiles = Array.from(files).filter(
444
468
  f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
469
+ || f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
445
470
  );
446
471
 
447
472
  if (supportedFiles.length === 0) return;
@@ -508,21 +533,31 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
508
533
  return false;
509
534
  }, [desktopEntitlement, promptDesktopUpgrade]);
510
535
 
511
- const handleToggleBottomPanel = useCallback((panel: 'script' | 'list') => {
536
+ const handleToggleBottomPanel = useCallback((panel: 'script' | 'list' | 'gantt') => {
512
537
  if (activeAnalysisExtension?.placement === 'bottom') {
513
538
  closeActiveAnalysisExtension();
514
539
  }
515
- const isScriptPanel = panel === 'script';
516
- const nextScriptVisible = isScriptPanel ? !scriptPanelVisible : false;
517
- const nextListVisible = isScriptPanel ? false : !listPanelVisible;
540
+ const nextScriptVisible = panel === 'script' ? !scriptPanelVisible : false;
541
+ const nextListVisible = panel === 'list' ? !listPanelVisible : false;
542
+ const nextGanttVisible = panel === 'gantt' ? !ganttPanelVisible : false;
518
543
 
519
544
  setScriptPanelVisible(nextScriptVisible);
520
545
  setListPanelVisible(nextListVisible);
546
+ setGanttPanelVisible(nextGanttVisible);
521
547
 
522
- if (nextScriptVisible || nextListVisible) {
548
+ if (nextScriptVisible || nextListVisible || nextGanttVisible) {
523
549
  setRightPanelCollapsed(false);
524
550
  }
525
- }, [activeAnalysisExtension?.placement, listPanelVisible, scriptPanelVisible, setListPanelVisible, setRightPanelCollapsed, setScriptPanelVisible]);
551
+ }, [
552
+ activeAnalysisExtension?.placement,
553
+ ganttPanelVisible,
554
+ listPanelVisible,
555
+ scriptPanelVisible,
556
+ setGanttPanelVisible,
557
+ setListPanelVisible,
558
+ setRightPanelCollapsed,
559
+ setScriptPanelVisible,
560
+ ]);
526
561
 
527
562
  const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens') => {
528
563
  if (activeAnalysisExtension?.placement !== 'bottom') {
@@ -577,6 +612,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
577
612
  if ((extension.placement ?? 'right') === 'bottom') {
578
613
  setScriptPanelVisible(false);
579
614
  setListPanelVisible(false);
615
+ setGanttPanelVisible(false);
580
616
  setRightPanelCollapsed(false);
581
617
  return;
582
618
  }
@@ -589,6 +625,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
589
625
  analysisExtensionState.activeId,
590
626
  analysisExtensionState.extensions,
591
627
  setBcfPanelVisible,
628
+ setGanttPanelVisible,
592
629
  setIdsPanelVisible,
593
630
  setLensPanelVisible,
594
631
  setListPanelVisible,
@@ -600,6 +637,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
600
637
  const panels = new Set<WorkspacePanel>();
601
638
  if (scriptPanelVisible) panels.add('script');
602
639
  if (listPanelVisible) panels.add('list');
640
+ if (ganttPanelVisible) panels.add('gantt');
603
641
  if (bcfPanelVisible) panels.add('bcf');
604
642
  if (idsPanelVisible) panels.add('ids');
605
643
  if (lensPanelVisible) panels.add('lens');
@@ -608,6 +646,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
608
646
  }, [
609
647
  analysisExtensionState.activeId,
610
648
  bcfPanelVisible,
649
+ ganttPanelVisible,
611
650
  idsPanelVisible,
612
651
  lensPanelVisible,
613
652
  listPanelVisible,
@@ -619,6 +658,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
619
658
  if (activeWorkspacePanels.size > 1) return 'Multiple Panels';
620
659
  if (activeWorkspacePanels.has('script')) return 'Script Editor';
621
660
  if (activeWorkspacePanels.has('list')) return 'Lists';
661
+ if (activeWorkspacePanels.has('gantt')) return 'Schedule';
622
662
  if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
623
663
  if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
624
664
  if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
@@ -741,7 +781,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
741
781
  id="file-input-open"
742
782
  ref={fileInputRef}
743
783
  type="file"
744
- accept=".ifc,.ifcx,.glb"
784
+ accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
745
785
  multiple
746
786
  onChange={handleFileSelect}
747
787
  className="hidden"
@@ -749,7 +789,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
749
789
  <input
750
790
  ref={addModelInputRef}
751
791
  type="file"
752
- accept=".ifc,.ifcx,.glb"
792
+ accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
753
793
  multiple
754
794
  onChange={handleAddModelSelect}
755
795
  className="hidden"
@@ -948,6 +988,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
948
988
  <FileSpreadsheet className="h-4 w-4 mr-2" />
949
989
  Lists
950
990
  </DropdownMenuCheckboxItem>
991
+ <DropdownMenuCheckboxItem
992
+ checked={activeWorkspacePanels.has('gantt')}
993
+ onCheckedChange={() => handleToggleBottomPanel('gantt')}
994
+ >
995
+ <CalendarClock className="h-4 w-4 mr-2" />
996
+ Schedule (Gantt)
997
+ </DropdownMenuCheckboxItem>
951
998
  <DropdownMenuSeparator />
952
999
  <DropdownMenuCheckboxItem
953
1000
  checked={activeWorkspacePanels.has('bcf')}
@@ -1011,6 +1058,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1011
1058
 
1012
1059
  <Separator orientation="vertical" className="h-6 mx-1" />
1013
1060
 
1061
+ {/* ── Search (Tier-0 inline; ⌘F or / to focus) ── */}
1062
+ <SearchInline />
1063
+
1064
+ <Separator orientation="vertical" className="h-6 mx-1" />
1065
+
1014
1066
  {/* ── Navigation Tools ── */}
1015
1067
  <ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" activeTool={activeTool} onToolChange={setActiveTool} />
1016
1068
  <ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" activeTool={activeTool} onToolChange={setActiveTool} />
@@ -1020,6 +1072,15 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1020
1072
  {/* ── Measurement & Section ── */}
1021
1073
  <ToolButton tool="measure" icon={Ruler} label="Measure" shortcut="M" activeTool={activeTool} onToolChange={setActiveTool} />
1022
1074
  <ToolButton tool="section" icon={Scissors} label="Section" shortcut="X" activeTool={activeTool} onToolChange={setActiveTool} />
1075
+ <ToolButton
1076
+ tool="annotate"
1077
+ icon={MapPin}
1078
+ label="Annotate"
1079
+ shortcut="P"
1080
+ activeTool={activeTool}
1081
+ onToolChange={setActiveTool}
1082
+ activeAccentClass="bg-amber-500 text-white hover:bg-amber-500/90"
1083
+ />
1023
1084
 
1024
1085
  {/* Floorplan dropdown */}
1025
1086
  {availableStoreys.length > 0 && (
@@ -0,0 +1,174 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Compact panel that exposes point cloud rendering controls (color mode,
7
+ * size mode, point size, EDL). Renders only when point cloud assets are
8
+ * loaded — sits over the canvas without affecting layout for IFC-only
9
+ * models.
10
+ */
11
+
12
+ import { useViewerStore } from '@/store';
13
+ import type { PointColorModeUi, PointSizeModeUi } from '@/store/slices/pointCloudSlice';
14
+ import { cn } from '@/lib/utils';
15
+
16
+ const COLOR_MODES: Array<{ value: PointColorModeUi; label: string; hint: string }> = [
17
+ { value: 'rgb', label: 'RGB', hint: 'Per-point colour from the source' },
18
+ { value: 'classification', label: 'Classification', hint: 'ASPRS class palette (ground, vegetation, building...)' },
19
+ { value: 'intensity', label: 'Intensity', hint: 'Greyscale ramp from per-point intensity' },
20
+ { value: 'height', label: 'Height', hint: 'Cool-warm ramp by Y-up world height' },
21
+ { value: 'fixed', label: 'Solid', hint: 'Single colour override' },
22
+ ];
23
+
24
+ const SIZE_MODES: Array<{ value: PointSizeModeUi; label: string; hint: string }> = [
25
+ { value: 'fixed-px', label: 'Fixed', hint: 'Always render at the slider value (in pixels)' },
26
+ { value: 'attenuated', label: 'Auto', hint: 'Adaptive (closer = bigger), clamped to the slider as max' },
27
+ { value: 'adaptive-world', label: 'World', hint: 'Pure world-space radius — splat covers N mm in source space' },
28
+ ];
29
+
30
+ export interface PointCloudPanelProps {
31
+ /** Number of currently-loaded point cloud assets — panel hides when 0. */
32
+ assetCount: number;
33
+ }
34
+
35
+ export function PointCloudPanel({ assetCount }: PointCloudPanelProps) {
36
+ const colorMode = useViewerStore((s) => s.pointCloudColorMode);
37
+ const setColorMode = useViewerStore((s) => s.setPointCloudColorMode);
38
+ const sizeMode = useViewerStore((s) => s.pointCloudSizeMode);
39
+ const setSizeMode = useViewerStore((s) => s.setPointCloudSizeMode);
40
+ const pointSize = useViewerStore((s) => s.pointCloudPointSize);
41
+ const setPointSize = useViewerStore((s) => s.setPointCloudPointSize);
42
+ const worldRadius = useViewerStore((s) => s.pointCloudWorldRadius);
43
+ const setWorldRadius = useViewerStore((s) => s.setPointCloudWorldRadius);
44
+ const edlEnabled = useViewerStore((s) => s.pointCloudEdlEnabled);
45
+ const setEdlEnabled = useViewerStore((s) => s.setPointCloudEdlEnabled);
46
+ const edlStrength = useViewerStore((s) => s.pointCloudEdlStrength);
47
+ const setEdlStrength = useViewerStore((s) => s.setPointCloudEdlStrength);
48
+
49
+ if (assetCount <= 0) return null;
50
+
51
+ return (
52
+ <div className="absolute bottom-4 left-4 z-10 pointer-events-auto bg-background/90 backdrop-blur-sm rounded-lg border shadow-lg p-2 flex flex-col gap-2 min-w-[200px]">
53
+ <div className="flex items-center justify-between gap-2">
54
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
55
+ Point Cloud
56
+ </span>
57
+ <span className="text-[10px] text-muted-foreground">
58
+ {assetCount} asset{assetCount === 1 ? '' : 's'}
59
+ </span>
60
+ </div>
61
+
62
+ {/* Color mode */}
63
+ <div className="flex flex-col gap-0.5">
64
+ <span className="text-[9px] uppercase text-muted-foreground tracking-wider">Colour</span>
65
+ {COLOR_MODES.map((mode) => {
66
+ const active = colorMode === mode.value;
67
+ return (
68
+ <button
69
+ key={mode.value}
70
+ aria-pressed={active}
71
+ onClick={() => setColorMode(mode.value)}
72
+ title={mode.hint}
73
+ className={cn(
74
+ 'flex items-center gap-2 px-2 py-1 rounded text-xs transition-colors text-left',
75
+ active
76
+ ? 'bg-teal-600 text-white'
77
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground',
78
+ )}
79
+ >
80
+ {mode.label}
81
+ </button>
82
+ );
83
+ })}
84
+ </div>
85
+
86
+ {/* Size mode */}
87
+ <div className="flex flex-col gap-0.5">
88
+ <span className="text-[9px] uppercase text-muted-foreground tracking-wider">Size</span>
89
+ <div className="grid grid-cols-3 gap-0.5">
90
+ {SIZE_MODES.map((mode) => {
91
+ const active = sizeMode === mode.value;
92
+ return (
93
+ <button
94
+ key={mode.value}
95
+ aria-pressed={active}
96
+ onClick={() => setSizeMode(mode.value)}
97
+ title={mode.hint}
98
+ className={cn(
99
+ 'px-1.5 py-1 rounded text-[11px] transition-colors',
100
+ active
101
+ ? 'bg-teal-600 text-white'
102
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground',
103
+ )}
104
+ >
105
+ {mode.label}
106
+ </button>
107
+ );
108
+ })}
109
+ </div>
110
+ <label className="flex items-center gap-2 mt-1">
111
+ <span className="text-[10px] text-muted-foreground w-8 shrink-0">{pointSize.toFixed(0)}px</span>
112
+ <input
113
+ type="range"
114
+ min={1}
115
+ max={20}
116
+ step={1}
117
+ value={pointSize}
118
+ onChange={(e) => setPointSize(Number(e.target.value))}
119
+ className="flex-1 h-1 accent-teal-600 cursor-pointer"
120
+ title="Splat size in pixels (or upper cap in Auto mode)"
121
+ />
122
+ </label>
123
+ {sizeMode !== 'fixed-px' && (
124
+ <label className="flex items-center gap-2">
125
+ <span className="text-[10px] text-muted-foreground w-8 shrink-0">
126
+ {(worldRadius * 1000).toFixed(0)}mm
127
+ </span>
128
+ <input
129
+ type="range"
130
+ min={1}
131
+ max={100}
132
+ step={1}
133
+ value={Math.round(worldRadius * 1000)}
134
+ onChange={(e) => setWorldRadius(Number(e.target.value) / 1000)}
135
+ className="flex-1 h-1 accent-teal-600 cursor-pointer"
136
+ title="World-space splat radius in millimetres"
137
+ />
138
+ </label>
139
+ )}
140
+ </div>
141
+
142
+ {/* EDL */}
143
+ <div className="flex flex-col gap-0.5">
144
+ <label className="flex items-center justify-between gap-2 cursor-pointer">
145
+ <span className="text-[9px] uppercase text-muted-foreground tracking-wider">EDL</span>
146
+ <input
147
+ type="checkbox"
148
+ checked={edlEnabled}
149
+ onChange={(e) => setEdlEnabled(e.target.checked)}
150
+ className="accent-teal-600"
151
+ title="Eye-Dome Lighting — adds depth perception via screen-space depth gradient"
152
+ />
153
+ </label>
154
+ {edlEnabled && (
155
+ <label className="flex items-center gap-2">
156
+ <span className="text-[10px] text-muted-foreground w-8 shrink-0">
157
+ {edlStrength.toFixed(1)}
158
+ </span>
159
+ <input
160
+ type="range"
161
+ min={0}
162
+ max={3}
163
+ step={0.1}
164
+ value={edlStrength}
165
+ onChange={(e) => setEdlStrength(Number(e.target.value))}
166
+ className="flex-1 h-1 accent-teal-600 cursor-pointer"
167
+ title="EDL strength multiplier"
168
+ />
169
+ </label>
170
+ )}
171
+ </div>
172
+ </div>
173
+ );
174
+ }