@ifc-lite/viewer 1.17.4 → 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 (196) hide show
  1. package/.turbo/turbo-build.log +20 -17
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +630 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -1
  5. package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  6. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  7. package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
  8. package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
  9. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  10. package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
  11. package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
  12. package/dist/assets/index-COnQRuqY.css +1 -0
  13. package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
  14. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  15. package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
  16. package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
  17. package/dist/index.html +8 -7
  18. package/index.html +1 -0
  19. package/package.json +13 -13
  20. package/src/App.tsx +16 -2
  21. package/src/apache-arrow.d.ts +30 -0
  22. package/src/components/viewer/AddElementPanel.tsx +758 -0
  23. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  24. package/src/components/viewer/CesiumOverlay.tsx +62 -19
  25. package/src/components/viewer/ChatPanel.tsx +259 -93
  26. package/src/components/viewer/CommandPalette.tsx +56 -7
  27. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  28. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  29. package/src/components/viewer/ExportDialog.tsx +19 -1
  30. package/src/components/viewer/MainToolbar.tsx +73 -13
  31. package/src/components/viewer/PropertiesPanel.tsx +237 -23
  32. package/src/components/viewer/SearchInline.tsx +669 -0
  33. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  34. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  35. package/src/components/viewer/SearchModal.text.tsx +388 -0
  36. package/src/components/viewer/SearchModal.tsx +235 -0
  37. package/src/components/viewer/SettingsPage.tsx +252 -101
  38. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  39. package/src/components/viewer/ToolOverlays.tsx +5 -0
  40. package/src/components/viewer/ViewerLayout.tsx +25 -4
  41. package/src/components/viewer/Viewport.tsx +25 -3
  42. package/src/components/viewer/ViewportContainer.tsx +51 -64
  43. package/src/components/viewer/ViewportOverlays.tsx +5 -2
  44. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  45. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  46. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  47. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  48. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  49. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  50. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  51. package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
  52. package/src/components/viewer/properties/LocationMap.tsx +9 -7
  53. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  54. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  55. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  56. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  57. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  58. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  59. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  60. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  61. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  62. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  63. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  64. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  65. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  66. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  67. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  68. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  69. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  70. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  71. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  72. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  73. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  74. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  75. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  76. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  77. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  78. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  79. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  80. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  81. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  82. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  83. package/src/components/viewer/selectionHandlers.ts +446 -0
  84. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  85. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  86. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  87. package/src/components/viewer/useAnimationLoop.ts +9 -1
  88. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  89. package/src/components/viewer/useMouseControls.ts +9 -1
  90. package/src/components/viewer/useRenderUpdates.ts +1 -1
  91. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  92. package/src/hooks/ingest/viewerModelIngest.ts +7 -2
  93. package/src/hooks/useIfcFederation.ts +326 -71
  94. package/src/hooks/useIfcLoader.ts +23 -10
  95. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  96. package/src/hooks/useSandbox.ts +1 -1
  97. package/src/hooks/useSearchIndex.ts +125 -0
  98. package/src/hooks/useViewControls.ts +13 -5
  99. package/src/index.css +550 -10
  100. package/src/lib/desktop-entitlement.ts +2 -4
  101. package/src/lib/geo/cesium-bridge.ts +15 -7
  102. package/src/lib/geo/effective-georef.test.ts +73 -0
  103. package/src/lib/geo/effective-georef.ts +111 -0
  104. package/src/lib/geo/reproject.ts +105 -19
  105. package/src/lib/llm/byok-guard.test.ts +77 -0
  106. package/src/lib/llm/byok-guard.ts +39 -0
  107. package/src/lib/llm/free-models.test.ts +0 -6
  108. package/src/lib/llm/models.ts +104 -42
  109. package/src/lib/llm/stream-client.ts +74 -110
  110. package/src/lib/llm/stream-direct.test.ts +130 -0
  111. package/src/lib/llm/stream-direct.ts +316 -0
  112. package/src/lib/llm/system-prompt.test.ts +14 -0
  113. package/src/lib/llm/system-prompt.ts +102 -1
  114. package/src/lib/llm/types.ts +20 -2
  115. package/src/lib/recent-files.ts +38 -4
  116. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  117. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  118. package/src/lib/scripts/templates.ts +7 -0
  119. package/src/lib/search/common-ifc-types.ts +36 -0
  120. package/src/lib/search/filter-evaluate.test.ts +537 -0
  121. package/src/lib/search/filter-evaluate.ts +610 -0
  122. package/src/lib/search/filter-rules.test.ts +119 -0
  123. package/src/lib/search/filter-rules.ts +198 -0
  124. package/src/lib/search/filter-schema.test.ts +233 -0
  125. package/src/lib/search/filter-schema.ts +146 -0
  126. package/src/lib/search/recent-searches.test.ts +116 -0
  127. package/src/lib/search/recent-searches.ts +93 -0
  128. package/src/lib/search/result-export.test.ts +101 -0
  129. package/src/lib/search/result-export.ts +104 -0
  130. package/src/lib/search/saved-filters.test.ts +118 -0
  131. package/src/lib/search/saved-filters.ts +154 -0
  132. package/src/lib/search/tier0-scan.test.ts +196 -0
  133. package/src/lib/search/tier0-scan.ts +237 -0
  134. package/src/lib/search/tier1-index.test.ts +242 -0
  135. package/src/lib/search/tier1-index.ts +448 -0
  136. package/src/main.tsx +1 -10
  137. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  138. package/src/sdk/adapters/export-adapter.ts +404 -1
  139. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  140. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  141. package/src/sdk/adapters/model-compat.ts +8 -2
  142. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  143. package/src/sdk/adapters/store-adapter.ts +201 -0
  144. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  145. package/src/sdk/local-backend.ts +16 -8
  146. package/src/services/api-keys.ts +73 -0
  147. package/src/services/desktop-export.ts +3 -1
  148. package/src/services/desktop-native-metadata.ts +41 -18
  149. package/src/services/file-dialog.ts +4 -1
  150. package/src/services/tauri-modules.d.ts +25 -0
  151. package/src/store/basketVisibleSet.ts +3 -0
  152. package/src/store/constants.ts +20 -2
  153. package/src/store/globalId.ts +4 -1
  154. package/src/store/index.ts +82 -6
  155. package/src/store/slices/addElementMeshes.ts +365 -0
  156. package/src/store/slices/addElementSlice.ts +275 -0
  157. package/src/store/slices/annotationsSlice.test.ts +133 -0
  158. package/src/store/slices/annotationsSlice.ts +251 -0
  159. package/src/store/slices/cesiumSlice.ts +5 -0
  160. package/src/store/slices/chatSlice.test.ts +6 -76
  161. package/src/store/slices/chatSlice.ts +17 -58
  162. package/src/store/slices/dataSlice.test.ts +23 -4
  163. package/src/store/slices/dataSlice.ts +1 -1
  164. package/src/store/slices/modelSlice.test.ts +67 -9
  165. package/src/store/slices/modelSlice.ts +39 -7
  166. package/src/store/slices/mutationSlice.ts +964 -3
  167. package/src/store/slices/overlayCompositor.test.ts +164 -0
  168. package/src/store/slices/overlaySlice.test.ts +93 -0
  169. package/src/store/slices/overlaySlice.ts +151 -0
  170. package/src/store/slices/pinboardSlice.test.ts +6 -1
  171. package/src/store/slices/playbackSlice.ts +128 -0
  172. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  173. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  174. package/src/store/slices/scheduleSlice.test.ts +694 -0
  175. package/src/store/slices/scheduleSlice.ts +1330 -0
  176. package/src/store/slices/searchSlice.test.ts +342 -0
  177. package/src/store/slices/searchSlice.ts +341 -0
  178. package/src/store/slices/sectionSlice.test.ts +87 -7
  179. package/src/store/slices/sectionSlice.ts +151 -5
  180. package/src/store/slices/selectionSlice.test.ts +46 -0
  181. package/src/store/slices/selectionSlice.ts +20 -0
  182. package/src/store/slices/uiSlice.ts +28 -5
  183. package/src/store/types.ts +26 -0
  184. package/src/store.ts +14 -0
  185. package/src/utils/nativeSpatialDataStore.ts +4 -1
  186. package/src/utils/viewportUtils.ts +7 -2
  187. package/src/vite-env.d.ts +0 -4
  188. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  189. package/dist/assets/ids-B4jTqB1O.js +0 -1
  190. package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
  191. package/dist/assets/index-DckuDqlv.css +0 -1
  192. package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
  193. package/src/components/viewer/UpgradePage.tsx +0 -71
  194. package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
  195. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  196. package/src/lib/llm/clerk-auth.ts +0 -62
