@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.
- package/.turbo/turbo-build.log +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- 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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
833
|
-
|
|
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
|
-
|
|
907
|
-
|
|
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">
|
|
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"></></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
|
|
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>
|