@ifc-lite/viewer 1.17.6 → 1.18.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 (143) hide show
  1. package/.turbo/turbo-build.log +17 -14
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +513 -0
  4. package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  5. package/dist/assets/{exporters-CcPS9MK5.js → exporters-B_OBqIyD.js} +3235 -2648
  6. package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-xHHy-9DV.js} +1 -1
  7. package/dist/assets/{ifc-lite_bg-BINvzoCP.wasm → ifc-lite_bg-ADjKXSms.wasm} +0 -0
  8. package/dist/assets/{index-Bfms9I4A.js → index-BKq-M3Mk.js} +44124 -30920
  9. package/dist/assets/index-COnQRuqY.css +1 -0
  10. package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-SHXiQwFW.js} +1 -1
  11. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  12. package/dist/assets/{server-client-BuZK7OST.js → server-client-ncOQVNso.js} +1 -1
  13. package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-DyfBSB8z.js} +1 -1
  14. package/dist/index.html +6 -6
  15. package/package.json +10 -10
  16. package/src/apache-arrow.d.ts +30 -0
  17. package/src/components/viewer/AddElementPanel.tsx +758 -0
  18. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  19. package/src/components/viewer/ChatPanel.tsx +64 -2
  20. package/src/components/viewer/CommandPalette.tsx +56 -7
  21. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  22. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  23. package/src/components/viewer/ExportDialog.tsx +19 -1
  24. package/src/components/viewer/MainToolbar.tsx +69 -10
  25. package/src/components/viewer/PropertiesPanel.tsx +222 -22
  26. package/src/components/viewer/SearchInline.tsx +669 -0
  27. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  28. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  29. package/src/components/viewer/SearchModal.text.tsx +388 -0
  30. package/src/components/viewer/SearchModal.tsx +235 -0
  31. package/src/components/viewer/ToolOverlays.tsx +5 -0
  32. package/src/components/viewer/ViewerLayout.tsx +24 -4
  33. package/src/components/viewer/Viewport.tsx +11 -1
  34. package/src/components/viewer/ViewportContainer.tsx +2 -0
  35. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  36. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  37. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  38. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  39. package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
  40. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  41. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  42. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  43. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  44. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  45. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  46. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  47. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  48. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  49. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  50. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  51. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  52. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  53. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  54. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  55. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  56. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  57. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  58. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  59. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  60. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  61. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  62. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  63. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  64. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  65. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  66. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  67. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  68. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  69. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  70. package/src/components/viewer/selectionHandlers.ts +446 -0
  71. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  72. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  73. package/src/components/viewer/useMouseControls.ts +9 -1
  74. package/src/hooks/useIfcLoader.ts +22 -10
  75. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  76. package/src/hooks/useSandbox.ts +1 -1
  77. package/src/hooks/useSearchIndex.ts +125 -0
  78. package/src/index.css +66 -0
  79. package/src/lib/llm/system-prompt.test.ts +14 -0
  80. package/src/lib/llm/system-prompt.ts +102 -1
  81. package/src/lib/llm/types.ts +6 -0
  82. package/src/lib/recent-files.ts +38 -4
  83. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  84. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  85. package/src/lib/scripts/templates.ts +7 -0
  86. package/src/lib/search/common-ifc-types.ts +36 -0
  87. package/src/lib/search/filter-evaluate.test.ts +537 -0
  88. package/src/lib/search/filter-evaluate.ts +610 -0
  89. package/src/lib/search/filter-rules.test.ts +119 -0
  90. package/src/lib/search/filter-rules.ts +198 -0
  91. package/src/lib/search/filter-schema.test.ts +233 -0
  92. package/src/lib/search/filter-schema.ts +146 -0
  93. package/src/lib/search/recent-searches.test.ts +116 -0
  94. package/src/lib/search/recent-searches.ts +93 -0
  95. package/src/lib/search/result-export.test.ts +101 -0
  96. package/src/lib/search/result-export.ts +104 -0
  97. package/src/lib/search/saved-filters.test.ts +118 -0
  98. package/src/lib/search/saved-filters.ts +154 -0
  99. package/src/lib/search/tier0-scan.test.ts +196 -0
  100. package/src/lib/search/tier0-scan.ts +237 -0
  101. package/src/lib/search/tier1-index.test.ts +242 -0
  102. package/src/lib/search/tier1-index.ts +448 -0
  103. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  104. package/src/sdk/adapters/export-adapter.ts +404 -1
  105. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  106. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  107. package/src/sdk/adapters/model-compat.ts +8 -2
  108. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  109. package/src/sdk/adapters/store-adapter.ts +201 -0
  110. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  111. package/src/sdk/local-backend.ts +16 -8
  112. package/src/services/desktop-export.ts +3 -1
  113. package/src/services/desktop-native-metadata.ts +41 -18
  114. package/src/services/file-dialog.ts +4 -1
  115. package/src/services/tauri-modules.d.ts +25 -0
  116. package/src/store/basketVisibleSet.ts +3 -0
  117. package/src/store/globalId.ts +4 -1
  118. package/src/store/index.ts +70 -1
  119. package/src/store/slices/addElementMeshes.ts +365 -0
  120. package/src/store/slices/addElementSlice.ts +275 -0
  121. package/src/store/slices/annotationsSlice.test.ts +133 -0
  122. package/src/store/slices/annotationsSlice.ts +251 -0
  123. package/src/store/slices/dataSlice.test.ts +23 -4
  124. package/src/store/slices/dataSlice.ts +1 -1
  125. package/src/store/slices/modelSlice.test.ts +67 -9
  126. package/src/store/slices/modelSlice.ts +39 -7
  127. package/src/store/slices/mutationSlice.ts +964 -3
  128. package/src/store/slices/overlayCompositor.test.ts +164 -0
  129. package/src/store/slices/overlaySlice.test.ts +93 -0
  130. package/src/store/slices/overlaySlice.ts +151 -0
  131. package/src/store/slices/pinboardSlice.test.ts +6 -1
  132. package/src/store/slices/playbackSlice.ts +128 -0
  133. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  134. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  135. package/src/store/slices/scheduleSlice.test.ts +694 -0
  136. package/src/store/slices/scheduleSlice.ts +1330 -0
  137. package/src/store/slices/searchSlice.test.ts +342 -0
  138. package/src/store/slices/searchSlice.ts +341 -0
  139. package/src/store/slices/selectionSlice.test.ts +46 -0
  140. package/src/store/slices/selectionSlice.ts +20 -0
  141. package/src/store.ts +14 -0
  142. package/dist/assets/index-_bfZsDCC.css +0 -1
  143. 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);
