@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.
Files changed (106) hide show
  1. package/.turbo/turbo-build.log +59 -44
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +488 -0
  4. package/dist/assets/{basketViewActivator-CA2CTcVo.js → basketViewActivator-Bzw51jhm.js} +6 -6
  5. package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
  6. package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
  7. package/dist/assets/exporters-u0sz2Upj.js +259119 -0
  8. package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
  9. package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
  10. package/dist/assets/ids-B7AXEv7h.js +4067 -0
  11. package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
  12. package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
  13. package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
  14. package/dist/assets/index-CSWgTe1s.css +1 -0
  15. package/dist/assets/{index-D8Epw-e7.js → index-DVNSvEMh.js} +40146 -35823
  16. package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
  17. package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
  18. package/dist/assets/{native-bridge-DKmx1z95.js → native-bridge-BiD01jI9.js} +1 -1
  19. package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
  20. package/dist/assets/{sandbox-tccwm5Bo.js → sandbox-DPD1ROr0.js} +4 -4
  21. package/dist/assets/{server-client-LoWPK1N2.js → server-client-DP8fMPY9.js} +1 -1
  22. package/dist/assets/{wasm-bridge-BsJGgPMs.js → wasm-bridge-CErti6zX.js} +1 -1
  23. package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
  24. package/dist/index.html +8 -8
  25. package/index.html +1 -1
  26. package/package.json +10 -10
  27. package/src/components/viewer/BasketPresentationDock.tsx +3 -0
  28. package/src/components/viewer/CesiumOverlay.tsx +165 -120
  29. package/src/components/viewer/DeviationPanel.tsx +172 -0
  30. package/src/components/viewer/HierarchyPanel.tsx +29 -3
  31. package/src/components/viewer/HoverTooltip.tsx +5 -0
  32. package/src/components/viewer/IDSAuditSummary.tsx +389 -0
  33. package/src/components/viewer/IDSPanel.tsx +80 -26
  34. package/src/components/viewer/MainToolbar.tsx +60 -7
  35. package/src/components/viewer/MergeLayersBanner.tsx +108 -0
  36. package/src/components/viewer/MobileToolbar.tsx +326 -0
  37. package/src/components/viewer/PointCloudClasses.tsx +111 -0
  38. package/src/components/viewer/PointCloudLegend.tsx +119 -0
  39. package/src/components/viewer/PointCloudPanel.tsx +52 -1
  40. package/src/components/viewer/PropertiesPanel.tsx +37 -6
  41. package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
  42. package/src/components/viewer/StatusBar.tsx +14 -0
  43. package/src/components/viewer/ViewerLayout.tsx +288 -95
  44. package/src/components/viewer/Viewport.tsx +86 -18
  45. package/src/components/viewer/ViewportContainer.tsx +25 -11
  46. package/src/components/viewer/ViewportOverlays.tsx +41 -26
  47. package/src/components/viewer/mouseHandlerTypes.ts +22 -0
  48. package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
  49. package/src/components/viewer/properties/MaterialCard.tsx +2 -2
  50. package/src/components/viewer/selectionHandlers.ts +41 -0
  51. package/src/components/viewer/tools/SectionPanel.tsx +181 -24
  52. package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
  53. package/src/components/viewer/useAnimationLoop.ts +22 -0
  54. package/src/components/viewer/useMouseControls.ts +296 -3
  55. package/src/components/viewer/usePointCloudSync.ts +8 -1
  56. package/src/components/viewer/useRenderUpdates.ts +21 -1
  57. package/src/components/viewer/useTouchControls.ts +100 -41
  58. package/src/hooks/federationLoadGate.test.ts +90 -0
  59. package/src/hooks/federationLoadGate.ts +127 -0
  60. package/src/hooks/ids/idsDataAccessor.ts +11 -259
  61. package/src/hooks/ingest/pointCloudIngest.ts +127 -16
  62. package/src/hooks/useDrawingGeneration.ts +81 -8
  63. package/src/hooks/useIDS.ts +90 -10
  64. package/src/hooks/useIfcFederation.ts +94 -16
  65. package/src/hooks/useIfcLoader.ts +289 -64
  66. package/src/hooks/useViewerSelectors.ts +10 -0
  67. package/src/lib/geo/cesium-bridge.ts +84 -67
  68. package/src/lib/geo/clamp-anchor.test.ts +80 -0
  69. package/src/lib/geo/clamp-anchor.ts +57 -0
  70. package/src/lib/geo/effective-georef.test.ts +79 -1
  71. package/src/lib/geo/effective-georef.ts +83 -0
  72. package/src/lib/geo/reproject.ts +26 -13
  73. package/src/lib/geo/terrain-elevation.ts +166 -0
  74. package/src/lib/lens/adapter.ts +1 -1
  75. package/src/lib/llm/context-builder.ts +1 -1
  76. package/src/lib/perf/memoryAccounting.test.ts +92 -0
  77. package/src/lib/perf/memoryAccounting.ts +235 -0
  78. package/src/sdk/adapters/mutation-view.ts +1 -1
  79. package/src/store/constants.ts +39 -2
  80. package/src/store/index.ts +6 -1
  81. package/src/store/slices/cesiumSlice.ts +1 -1
  82. package/src/store/slices/idsSlice.ts +24 -0
  83. package/src/store/slices/loadingSlice.ts +12 -0
  84. package/src/store/slices/pointCloudSlice.ts +72 -1
  85. package/src/store/slices/sectionSlice.test.ts +590 -1
  86. package/src/store/slices/sectionSlice.ts +344 -17
  87. package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
  88. package/src/store/slices/uiSlice.ts +60 -2
  89. package/src/store/types.ts +42 -0
  90. package/src/store.ts +13 -0
  91. package/src/utils/acquireFileBuffer.test.ts +231 -0
  92. package/src/utils/acquireFileBuffer.ts +128 -0
  93. package/src/utils/ifcConfig.ts +24 -0
  94. package/src/utils/nativeSpatialDataStore.ts +20 -2
  95. package/src/utils/spatialHierarchy.test.ts +116 -0
  96. package/src/utils/spatialHierarchy.ts +23 -0
  97. package/tailwind.config.js +5 -0
  98. package/tsconfig.json +1 -0
  99. package/vite.config.ts +6 -0
  100. package/dist/assets/decode-worker-Collf_X_.js +0 -1320
  101. package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
  102. package/dist/assets/exporters-xbXqEDlO.js +0 -81590
  103. package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
  104. package/dist/assets/ids-2WdONLlu.js +0 -2033
  105. package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
  106. 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 items-center justify-center h-full p-6 text-center">
567
- <FileText className="h-12 w-12 text-muted-foreground mb-4" />
568
- <h3 className="font-medium text-sm mb-2">No IDS Loaded</h3>
569
- <p className="text-xs text-muted-foreground mb-4">
570
- Load an IDS (Information Delivery Specification) file to validate your model
571
- </p>
572
- <input
573
- ref={fileInputRef}
574
- type="file"
575
- accept=".ids,.xml"
576
- className="hidden"
577
- onChange={handleFileSelect}
578
- />
579
- <Button onClick={() => { void handleLoadIdsClick(); }}>
580
- <Upload className="h-4 w-4 mr-2" />
581
- Load IDS File
582
- </Button>
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 mb-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
- <Button className="w-full" onClick={runValidation} disabled={loading}>
605
- {loading ? (
606
- <Loader2 className="h-4 w-4 mr-2 animate-spin" />
607
- ) : (
608
- <Play className="h-4 w-4 mr-2" />
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
- Run Validation
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 variant="ghost" size="icon-sm" disabled={!geometryResult && models.size === 0}>
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>Class Visibility</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
+ }