@ifc-lite/viewer 1.19.1 → 1.21.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 +59 -44
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +488 -0
- package/dist/assets/{basketViewActivator-CA2CTcVo.js → basketViewActivator-Bzw51jhm.js} +6 -6
- package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
- package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
- package/dist/assets/exporters-u0sz2Upj.js +259119 -0
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
- package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
- package/dist/assets/ids-B7AXEv7h.js +4067 -0
- package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
- package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
- package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
- package/dist/assets/index-CSWgTe1s.css +1 -0
- package/dist/assets/{index-D8Epw-e7.js → index-DVNSvEMh.js} +40146 -35823
- package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
- package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
- package/dist/assets/{native-bridge-DKmx1z95.js → native-bridge-BiD01jI9.js} +1 -1
- package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
- package/dist/assets/{sandbox-tccwm5Bo.js → sandbox-DPD1ROr0.js} +4 -4
- package/dist/assets/{server-client-LoWPK1N2.js → server-client-DP8fMPY9.js} +1 -1
- package/dist/assets/{wasm-bridge-BsJGgPMs.js → wasm-bridge-CErti6zX.js} +1 -1
- package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
- package/dist/index.html +8 -8
- package/index.html +1 -1
- package/package.json +10 -10
- package/src/components/viewer/BasketPresentationDock.tsx +3 -0
- package/src/components/viewer/CesiumOverlay.tsx +165 -120
- package/src/components/viewer/DeviationPanel.tsx +172 -0
- package/src/components/viewer/HierarchyPanel.tsx +29 -3
- package/src/components/viewer/HoverTooltip.tsx +5 -0
- package/src/components/viewer/IDSAuditSummary.tsx +389 -0
- package/src/components/viewer/IDSPanel.tsx +80 -26
- package/src/components/viewer/MainToolbar.tsx +60 -7
- package/src/components/viewer/MergeLayersBanner.tsx +108 -0
- package/src/components/viewer/MobileToolbar.tsx +326 -0
- package/src/components/viewer/PointCloudClasses.tsx +111 -0
- package/src/components/viewer/PointCloudLegend.tsx +119 -0
- package/src/components/viewer/PointCloudPanel.tsx +52 -1
- package/src/components/viewer/PropertiesPanel.tsx +37 -6
- package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
- package/src/components/viewer/StatusBar.tsx +14 -0
- package/src/components/viewer/ViewerLayout.tsx +288 -95
- package/src/components/viewer/Viewport.tsx +86 -18
- package/src/components/viewer/ViewportContainer.tsx +25 -11
- package/src/components/viewer/ViewportOverlays.tsx +41 -26
- package/src/components/viewer/mouseHandlerTypes.ts +22 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
- package/src/components/viewer/properties/MaterialCard.tsx +2 -2
- package/src/components/viewer/selectionHandlers.ts +41 -0
- package/src/components/viewer/tools/SectionPanel.tsx +181 -24
- package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
- package/src/components/viewer/useAnimationLoop.ts +22 -0
- package/src/components/viewer/useMouseControls.ts +296 -3
- package/src/components/viewer/usePointCloudSync.ts +8 -1
- package/src/components/viewer/useRenderUpdates.ts +21 -1
- package/src/components/viewer/useTouchControls.ts +100 -41
- package/src/hooks/federationLoadGate.test.ts +90 -0
- package/src/hooks/federationLoadGate.ts +127 -0
- package/src/hooks/ids/idsDataAccessor.ts +11 -259
- package/src/hooks/ingest/pointCloudIngest.ts +127 -16
- package/src/hooks/useDrawingGeneration.ts +81 -8
- package/src/hooks/useIDS.ts +90 -10
- package/src/hooks/useIfcFederation.ts +94 -16
- package/src/hooks/useIfcLoader.ts +289 -64
- package/src/hooks/useViewerSelectors.ts +10 -0
- package/src/lib/geo/cesium-bridge.ts +84 -67
- package/src/lib/geo/clamp-anchor.test.ts +80 -0
- package/src/lib/geo/clamp-anchor.ts +57 -0
- package/src/lib/geo/effective-georef.test.ts +79 -1
- package/src/lib/geo/effective-georef.ts +83 -0
- package/src/lib/geo/reproject.ts +26 -13
- package/src/lib/geo/terrain-elevation.ts +166 -0
- package/src/lib/lens/adapter.ts +1 -1
- package/src/lib/llm/context-builder.ts +1 -1
- package/src/lib/perf/memoryAccounting.test.ts +92 -0
- package/src/lib/perf/memoryAccounting.ts +235 -0
- package/src/sdk/adapters/mutation-view.ts +1 -1
- package/src/store/constants.ts +39 -2
- package/src/store/index.ts +6 -1
- package/src/store/slices/cesiumSlice.ts +1 -1
- package/src/store/slices/idsSlice.ts +24 -0
- package/src/store/slices/loadingSlice.ts +12 -0
- package/src/store/slices/pointCloudSlice.ts +72 -1
- package/src/store/slices/sectionSlice.test.ts +590 -1
- package/src/store/slices/sectionSlice.ts +344 -17
- package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
- package/src/store/slices/uiSlice.ts +60 -2
- package/src/store/types.ts +42 -0
- package/src/store.ts +13 -0
- package/src/utils/acquireFileBuffer.test.ts +231 -0
- package/src/utils/acquireFileBuffer.ts +128 -0
- package/src/utils/ifcConfig.ts +24 -0
- package/src/utils/nativeSpatialDataStore.ts +20 -2
- package/src/utils/spatialHierarchy.test.ts +116 -0
- package/src/utils/spatialHierarchy.ts +23 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +6 -0
- package/dist/assets/decode-worker-Collf_X_.js +0 -1320
- package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
- package/dist/assets/exporters-xbXqEDlO.js +0 -81590
- package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
- package/dist/assets/ids-2WdONLlu.js +0 -2033
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-BXeEKqJG.css +0 -1
|
@@ -72,6 +72,7 @@ import type {
|
|
|
72
72
|
IDSRequirementResult,
|
|
73
73
|
} from '@ifc-lite/ids';
|
|
74
74
|
import { cn } from '@/lib/utils';
|
|
75
|
+
import { IDSAuditSummary } from './IDSAuditSummary';
|
|
75
76
|
import { IDSExportDialog } from './IDSExportDialog';
|
|
76
77
|
import type { IDSBCFExportSettings, IDSExportProgress } from './IDSExportDialog';
|
|
77
78
|
import { claimNextDesktopPanelAction, subscribeDesktopPanelActions } from '@/services/desktop-panel-actions';
|
|
@@ -452,6 +453,8 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
452
453
|
const {
|
|
453
454
|
// State
|
|
454
455
|
document,
|
|
456
|
+
auditReport,
|
|
457
|
+
auditing,
|
|
455
458
|
report,
|
|
456
459
|
loading,
|
|
457
460
|
progress,
|
|
@@ -562,24 +565,43 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
562
565
|
const renderEmptyState = () => {
|
|
563
566
|
if (document) return null;
|
|
564
567
|
|
|
568
|
+
// When parse failed but the auditor still produced issues, surface
|
|
569
|
+
// them here. This is the most common path for malformed input —
|
|
570
|
+
// bare "Invalid XML format" tells the user nothing actionable, but
|
|
571
|
+
// the audit lists the specific structural problems.
|
|
572
|
+
const hasAuditIssues =
|
|
573
|
+
auditReport !== null && auditReport.issues.length > 0;
|
|
574
|
+
|
|
565
575
|
return (
|
|
566
|
-
<div className="flex flex-col
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
576
|
+
<div className="flex flex-col h-full p-6">
|
|
577
|
+
{hasAuditIssues && (
|
|
578
|
+
<div className="mb-4">
|
|
579
|
+
<IDSAuditSummary report={auditReport} auditing={auditing} />
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
|
|
583
|
+
<div className="flex flex-col items-center justify-center flex-1 text-center">
|
|
584
|
+
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
|
|
585
|
+
<h3 className="font-medium text-sm mb-2">
|
|
586
|
+
{hasAuditIssues ? 'IDS Document Has Errors' : 'No IDS Loaded'}
|
|
587
|
+
</h3>
|
|
588
|
+
<p className="text-xs text-muted-foreground mb-4">
|
|
589
|
+
{hasAuditIssues
|
|
590
|
+
? 'Fix the issues above and try loading again.'
|
|
591
|
+
: 'Load an IDS (Information Delivery Specification) file to validate your model'}
|
|
592
|
+
</p>
|
|
593
|
+
<input
|
|
594
|
+
ref={fileInputRef}
|
|
595
|
+
type="file"
|
|
596
|
+
accept=".ids,.xml"
|
|
597
|
+
className="hidden"
|
|
598
|
+
onChange={handleFileSelect}
|
|
599
|
+
/>
|
|
600
|
+
<Button onClick={() => { void handleLoadIdsClick(); }}>
|
|
601
|
+
<Upload className="h-4 w-4 mr-2" />
|
|
602
|
+
{hasAuditIssues ? 'Load Different File' : 'Load IDS File'}
|
|
603
|
+
</Button>
|
|
604
|
+
</div>
|
|
583
605
|
</div>
|
|
584
606
|
);
|
|
585
607
|
};
|
|
@@ -588,9 +610,15 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
588
610
|
const renderDocumentLoaded = () => {
|
|
589
611
|
if (!document || report) return null;
|
|
590
612
|
|
|
613
|
+
// Only the document-level auditor's `error` verdict gates model
|
|
614
|
+
// validation — warnings still let the user proceed (they're style
|
|
615
|
+
// hints, not blockers). The button keeps its primary affordance
|
|
616
|
+
// unless we genuinely can't validate.
|
|
617
|
+
const auditHasErrors = auditReport?.status === 'error';
|
|
618
|
+
|
|
591
619
|
return (
|
|
592
|
-
<div className="p-4">
|
|
593
|
-
<div className="rounded-lg border p-4
|
|
620
|
+
<div className="p-4 space-y-3">
|
|
621
|
+
<div className="rounded-lg border p-4">
|
|
594
622
|
<h3 className="font-medium text-sm mb-1">{document.info.title}</h3>
|
|
595
623
|
{document.info.description && (
|
|
596
624
|
<p className="text-xs text-muted-foreground mb-2">{document.info.description}</p>
|
|
@@ -601,14 +629,32 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
601
629
|
</div>
|
|
602
630
|
</div>
|
|
603
631
|
|
|
604
|
-
<
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
<
|
|
632
|
+
<IDSAuditSummary report={auditReport} auditing={auditing} />
|
|
633
|
+
|
|
634
|
+
<Tooltip>
|
|
635
|
+
<TooltipTrigger asChild>
|
|
636
|
+
<span className="block">
|
|
637
|
+
<Button
|
|
638
|
+
className="w-full"
|
|
639
|
+
onClick={runValidation}
|
|
640
|
+
disabled={loading || auditHasErrors}
|
|
641
|
+
variant={auditHasErrors ? 'secondary' : 'default'}
|
|
642
|
+
>
|
|
643
|
+
{loading ? (
|
|
644
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
645
|
+
) : (
|
|
646
|
+
<Play className="h-4 w-4 mr-2" />
|
|
647
|
+
)}
|
|
648
|
+
Run Validation
|
|
649
|
+
</Button>
|
|
650
|
+
</span>
|
|
651
|
+
</TooltipTrigger>
|
|
652
|
+
{auditHasErrors && (
|
|
653
|
+
<TooltipContent>
|
|
654
|
+
Resolve audit errors before validating against a model.
|
|
655
|
+
</TooltipContent>
|
|
609
656
|
)}
|
|
610
|
-
|
|
611
|
-
</Button>
|
|
657
|
+
</Tooltip>
|
|
612
658
|
</div>
|
|
613
659
|
);
|
|
614
660
|
};
|
|
@@ -619,6 +665,14 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
619
665
|
|
|
620
666
|
return (
|
|
621
667
|
<>
|
|
668
|
+
{/* Audit summary stays visible above the validation report so
|
|
669
|
+
users can still see authoring issues alongside model results. */}
|
|
670
|
+
{auditReport && auditReport.status !== 'valid' && (
|
|
671
|
+
<div className="p-3 border-b">
|
|
672
|
+
<IDSAuditSummary report={auditReport} auditing={false} />
|
|
673
|
+
</div>
|
|
674
|
+
)}
|
|
675
|
+
|
|
622
676
|
{/* Summary Header */}
|
|
623
677
|
<div className="p-3 border-b bg-muted/30">
|
|
624
678
|
<div className="flex items-center gap-2 mb-2">
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Camera,
|
|
30
30
|
Info,
|
|
31
31
|
Layers,
|
|
32
|
+
Layers2,
|
|
32
33
|
SquareX,
|
|
33
34
|
Building2,
|
|
34
35
|
Plus,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
DropdownMenuContent,
|
|
52
53
|
DropdownMenuItem,
|
|
53
54
|
DropdownMenuCheckboxItem,
|
|
55
|
+
DropdownMenuLabel,
|
|
54
56
|
DropdownMenuSeparator,
|
|
55
57
|
DropdownMenuTrigger,
|
|
56
58
|
DropdownMenuSub,
|
|
@@ -307,6 +309,12 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
307
309
|
const toggleHoverTooltips = useViewerStore((state) => state.toggleHoverTooltips);
|
|
308
310
|
const typeVisibility = useViewerStore((state) => state.typeVisibility);
|
|
309
311
|
const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
|
|
312
|
+
// Issue #540: load-time toggle that asks the WASM bridge to merge
|
|
313
|
+
// Revit-style multilayer walls. We surface this in the Class
|
|
314
|
+
// Visibility dropdown so users discover it next to the other
|
|
315
|
+
// "what shows in the scene" controls.
|
|
316
|
+
const mergeLayers = useViewerStore((state) => state.mergeLayers);
|
|
317
|
+
const setMergeLayers = useViewerStore((state) => state.setMergeLayers);
|
|
310
318
|
const resetViewerState = useViewerStore((state) => state.resetViewerState);
|
|
311
319
|
const bcfPanelVisible = useViewerStore((state) => state.bcfPanelVisible);
|
|
312
320
|
const setBcfPanelVisible = useViewerStore((state) => state.setBcfPanelVisible);
|
|
@@ -426,7 +434,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
426
434
|
// Filter to supported files (IFC, IFCX, GLB)
|
|
427
435
|
const supportedFiles = Array.from(files).filter(
|
|
428
436
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
429
|
-
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
|
|
437
|
+
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57') || f.name.toLowerCase().endsWith('.pts') || f.name.toLowerCase().endsWith('.xyz')
|
|
430
438
|
);
|
|
431
439
|
|
|
432
440
|
if (supportedFiles.length === 0) return;
|
|
@@ -467,7 +475,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
467
475
|
// Filter to supported files (IFC, IFCX, GLB)
|
|
468
476
|
const supportedFiles = Array.from(files).filter(
|
|
469
477
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
470
|
-
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
|
|
478
|
+
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57') || f.name.toLowerCase().endsWith('.pts') || f.name.toLowerCase().endsWith('.xyz')
|
|
471
479
|
);
|
|
472
480
|
|
|
473
481
|
if (supportedFiles.length === 0) return;
|
|
@@ -782,7 +790,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
782
790
|
id="file-input-open"
|
|
783
791
|
ref={fileInputRef}
|
|
784
792
|
type="file"
|
|
785
|
-
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
|
|
793
|
+
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57,.pts,.xyz"
|
|
786
794
|
multiple
|
|
787
795
|
onChange={handleFileSelect}
|
|
788
796
|
className="hidden"
|
|
@@ -790,7 +798,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
790
798
|
<input
|
|
791
799
|
ref={addModelInputRef}
|
|
792
800
|
type="file"
|
|
793
|
-
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
|
|
801
|
+
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57,.pts,.xyz"
|
|
794
802
|
multiple
|
|
795
803
|
onChange={handleAddModelSelect}
|
|
796
804
|
className="hidden"
|
|
@@ -1157,14 +1165,35 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1157
1165
|
<Tooltip>
|
|
1158
1166
|
<TooltipTrigger asChild>
|
|
1159
1167
|
<DropdownMenuTrigger asChild>
|
|
1160
|
-
<Button
|
|
1168
|
+
<Button
|
|
1169
|
+
variant="ghost"
|
|
1170
|
+
size="icon-sm"
|
|
1171
|
+
// Stay enabled even with no model loaded — the dropdown
|
|
1172
|
+
// also exposes load-time settings (Merge Multilayer
|
|
1173
|
+
// Walls) that the user should be able to set BEFORE
|
|
1174
|
+
// opening a file. Runtime items inside self-gate via
|
|
1175
|
+
// typeGeometryExists.
|
|
1176
|
+
aria-label={mergeLayers ? 'Class Visibility (Merge Multilayer Walls is on)' : 'Class Visibility'}
|
|
1177
|
+
className="relative"
|
|
1178
|
+
>
|
|
1161
1179
|
<Layers className="h-4 w-4" />
|
|
1180
|
+
{mergeLayers && (
|
|
1181
|
+
// Tiny accent dot announcing that a non-default load
|
|
1182
|
+
// setting is active. Decorative — semantics live on
|
|
1183
|
+
// the button's aria-label and the tooltip.
|
|
1184
|
+
<span
|
|
1185
|
+
aria-hidden="true"
|
|
1186
|
+
className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-primary ring-1 ring-background"
|
|
1187
|
+
/>
|
|
1188
|
+
)}
|
|
1162
1189
|
</Button>
|
|
1163
1190
|
</DropdownMenuTrigger>
|
|
1164
1191
|
</TooltipTrigger>
|
|
1165
|
-
<TooltipContent>
|
|
1192
|
+
<TooltipContent>
|
|
1193
|
+
{mergeLayers ? 'Class Visibility · Merge Multilayer Walls is on' : 'Class Visibility'}
|
|
1194
|
+
</TooltipContent>
|
|
1166
1195
|
</Tooltip>
|
|
1167
|
-
<DropdownMenuContent>
|
|
1196
|
+
<DropdownMenuContent className="w-72">
|
|
1168
1197
|
{typeGeometryExists.spaces && (
|
|
1169
1198
|
<DropdownMenuCheckboxItem
|
|
1170
1199
|
checked={typeVisibility.spaces}
|
|
@@ -1192,6 +1221,30 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1192
1221
|
Show Site
|
|
1193
1222
|
</DropdownMenuCheckboxItem>
|
|
1194
1223
|
)}
|
|
1224
|
+
|
|
1225
|
+
{/* Load-time toggles live below the runtime visibility
|
|
1226
|
+
switches — they apply on next model open rather than
|
|
1227
|
+
affecting the current scene. The subheader makes that
|
|
1228
|
+
boundary visible at a glance. */}
|
|
1229
|
+
<DropdownMenuSeparator />
|
|
1230
|
+
<DropdownMenuLabel className="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1231
|
+
Load Settings
|
|
1232
|
+
</DropdownMenuLabel>
|
|
1233
|
+
<DropdownMenuCheckboxItem
|
|
1234
|
+
checked={mergeLayers}
|
|
1235
|
+
onCheckedChange={(next) => setMergeLayers(next === true)}
|
|
1236
|
+
// Use items-start so the checkmark and icon line up with
|
|
1237
|
+
// the primary label while the description wraps below.
|
|
1238
|
+
className="items-start gap-2 py-2"
|
|
1239
|
+
>
|
|
1240
|
+
<Layers2 className="h-4 w-4 mr-2 mt-0.5 shrink-0 text-primary" />
|
|
1241
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
1242
|
+
<span className="text-sm font-medium leading-tight">Merge Multilayer Walls</span>
|
|
1243
|
+
<span className="text-[11px] leading-tight text-muted-foreground">
|
|
1244
|
+
Render walls as 1 solid · Applies on reload
|
|
1245
|
+
</span>
|
|
1246
|
+
</div>
|
|
1247
|
+
</DropdownMenuCheckboxItem>
|
|
1195
1248
|
</DropdownMenuContent>
|
|
1196
1249
|
</DropdownMenu>
|
|
1197
1250
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reload-to-apply banner for the "Merge Multilayer Walls" load-time
|
|
7
|
+
* toggle (issue #540). The user flips the toggle in the Class
|
|
8
|
+
* Visibility dropdown; when a model is already loaded, the UI sets
|
|
9
|
+
* `mergeLayersPendingReload` and we surface this non-modal banner
|
|
10
|
+
* above the canvas asking the user to reload.
|
|
11
|
+
*
|
|
12
|
+
* Design note: this codebase has no "reload current model" function
|
|
13
|
+
* — `useIfcLoader.loadFile` is one-shot and does not retain the
|
|
14
|
+
* source File / NativeFileHandle. The pragmatic approach here is to
|
|
15
|
+
* call `window.location.reload()` for the Reload button, which is
|
|
16
|
+
* exactly what the wording promises ("Reload model to apply") and
|
|
17
|
+
* works on both web and the Tauri shell (which keeps its window).
|
|
18
|
+
* If a true in-place reload lands later, swap the handler — the
|
|
19
|
+
* banner contract stays the same.
|
|
20
|
+
*/
|
|
21
|
+
import { useCallback } from 'react';
|
|
22
|
+
import { Layers2, RefreshCw, X } from 'lucide-react';
|
|
23
|
+
import { useViewerStore } from '@/store';
|
|
24
|
+
import { Button } from '@/components/ui/button';
|
|
25
|
+
import { cn } from '@/lib/utils';
|
|
26
|
+
|
|
27
|
+
export interface MergeLayersBannerProps {
|
|
28
|
+
/**
|
|
29
|
+
* When set, this overrides the default `window.location.reload()`
|
|
30
|
+
* fallback. Once a true "reload current model in place" path lands,
|
|
31
|
+
* the caller can pass it in here without changing the banner's
|
|
32
|
+
* visual contract.
|
|
33
|
+
*/
|
|
34
|
+
onReload?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function MergeLayersBanner({ onReload }: MergeLayersBannerProps) {
|
|
38
|
+
const pending = useViewerStore((s) => s.mergeLayersPendingReload);
|
|
39
|
+
const merging = useViewerStore((s) => s.mergeLayers);
|
|
40
|
+
const dismiss = useViewerStore((s) => s.clearMergeLayersPendingReload);
|
|
41
|
+
|
|
42
|
+
const handleReload = useCallback(() => {
|
|
43
|
+
if (onReload) {
|
|
44
|
+
onReload();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Full-page reload is the only path we can guarantee works: the
|
|
48
|
+
// viewer doesn't retain the source File/handle once loading
|
|
49
|
+
// completes, so we can't re-run loadFile with the original input.
|
|
50
|
+
// The toggle is already persisted in localStorage so it will pick
|
|
51
|
+
// up the new value on the next boot.
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
window.location.reload();
|
|
54
|
+
}
|
|
55
|
+
}, [onReload]);
|
|
56
|
+
|
|
57
|
+
if (!pending) return null;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
// Centred non-modal overlay anchored to the top of the canvas.
|
|
61
|
+
// pointer-events-none on the wrapper lets clicks pass through
|
|
62
|
+
// unless they land on the inner card, so the underlying 3D
|
|
63
|
+
// viewport stays interactive.
|
|
64
|
+
<div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 z-40 max-w-[min(640px,calc(100%-1.5rem))] w-fit">
|
|
65
|
+
<div
|
|
66
|
+
role="status"
|
|
67
|
+
aria-live="polite"
|
|
68
|
+
className={cn(
|
|
69
|
+
'pointer-events-auto flex items-center gap-3 border border-primary/40 bg-background/95 backdrop-blur',
|
|
70
|
+
'px-3 py-2 shadow-[0_8px_24px_-12px_rgba(0,0,0,0.45)] rounded-md',
|
|
71
|
+
'animate-in slide-in-from-top-2 fade-in-0 duration-200',
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
|
|
75
|
+
<Layers2 className="h-4 w-4" />
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex flex-col leading-tight min-w-0">
|
|
78
|
+
<span className="text-xs font-semibold text-foreground">
|
|
79
|
+
Merge Multilayer Walls {merging ? 'enabled' : 'disabled'}
|
|
80
|
+
</span>
|
|
81
|
+
<span className="text-[11px] text-muted-foreground truncate">
|
|
82
|
+
Reload model to apply the new setting.
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="flex items-center gap-1.5 ml-2">
|
|
86
|
+
<Button
|
|
87
|
+
size="sm"
|
|
88
|
+
variant="default"
|
|
89
|
+
className="h-7 px-2.5 gap-1.5 text-[11px] font-semibold uppercase tracking-wider"
|
|
90
|
+
onClick={handleReload}
|
|
91
|
+
>
|
|
92
|
+
<RefreshCw className="h-3.5 w-3.5" />
|
|
93
|
+
Reload
|
|
94
|
+
</Button>
|
|
95
|
+
<Button
|
|
96
|
+
size="icon-sm"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
className="h-7 w-7"
|
|
99
|
+
onClick={dismiss}
|
|
100
|
+
aria-label="Dismiss reload reminder"
|
|
101
|
+
>
|
|
102
|
+
<X className="h-3.5 w-3.5" />
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|