@@ -508,21 +531,31 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
508
531
  return false;
509
532
  }, [desktopEntitlement, promptDesktopUpgrade]);
510
533
 
511
- const handleToggleBottomPanel = useCallback((panel: 'script' | 'list') => {
534
+ const handleToggleBottomPanel = useCallback((panel: 'script' | 'list' | 'gantt') => {
512
535
  if (activeAnalysisExtension?.placement === 'bottom') {
513
536
  closeActiveAnalysisExtension();
514
537
  }
515
- const isScriptPanel = panel === 'script';
516
- const nextScriptVisible = isScriptPanel ? !scriptPanelVisible : false;
517
- const nextListVisible = isScriptPanel ? false : !listPanelVisible;
538
+ const nextScriptVisible = panel === 'script' ? !scriptPanelVisible : false;
539
+ const nextListVisible = panel === 'list' ? !listPanelVisible : false;
540
+ const nextGanttVisible = panel === 'gantt' ? !ganttPanelVisible : false;
518
541
 
519
542
  setScriptPanelVisible(nextScriptVisible);
520
543
  setListPanelVisible(nextListVisible);
544
+ setGanttPanelVisible(nextGanttVisible);
521
545
 
522
- if (nextScriptVisible || nextListVisible) {
546
+ if (nextScriptVisible || nextListVisible || nextGanttVisible) {
523
547
  setRightPanelCollapsed(false);
524
548
  }
525
- }, [activeAnalysisExtension?.placement, listPanelVisible, scriptPanelVisible, setListPanelVisible, setRightPanelCollapsed, setScriptPanelVisible]);
549
+ }, [
550
+ activeAnalysisExtension?.placement,
551
+ ganttPanelVisible,
552
+ listPanelVisible,
553
+ scriptPanelVisible,
554
+ setGanttPanelVisible,
555
+ setListPanelVisible,
556
+ setRightPanelCollapsed,
557
+ setScriptPanelVisible,
558
+ ]);
526
559
 
527
560
  const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens') => {
528
561
  if (activeAnalysisExtension?.placement !== 'bottom') {
@@ -577,6 +610,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
577
610
  if ((extension.placement ?? 'right') === 'bottom') {
578
611
  setScriptPanelVisible(false);
579
612
  setListPanelVisible(false);
613
+ setGanttPanelVisible(false);
580
614
  setRightPanelCollapsed(false);
581
615
  return;
582
616
  }
@@ -589,6 +623,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
589
623
  analysisExtensionState.activeId,
590
624
  analysisExtensionState.extensions,
591
625
  setBcfPanelVisible,
626
+ setGanttPanelVisible,
592
627
  setIdsPanelVisible,
593
628
  setLensPanelVisible,
594
629
  setListPanelVisible,
@@ -600,6 +635,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
600
635
  const panels = new Set<WorkspacePanel>();
601
636
  if (scriptPanelVisible) panels.add('script');
602
637
  if (listPanelVisible) panels.add('list');
638
+ if (ganttPanelVisible) panels.add('gantt');
603
639
  if (bcfPanelVisible) panels.add('bcf');
604
640
  if (idsPanelVisible) panels.add('ids');
605
641
  if (lensPanelVisible) panels.add('lens');
@@ -608,6 +644,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
608
644
  }, [
609
645
  analysisExtensionState.activeId,
610
646
  bcfPanelVisible,
647
+ ganttPanelVisible,
611
648
  idsPanelVisible,
612
649
  lensPanelVisible,
613
650
  listPanelVisible,
@@ -619,6 +656,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
619
656
  if (activeWorkspacePanels.size > 1) return 'Multiple Panels';
620
657
  if (activeWorkspacePanels.has('script')) return 'Script Editor';
621
658
  if (activeWorkspacePanels.has('list')) return 'Lists';
659
+ if (activeWorkspacePanels.has('gantt')) return 'Schedule';
622
660
  if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
623
661
  if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
624
662
  if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
@@ -948,6 +986,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
948
986
  <FileSpreadsheet className="h-4 w-4 mr-2" />
949
987
  Lists
950
988
  </DropdownMenuCheckboxItem>
989
+ <DropdownMenuCheckboxItem
990
+ checked={activeWorkspacePanels.has('gantt')}
991
+ onCheckedChange={() => handleToggleBottomPanel('gantt')}
992
+ >
993
+ <CalendarClock className="h-4 w-4 mr-2" />
994
+ Schedule (Gantt)
995
+ </DropdownMenuCheckboxItem>
951
996
  <DropdownMenuSeparator />
952
997
  <DropdownMenuCheckboxItem
953
998
  checked={activeWorkspacePanels.has('bcf')}
@@ -1011,6 +1056,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1011
1056
 
1012
1057
  <Separator orientation="vertical" className="h-6 mx-1" />
1013
1058
 
1059
+ {/* ── Search (Tier-0 inline; ⌘F or / to focus) ── */}
1060
+ <SearchInline />
1061
+
1062
+ <Separator orientation="vertical" className="h-6 mx-1" />
1063
+
1014
1064
  {/* ── Navigation Tools ── */}
1015
1065
  <ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" activeTool={activeTool} onToolChange={setActiveTool} />
1016
1066
  <ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" activeTool={activeTool} onToolChange={setActiveTool} />
@@ -1020,6 +1070,15 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
1020
1070
  {/* ── Measurement & Section ── */}
1021
1071
  <ToolButton tool="measure" icon={Ruler} label="Measure" shortcut="M" activeTool={activeTool} onToolChange={setActiveTool} />
1022
1072
  <ToolButton tool="section" icon={Scissors} label="Section" shortcut="X" activeTool={activeTool} onToolChange={setActiveTool} />
1073
+ <ToolButton
1074
+ tool="annotate"
1075
+ icon={MapPin}
1076
+ label="Annotate"
1077
+ shortcut="P"
1078
+ activeTool={activeTool}
1079
+ onToolChange={setActiveTool}
1080
+ activeAccentClass="bg-amber-500 text-white hover:bg-amber-500/90"
1081
+ />
1023
1082
 
1024
1083
  {/* Floorplan dropdown */}
1025
1084
  {availableStoreys.length > 0 && (
@@ -33,7 +33,8 @@ import { getNativeEntityDetails } from '@/services/desktop-native-metadata';
33
33
  import { configureMutationView } from '@/utils/configureMutationView';
34
34
  import { IfcQuery } from '@ifc-lite/query';
35
35
  import { MutablePropertyView } from '@ifc-lite/mutations';
36
- import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
36
+ import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, getAttributeNames, type IfcDataStore } from '@ifc-lite/parser';
37
+ import type { NewEntity } from '@ifc-lite/mutations';
37
38
  import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
38
39
  import type { EntityRef, FederatedModel } from '@/store/types';
39
40
 
@@ -43,11 +44,14 @@ import { QuantitySetCard } from './properties/QuantitySetCard';
43
44
  import { ModelMetadataPanel } from './properties/ModelMetadataPanel';
44
45
  import { ClassificationCard } from './properties/ClassificationCard';
45
46
  import { MaterialCard } from './properties/MaterialCard';
47
+ import { ScheduleCard } from './properties/ScheduleCard';
48
+ import { TaskEditCard } from './properties/TaskEditCard';
46
49
  import { DocumentCard } from './properties/DocumentCard';
47
50
  import { RelationshipsCard } from './properties/RelationshipsCard';
48
51
  import type { PropertySet, QuantitySet } from './properties/encodingUtils';
49
52
  import { BsddCard } from './properties/BsddCard';
50
53
  import { GeoreferencingPanel } from './properties/GeoreferencingPanel';
54
+ import { RawStepCard } from './properties/RawStepCard';
51
55
 
52
56
  type DisplayProperty = { name: string; value: unknown; isMutated: boolean };
53
57
  type DisplayPropertySet = {
@@ -57,6 +61,41 @@ type DisplayPropertySet = {
57
61
  source?: PropertySet['source'];
58
62
  };
59
63
 
64
+ /**
65
+ * Synthesize an attribute list from a NewEntity record so the panel's
66
+ * attributes section renders for overlay-only duplicates / scripted
67
+ * adds. Positional indices are mapped to schema names; everything past
68
+ * the schema's defined slots is dropped (no "Arg 9" rows in the bSDD
69
+ * panel).
70
+ */
71
+ function attributesFromOverlayEntity(entity: NewEntity): Array<{ name: string; value: string }> {
72
+ const names = getAttributeNames(entity.type) ?? [];
73
+ if (names.length === 0) return [];
74
+ const out: Array<{ name: string; value: string }> = [];
75
+ // Stop at the smaller of the schema and the actual attributes — IFC
76
+ // entities can be partially populated (trailing optionals omitted).
77
+ const len = Math.min(names.length, entity.attributes.length);
78
+ for (let i = 0; i < len; i++) {
79
+ const value = entity.attributes[i];
80
+ let display: string;
81
+ if (value === null || value === undefined) continue;
82
+ if (typeof value === 'string') {
83
+ if (value === '$' || value.length === 0) continue;
84
+ display = value;
85
+ } else if (typeof value === 'number') {
86
+ display = String(value);
87
+ } else if (typeof value === 'boolean') {
88
+ display = value ? 'true' : 'false';
89
+ } else {
90
+ // Lists / typed values — skip the bSDD attributes panel; users
91
+ // can still see them on the Raw STEP tab.
92
+ continue;
93
+ }
94
+ out.push({ name: names[i], value: display });
95
+ }
96
+ return out;
97
+ }
98
+
60
99
  function mergePropertySetLists(base: DisplayPropertySet[], incoming: DisplayPropertySet[]): DisplayPropertySet[] {
61
100
  const merged = base.map(pset => ({
62
101
  ...pset,
@@ -364,6 +403,32 @@ export function PropertiesPanel() {
364
403
  return modelQuery.entity(originalExpressId);
365
404
  }, [selectedEntity, modelQuery]);
366
405
 
406
+ // Overlay-only entity record (duplicates, scripted adds). Carries
407
+ // the type + positional attributes the StoreEditor recorded — used
408
+ // as a fallback when the parsed entityNode comes up empty so the
409
+ // panel doesn't render `UNKNOWN / Unknown` for fresh entities.
410
+ const overlayEntity = useMemo(() => {
411
+ let modelId = selectedEntity?.modelId;
412
+ if (modelId === 'legacy') modelId = '__legacy__';
413
+ const expressId = selectedEntity?.expressId;
414
+ if (!modelId || !expressId) return null;
415
+ const view = mutationViews.get(modelId);
416
+ return view?.getNewEntity(expressId) ?? null;
417
+ // eslint-disable-next-line react-hooks/exhaustive-deps
418
+ }, [selectedEntity, mutationViews, mutationVersion]);
419
+
420
+ /**
421
+ * Read a positional attribute from the overlay entity record as a
422
+ * display string. Returns null when the entity isn't overlay-only
423
+ * or the slot is empty / not stringy.
424
+ */
425
+ const overlayAttr = useCallback((index: number): string | null => {
426
+ if (!overlayEntity) return null;
427
+ const value = overlayEntity.attributes[index];
428
+ if (typeof value === 'string' && value.length > 0 && value !== '$') return value;
429
+ return null;
430
+ }, [overlayEntity]);
431
+
367
432
  // Check if the selected entity is a type entity (IfcWallType, etc.)
368
433
  // Uses the entity type name to detect — type entity names end with "Type"
369
434
  const isTypeEntity = useMemo(() => {
@@ -483,7 +548,14 @@ export function PropertiesPanel() {
483
548
  // Merges mutated attributes (from bSDD) into the base attribute list.
484
549
  // Note: GlobalId is intentionally excluded since it's shown in the dedicated GUID field above
485
550
  const attributes = useMemo(() => {
486
- const base = entityNode ? entityNode.allAttributes() : [];
551
+ const base = entityNode
552
+ ? entityNode.allAttributes()
553
+ // Overlay-only entity (duplicate / scripted add) — synthesize the
554
+ // attribute list from the NewEntity record using the schema's
555
+ // positional names so the panel still shows Name/Description/etc.
556
+ : overlayEntity
557
+ ? attributesFromOverlayEntity(overlayEntity)
558
+ : [];
487
559
 
488
560
  // Merge mutated attributes from bSDD
489
561
  let modelId = selectedEntity?.modelId;
@@ -510,41 +582,100 @@ export function PropertiesPanel() {
510
582
  }
511
583
 
512
584
  return base;
513
- }, [entityNode, selectedEntity, mutationViews, mutationVersion]);
585
+ }, [entityNode, overlayEntity, selectedEntity, mutationViews, mutationVersion]);
586
+
587
+ // Resolve the entity id used for parsed-store lookups. For overlay
588
+ // duplicates this is the source entity (via the view's alias) — so
589
+ // materials / classifications / documents / structural rels appear
590
+ // on the duplicate exactly as they do on the source. Without the
591
+ // alias resolution the parsed maps would return empty for the
592
+ // overlay-only id.
593
+ const lookupExpressId = useMemo(() => {
594
+ const expressId = selectedEntity?.expressId;
595
+ if (!expressId) return null;
596
+ let modelId = selectedEntity?.modelId;
597
+ if (modelId === 'legacy') modelId = '__legacy__';
598
+ const view = modelId ? mutationViews.get(modelId) : null;
599
+ return view?.resolveBaseEntityId(expressId) ?? expressId;
600
+ // eslint-disable-next-line react-hooks/exhaustive-deps
601
+ }, [selectedEntity, mutationViews, mutationVersion]);
514
602
 
515
603
  // Extract classifications for the selected entity from the IFC data store
516
604
  const classifications = useMemo(() => {
517
- if (!selectedEntity) return [];
605
+ if (!selectedEntity || lookupExpressId === null) return [];
518
606
  const dataStore = model?.ifcDataStore ?? ifcDataStore;
519
607
  if (!dataStore) return [];
520
- return extractClassificationsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
521
- }, [selectedEntity, model, ifcDataStore]);
608
+ return extractClassificationsOnDemand(dataStore as IfcDataStore, lookupExpressId);
609
+ }, [selectedEntity, lookupExpressId, model, ifcDataStore]);
522
610
 
523
611
  // Extract materials for the selected entity from the IFC data store
524
612
  const materialInfo = useMemo(() => {
525
- if (!selectedEntity) return null;
613
+ if (!selectedEntity || lookupExpressId === null) return null;
526
614
  const dataStore = model?.ifcDataStore ?? ifcDataStore;
527
615
  if (!dataStore) return null;
528
- return extractMaterialsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
529
- }, [selectedEntity, model, ifcDataStore]);
616
+ return extractMaterialsOnDemand(dataStore as IfcDataStore, lookupExpressId);
617
+ }, [selectedEntity, lookupExpressId, model, ifcDataStore]);
530
618
 
531
619
  // Extract documents for the selected entity from the IFC data store
532
620
  const documents = useMemo(() => {
533
- if (!selectedEntity) return [];
621
+ if (!selectedEntity || lookupExpressId === null) return [];
534
622
  const dataStore = model?.ifcDataStore ?? ifcDataStore;
535
623
  if (!dataStore) return [];
536
- return extractDocumentsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
537
- }, [selectedEntity, model, ifcDataStore]);
624
+ return extractDocumentsOnDemand(dataStore as IfcDataStore, lookupExpressId);
625
+ }, [selectedEntity, lookupExpressId, model, ifcDataStore]);
538
626
 
539
627
  // Extract structural relationships (openings, fills, groups, connections)
540
628
  const entityRelationships = useMemo(() => {
541
- if (!selectedEntity) return null;
629
+ if (!selectedEntity || lookupExpressId === null) return null;
542
630
  const dataStore = model?.ifcDataStore ?? ifcDataStore;
543
631
  if (!dataStore) return null;
544
- const rels = extractRelationshipsOnDemand(dataStore as IfcDataStore, selectedEntity.expressId);
632
+ const rels = extractRelationshipsOnDemand(dataStore as IfcDataStore, lookupExpressId);
545
633
  const totalCount = rels.voids.length + rels.fills.length + rels.groups.length + rels.connections.length;
546
634
  return totalCount > 0 ? rels : null;
635
+ }, [selectedEntity, lookupExpressId, model, ifcDataStore]);
636
+
637
+ // 4D schedule — both parsed-from-IFC and locally-generated schedules live in
638
+ // the schedule slice. ScheduleCard renders nothing when no task in the
639
+ // schedule lists this entity as a controlled product, so it's safe to call
640
+ // unconditionally.
641
+ const scheduleData = useViewerStore((s) => s.scheduleData);
642
+ // Single-task selection from the Gantt triggers the Task edit card —
643
+ // pull the set and its size so the Inspector can react to any change.
644
+ const selectedTaskGlobalIds = useViewerStore((s) => s.selectedTaskGlobalIds);
645
+ const singleSelectedTaskGlobalId = useMemo(() => {
646
+ if (selectedTaskGlobalIds.size !== 1) return null;
647
+ return selectedTaskGlobalIds.values().next().value ?? null;
648
+ }, [selectedTaskGlobalIds]);
649
+ // True when the schedule contains at least one task the user generated
650
+ // locally (no expressId in the host STEP). Mixed schedules — parsed tail +
651
+ // user-appended task — still surface the pending banner so the user sees
652
+ // that something will be spliced on export.
653
+ const scheduleIsGenerated = useMemo(() => {
654
+ if (!scheduleData || scheduleData.tasks.length === 0) return false;
655
+ return scheduleData.tasks.some(t => !t.expressId || t.expressId <= 0);
656
+ }, [scheduleData]);
657
+ const selectedEntityGlobalId = useMemo(() => {
658
+ if (!selectedEntity) return null;
659
+ const dataStore = model?.ifcDataStore ?? ifcDataStore;
660
+ return (dataStore as IfcDataStore | null)?.entities?.getGlobalId?.(selectedEntity.expressId) ?? null;
547
661
  }, [selectedEntity, model, ifcDataStore]);
662
+ /** True when at least one task in the current schedule controls this entity —
663
+ * used to keep the Inspector's empty-state from hiding a populated card.
664
+ * Federation-aware: matches globalId first (see `ScheduleCard`). */
665
+ const hasScheduleForSelection = useMemo(() => {
666
+ if (!selectedEntity || !scheduleData || scheduleData.tasks.length === 0) return false;
667
+ const expressId = selectedEntity.expressId;
668
+ const gid = selectedEntityGlobalId;
669
+ for (const task of scheduleData.tasks) {
670
+ const taskHasGlobalIds = task.productGlobalIds.some(Boolean);
671
+ if (gid && taskHasGlobalIds) {
672
+ if (task.productGlobalIds.includes(gid)) return true;
673
+ continue;
674
+ }
675
+ if (expressId > 0 && task.productExpressIds.includes(expressId)) return true;
676
+ }
677
+ return false;
678
+ }, [selectedEntity, scheduleData, selectedEntityGlobalId]);
548
679
 
549
680
  // Extract georeferencing info for the model (used in coordinates section)
550
681
  const georef = useMemo(() => {
@@ -829,15 +960,19 @@ export function PropertiesPanel() {
829
960
 
830
961
  const renderedEntityType = isNativeLazySelection
831
962
  ? (nativeDetails?.summary.type ?? 'Loading...')
832
- : (entityNode?.type ?? 'Unknown');
963
+ : (entityNode?.type ?? overlayEntity?.type ?? 'Unknown');
833
964
  const renderedEntityName = isNativeLazySelection
834
965
  ? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
835
- : entityNode?.name;
966
+ : (entityNode?.name ?? overlayAttr(2) ?? undefined);
836
967
  const renderedEntityGlobalId = isNativeLazySelection
837
968
  ? (nativeDetails?.summary.globalId ?? null)
838
- : entityNode?.globalId;
839
- const renderedEntityDescription = isNativeLazySelection ? undefined : entityNode?.description;
840
- const renderedEntityObjectType = isNativeLazySelection ? undefined : entityNode?.objectType;
969
+ : (entityNode?.globalId ?? overlayAttr(0));
970
+ const renderedEntityDescription = isNativeLazySelection
971
+ ? undefined
972
+ : (entityNode?.description ?? overlayAttr(3) ?? undefined);
973
+ const renderedEntityObjectType = isNativeLazySelection
974
+ ? undefined
975
+ : (entityNode?.objectType ?? overlayAttr(4) ?? undefined);
841
976
  const renderedSpatialInfo = isNativeLazySelection ? nativeSpatialInfo : spatialInfo;
842
977
  const renderedOccurrenceProperties = isNativeLazySelection ? nativeOccurrenceProperties : occurrenceProperties;
843
978
  const renderedInheritedTypeProperties = isNativeLazySelection ? [] : inheritedTypeProperties;
@@ -910,7 +1045,12 @@ export function PropertiesPanel() {
910
1045
  );
911
1046
  }
912
1047
 
913
- if (!selectedEntityId || (!isNativeLazySelection && (!modelQuery || !entityNode))) {
1048
+ // Newly-created/duplicated entities live only in the mutation overlay,
1049
+ // so the synthesized attributes + Raw STEP tab fall back to
1050
+ // `overlayEntity` when `entityNode` is empty. Without including
1051
+ // `overlayEntity` here the panel collapses to the model-metadata
1052
+ // view the moment a fresh add lands.
1053
+ if (!selectedEntityId || (!isNativeLazySelection && (!modelQuery || (!entityNode && !overlayEntity)))) {
914
1054
  // Show model metadata when a single model is loaded and nothing selected.
915
1055
  // Handles both federated models (models.size >= 1) and legacy single-model path (models.size === 0).
916
1056
  if (models.size === 1) {
@@ -937,7 +1077,7 @@ export function PropertiesPanel() {
937
1077
  return (
938
1078
  <div className="h-full flex flex-col border-l-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black">
939
1079
  <div className="p-3 border-b-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
940
- <h2 className="font-bold uppercase tracking-wider text-xs text-zinc-900 dark:text-zinc-100">Properties</h2>
1080
+ <h2 className="font-bold uppercase tracking-wider text-xs text-zinc-900 dark:text-zinc-100">Inspector</h2>
941
1081
  </div>
942
1082
  <div className="flex-1 flex flex-col items-center justify-center text-center p-6 bg-white dark:bg-black">
943
1083
  <div className="w-16 h-16 border-2 border-dashed border-zinc-300 dark:border-zinc-800 flex items-center justify-center mb-4 bg-zinc-100 dark:bg-zinc-950">
@@ -1266,10 +1406,32 @@ export function PropertiesPanel() {
1266
1406
  <Tag className="h-3 w-3 shrink-0 panel-compact-icon" />
1267
1407
  <span className="panel-compact-text">bSDD</span>
1268
1408
  </TabsTrigger>
1409
+ <TabsTrigger
1410
+ value="raw-step"
1411
+ title="Raw STEP — developer view of positional arguments"
1412
+ className="properties-tab-trigger raw-step-tab-trigger shrink-0 grow-0 px-2 font-mono"
1413
+ >
1414
+ {/* Bracket glyphs read as "code" without an icon dependency,
1415
+ stay readable at 9px, and free up width for the three
1416
+ primary tabs to keep their text visible at the default
1417
+ panel size. */}
1418
+ <span aria-hidden className="text-[10px] leading-none tracking-tight">&lt;/&gt;</span>
1419
+ <span className="sr-only">Raw STEP</span>
1420
+ </TabsTrigger>
1269
1421
  </TabsList>
1270
1422
 
1271
1423
  <ScrollArea className="flex-1 bg-white dark:bg-black">
1272
1424
  <TabsContent value="properties" className="m-0 p-3 overflow-hidden">
1425
+ {/* Task edit card — renders when exactly one Gantt task is
1426
+ selected. Shown above any entity properties because the
1427
+ user's attention shifted to editing the task, not the 3D
1428
+ element. Other tabs (quantities / relationships / bSDD)
1429
+ still show entity content regardless. */}
1430
+ {singleSelectedTaskGlobalId && (
1431
+ <div className="mb-3">
1432
+ <TaskEditCard taskGlobalId={singleSelectedTaskGlobalId} />
1433
+ </div>
1434
+ )}
1273
1435
  {/* Edit toolbar - only shown when edit mode is active */}
1274
1436
  {editMode && selectedEntity && !isNativeLazySelection && (
1275
1437
  <EditToolbar
@@ -1281,7 +1443,12 @@ export function PropertiesPanel() {
1281
1443
  schemaVersion={activeDataStore?.schemaVersion}
1282
1444
  />
1283
1445
  )}
1284
- {renderedMergedProperties.length === 0 && renderedClassifications.length === 0 && !renderedMaterialInfo && renderedDocuments.length === 0 ? (
1446
+ {renderedMergedProperties.length === 0
1447
+ && renderedClassifications.length === 0
1448
+ && !renderedMaterialInfo
1449
+ && renderedDocuments.length === 0
1450
+ && !renderedEntityRelationships
1451
+ && !hasScheduleForSelection ? (
1285
1452
  <p className="text-sm text-zinc-500 dark:text-zinc-500 text-center py-8 font-mono">No property sets</p>
1286
1453
  ) : (
1287
1454
  <div className="space-y-3 w-full overflow-hidden">
@@ -1377,6 +1544,21 @@ export function PropertiesPanel() {
1377
1544
  <RelationshipsCard relationships={renderedEntityRelationships} />
1378
1545
  </>
1379
1546
  )}
1547
+
1548
+ {/* 4D / Construction schedule — controlling tasks for this entity.
1549
+ Gated on `hasScheduleForSelection` so the separator above
1550
+ doesn't render on its own when ScheduleCard would return null. */}
1551
+ {selectedEntity && scheduleData && hasScheduleForSelection && (
1552
+ <>
1553
+ <div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
1554
+ <ScheduleCard
1555
+ scheduleData={scheduleData}
1556
+ selectedExpressId={selectedEntity.expressId}
1557
+ selectedGlobalId={selectedEntityGlobalId}
1558
+ isGenerated={scheduleIsGenerated}
1559
+ />
1560
+ </>
1561
+ )}
1380
1562
  </div>
1381
1563
  )}
1382
1564
  </TabsContent>
@@ -1407,6 +1589,24 @@ export function PropertiesPanel() {
1407
1589
  />
1408
1590
  )}
1409
1591
  </TabsContent>
1592
+
1593
+ <TabsContent value="raw-step" className="m-0 p-3 overflow-hidden">
1594
+ {selectedEntity && !isNativeLazySelection ? (
1595
+ <RawStepCard
1596
+ modelId={selectedEntity.modelId === 'legacy' ? '__legacy__' : selectedEntity.modelId}
1597
+ entityId={selectedEntity.expressId}
1598
+ entityType={entityType}
1599
+ dataStore={activeDataStore ?? null}
1600
+ enableEditing={editMode}
1601
+ />
1602
+ ) : (
1603
+ <p className="text-sm text-zinc-500 dark:text-zinc-500 text-center py-8 font-mono">
1604
+ {isNativeLazySelection
1605
+ ? 'Raw STEP is not available for native-metadata selections'
1606
+ : 'Select an entity to inspect raw STEP arguments'}
1607
+ </p>
1608
+ )}
1609
+ </TabsContent>
1410
1610
  </ScrollArea>
1411
1611
  </Tabs>
1412
1612
  </div>