@ifc-lite/viewer 1.17.6 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +20 -15
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +949 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/sandbox-Baez7n-t.js +9682 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +11 -10
- 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/ChatPanel.tsx +64 -2
- 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 -12
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- 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/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +29 -2
- package/src/components/viewer/ViewportContainer.tsx +45 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -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 +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- 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 +581 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +89 -13
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -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 +6 -0
- 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/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/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +8 -3
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +79 -1
- 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/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/pointCloudSlice.ts +102 -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/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/types.ts +7 -0
- package/src/store.ts +14 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
|
@@ -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
|
|
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(() => {
|
|
@@ -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
|
|
840
|
-
|
|
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
|
-
|
|
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">
|
|
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"></></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
|
|
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>
|