@@ -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, 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(() => {
@@ -554,6 +685,13 @@ export function PropertiesPanel() {
554
685
  return info?.hasGeoreference ? info : null;
555
686
  }, [model, ifcDataStore]);
556
687
 
688
+ // Extract IFC length unit scale (e.g. 0.001 for mm, 0.3048 for ft)
689
+ const lengthUnitScale = useMemo(() => {
690
+ const dataStore = model?.ifcDataStore ?? ifcDataStore;
691
+ if (!dataStore?.source?.length || !dataStore?.entityIndex) return 1;
692
+ return extractLengthUnitScale(dataStore.source, dataStore.entityIndex);
693
+ }, [model, ifcDataStore]);
694
+
557
695
  // Extract type-level properties (e.g., from IfcWallType's HasPropertySets)
558
696
  const typeProperties = useMemo(() => {
559
697
  if (!selectedEntity) return null;
@@ -822,15 +960,19 @@ export function PropertiesPanel() {
822
960
 
823
961
  const renderedEntityType = isNativeLazySelection
824
962
  ? (nativeDetails?.summary.type ?? 'Loading...')
825
- : (entityNode?.type ?? 'Unknown');
963
+ : (entityNode?.type ?? overlayEntity?.type ?? 'Unknown');
826
964
  const renderedEntityName = isNativeLazySelection
827
965
  ? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
828
- : entityNode?.name;
966
+ : (entityNode?.name ?? overlayAttr(2) ?? undefined);
829
967
  const renderedEntityGlobalId = isNativeLazySelection
830
968
  ? (nativeDetails?.summary.globalId ?? null)
831
- : entityNode?.globalId;
832
- const renderedEntityDescription = isNativeLazySelection ? undefined : entityNode?.description;
833
- 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);
834
976
  const renderedSpatialInfo = isNativeLazySelection ? nativeSpatialInfo : spatialInfo;
835
977
  const renderedOccurrenceProperties = isNativeLazySelection ? nativeOccurrenceProperties : occurrenceProperties;
836
978
  const renderedInheritedTypeProperties = isNativeLazySelection ? [] : inheritedTypeProperties;
@@ -903,8 +1045,18 @@ export function PropertiesPanel() {
903
1045
  );
904
1046
  }
905
1047
 
906
- if (!selectedEntityId || (!isNativeLazySelection && (!modelQuery || !entityNode))) {
907
- // Show model metadata when a single legacy model is loaded and nothing selected
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)))) {
1054
+ // Show model metadata when a single model is loaded and nothing selected.
1055
+ // Handles both federated models (models.size >= 1) and legacy single-model path (models.size === 0).
1056
+ if (models.size === 1) {
1057
+ const singleModel = models.values().next().value as FederatedModel;
1058
+ return <ModelMetadataPanel model={singleModel} />;
1059
+ }
908
1060
  if (ifcDataStore && models.size === 0 && geometryResult) {
909
1061
  const legacyModel: FederatedModel = {
910
1062
  id: '__legacy__',
@@ -921,10 +1073,11 @@ export function PropertiesPanel() {
921
1073
  };
922
1074
  return <ModelMetadataPanel model={legacyModel} />;
923
1075
  }
1076
+ // Multi-model or no model loaded: show empty state
924
1077
  return (
925
1078
  <div className="h-full flex flex-col border-l-2 border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-black">
926
1079
  <div className="p-3 border-b-2 border-zinc-200 dark:border-zinc-800 bg-white dark:bg-black">
927
- <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>
928
1081
  </div>
929
1082
  <div className="flex-1 flex flex-col items-center justify-center text-center p-6 bg-white dark:bg-black">
930
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">
@@ -1155,6 +1308,7 @@ export function PropertiesPanel() {
1155
1308
  schemaVersion={activeDataStore?.schemaVersion}
1156
1309
  coordinateInfo={(model?.geometryResult ?? geometryResult)?.coordinateInfo}
1157
1310
  geometryResult={model?.geometryResult ?? geometryResult}
1311
+ lengthUnitScale={lengthUnitScale}
1158
1312
  />
1159
1313
  </CollapsibleContent>
1160
1314
  </Collapsible>
@@ -1252,10 +1406,32 @@ export function PropertiesPanel() {
1252
1406
  <Tag className="h-3 w-3 shrink-0 panel-compact-icon" />
1253
1407
  <span className="panel-compact-text">bSDD</span>
1254
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>
1255
1421
  </TabsList>
1256
1422
 
1257
1423
  <ScrollArea className="flex-1 bg-white dark:bg-black">
1258
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
+ )}
1259
1435
  {/* Edit toolbar - only shown when edit mode is active */}
1260
1436
  {editMode && selectedEntity && !isNativeLazySelection && (
1261
1437
  <EditToolbar
@@ -1267,7 +1443,12 @@ export function PropertiesPanel() {
1267
1443
  schemaVersion={activeDataStore?.schemaVersion}
1268
1444
  />
1269
1445
  )}
1270
- {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 ? (
1271
1452
  <p className="text-sm text-zinc-500 dark:text-zinc-500 text-center py-8 font-mono">No property sets</p>
1272
1453
  ) : (
1273
1454
  <div className="space-y-3 w-full overflow-hidden">
@@ -1363,6 +1544,21 @@ export function PropertiesPanel() {
1363
1544
  <RelationshipsCard relationships={renderedEntityRelationships} />
1364
1545
  </>
1365
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
+ )}
1366
1562
  </div>
1367
1563
  )}
1368
1564
  </TabsContent>
@@ -1393,6 +1589,24 @@ export function PropertiesPanel() {
1393
1589
  />
1394
1590
  )}
1395
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>
1396
1610
  </ScrollArea>
1397
1611
  </Tabs>
1398
1612
  </div>