@ifc-lite/viewer 1.14.4 → 1.16.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/CHANGELOG.md +41 -0
- package/dist/assets/{Arrow.dom-_vGzMMKs.js → Arrow.dom--gdrQd-q.js} +1 -1
- package/dist/assets/{basketViewActivator-BZcoCL3V.js → basketViewActivator-CI3y6VYQ.js} +1 -1
- package/dist/assets/{browser-Czmf34bo.js → browser-vWDubxDI.js} +1 -1
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-BImINgzG.js +187371 -0
- package/dist/assets/{index-D7nEDctQ.js → index-RXIK18da.js} +4 -4
- package/dist/assets/index-ax1X2WPd.css +1 -0
- package/dist/assets/{native-bridge-DAOWftxE.js → native-bridge-4rLidc3f.js} +1 -1
- package/dist/assets/{wasm-bridge-D7jYpn8a.js → wasm-bridge-BkfXfw8O.js} +1 -1
- package/dist/index.html +7 -2
- package/index.html +5 -0
- package/package.json +9 -9
- package/src/components/viewer/ExportDialog.tsx +40 -2
- package/src/components/viewer/HierarchyPanel.tsx +99 -22
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +184 -82
- package/src/components/viewer/ViewportContainer.tsx +30 -25
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +26 -20
- package/src/components/viewer/hierarchy/ifc-icons.ts +90 -0
- package/src/hooks/useIfcCache.ts +9 -9
- package/src/hooks/useKeyboardShortcuts.ts +28 -2
- package/src/sdk/adapters/visibility-adapter.ts +82 -2
- package/src/store/basketVisibleSet.ts +72 -4
- package/src/store/index.ts +11 -1
- package/src/store/slices/pinboardSlice.ts +46 -45
- package/src/store/slices/visibilitySlice.ts +28 -2
- package/src/utils/spatialHierarchy.ts +1 -1
- package/src/vite-env.d.ts +6 -2
- package/vite.config.ts +75 -23
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-CMQ_Dgkr.css +0 -1
- package/dist/assets/index-DX-Qf5fA.js +0 -116950
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
|
-
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
5
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
6
6
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
7
7
|
import {
|
|
8
8
|
Search,
|
|
@@ -46,6 +46,12 @@ export function HierarchyPanel() {
|
|
|
46
46
|
const setStoreysSelection = useViewerStore((s) => s.setStoreysSelection);
|
|
47
47
|
const clearStoreySelection = useViewerStore((s) => s.clearStoreySelection);
|
|
48
48
|
const isolateEntities = useViewerStore((s) => s.isolateEntities);
|
|
49
|
+
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
50
|
+
const clearIsolation = useViewerStore((s) => s.clearIsolation);
|
|
51
|
+
const classFilter = useViewerStore((s) => s.classFilter);
|
|
52
|
+
const setClassFilter = useViewerStore((s) => s.setClassFilter);
|
|
53
|
+
const clearClassFilter = useViewerStore((s) => s.clearClassFilter);
|
|
54
|
+
const clearAllFilters = useViewerStore((s) => s.clearAllFilters);
|
|
49
55
|
const setHierarchyBasketSelection = useViewerStore((s) => s.setHierarchyBasketSelection);
|
|
50
56
|
|
|
51
57
|
const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
|
|
@@ -54,6 +60,26 @@ export function HierarchyPanel() {
|
|
|
54
60
|
const toggleEntityVisibility = useViewerStore((s) => s.toggleEntityVisibility);
|
|
55
61
|
const clearSelection = useViewerStore((s) => s.clearSelection);
|
|
56
62
|
|
|
63
|
+
// Derive label for type isolation (from Type tab) by checking mesh ifcType
|
|
64
|
+
const typeIsolationLabel = useMemo(() => {
|
|
65
|
+
if (!isolatedEntities || isolatedEntities.size === 0) return null;
|
|
66
|
+
const sampleId = isolatedEntities.values().next().value!;
|
|
67
|
+
for (const [, model] of models) {
|
|
68
|
+
const gr = model.geometryResult;
|
|
69
|
+
if (!gr?.meshes) continue;
|
|
70
|
+
const offset = model.idOffset ?? 0;
|
|
71
|
+
const mesh = gr.meshes.find((m: { expressId: number }) => m.expressId + offset === sampleId);
|
|
72
|
+
if (mesh?.ifcType) return mesh.ifcType;
|
|
73
|
+
}
|
|
74
|
+
if (geometryResult?.meshes) {
|
|
75
|
+
const mesh = geometryResult.meshes.find((m: { expressId: number }) => m.expressId === sampleId);
|
|
76
|
+
if (mesh?.ifcType) return mesh.ifcType;
|
|
77
|
+
}
|
|
78
|
+
return `${isolatedEntities.size} elements`;
|
|
79
|
+
}, [isolatedEntities, models, geometryResult]);
|
|
80
|
+
|
|
81
|
+
const hasActiveFilters = selectedStoreys.size > 0 || isolatedEntities !== null || classFilter !== null;
|
|
82
|
+
|
|
57
83
|
// Resizable panel split (percentage for storeys section, 0.5 = 50%)
|
|
58
84
|
const [splitRatio, setSplitRatio] = useState(0.5);
|
|
59
85
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -198,15 +224,20 @@ export function HierarchyPanel() {
|
|
|
198
224
|
}]);
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
// Type group nodes - click to isolate entities, expand via chevron only
|
|
227
|
+
// Type group nodes - click to filter/isolate entities, expand via chevron only
|
|
202
228
|
if (node.type === 'type-group') {
|
|
203
229
|
const elements = getNodeElements(node);
|
|
204
230
|
if (elements.length > 0) {
|
|
205
|
-
// Clear multi-selection highlight
|
|
206
|
-
// but we don't want every element highlighted/selected
|
|
231
|
+
// Clear multi-selection highlight
|
|
207
232
|
setSelectedEntityIds([]);
|
|
208
233
|
setSelectedEntity(resolveEntityRef(elements[0]));
|
|
209
|
-
|
|
234
|
+
if (groupingMode === 'type') {
|
|
235
|
+
// Class tab → class filter (combinable with storey + type isolation)
|
|
236
|
+
setClassFilter(elements, node.ifcType || node.name);
|
|
237
|
+
} else {
|
|
238
|
+
// Type tab → type isolation (combinable with storey + class filter)
|
|
239
|
+
isolateEntities(elements);
|
|
240
|
+
}
|
|
210
241
|
}
|
|
211
242
|
return;
|
|
212
243
|
}
|
|
@@ -357,7 +388,7 @@ export function HierarchyPanel() {
|
|
|
357
388
|
setSelectedEntity(resolveEntityRef(globalId));
|
|
358
389
|
}
|
|
359
390
|
}
|
|
360
|
-
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntityIds, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements, setHierarchyBasketSelection, toGlobalId]);
|
|
391
|
+
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntityIds, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements, setHierarchyBasketSelection, toGlobalId, groupingMode, setClassFilter]);
|
|
361
392
|
|
|
362
393
|
// Compute selection and visibility state for a node
|
|
363
394
|
const computeNodeState = useCallback((node: TreeNode): { isSelected: boolean; nodeHidden: boolean; modelVisible?: boolean } => {
|
|
@@ -551,21 +582,44 @@ export function HierarchyPanel() {
|
|
|
551
582
|
</div>
|
|
552
583
|
|
|
553
584
|
{/* Footer status */}
|
|
554
|
-
{
|
|
585
|
+
{hasActiveFilters ? (
|
|
555
586
|
<div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 bg-primary text-white dark:bg-primary">
|
|
556
|
-
<div className="flex items-center justify-between text-xs font-medium">
|
|
557
|
-
<
|
|
558
|
-
{selectedStoreys.size
|
|
559
|
-
|
|
560
|
-
|
|
587
|
+
<div className="flex items-center justify-between text-xs font-medium gap-2">
|
|
588
|
+
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
|
589
|
+
{selectedStoreys.size > 0 && (
|
|
590
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
591
|
+
{selectedStoreys.size} {selectedStoreys.size === 1 ? 'Storey' : 'Storeys'}
|
|
592
|
+
<button onClick={clearStoreySelection} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear storey filter">×</button>
|
|
593
|
+
</span>
|
|
594
|
+
)}
|
|
595
|
+
{classFilter !== null && (
|
|
596
|
+
<>
|
|
597
|
+
{selectedStoreys.size > 0 && <span className="text-[10px] opacity-50">+</span>}
|
|
598
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
599
|
+
{classFilter.label}
|
|
600
|
+
<button onClick={clearClassFilter} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear class filter">×</button>
|
|
601
|
+
</span>
|
|
602
|
+
</>
|
|
603
|
+
)}
|
|
604
|
+
{isolatedEntities !== null && (
|
|
605
|
+
<>
|
|
606
|
+
{(selectedStoreys.size > 0 || classFilter !== null) && <span className="text-[10px] opacity-50">+</span>}
|
|
607
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
608
|
+
{typeIsolationLabel}
|
|
609
|
+
<button onClick={clearIsolation} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear type filter">×</button>
|
|
610
|
+
</span>
|
|
611
|
+
</>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
561
615
|
<span className="opacity-70 text-[10px] font-mono">ESC</span>
|
|
562
616
|
<Button
|
|
563
617
|
variant="ghost"
|
|
564
618
|
size="sm"
|
|
565
619
|
className="h-6 text-[10px] uppercase border border-white/20 hover:bg-white/20 hover:text-white rounded-none px-2"
|
|
566
|
-
onClick={clearStoreySelection}
|
|
620
|
+
onClick={() => { clearStoreySelection(); clearAllFilters(); }}
|
|
567
621
|
>
|
|
568
|
-
Clear
|
|
622
|
+
Clear all
|
|
569
623
|
</Button>
|
|
570
624
|
</div>
|
|
571
625
|
</div>
|
|
@@ -614,21 +668,44 @@ export function HierarchyPanel() {
|
|
|
614
668
|
</div>
|
|
615
669
|
|
|
616
670
|
{/* Footer status */}
|
|
617
|
-
{
|
|
671
|
+
{hasActiveFilters ? (
|
|
618
672
|
<div className="p-2 border-t-2 border-zinc-200 dark:border-zinc-800 bg-primary text-white dark:bg-primary">
|
|
619
|
-
<div className="flex items-center justify-between text-xs font-medium">
|
|
620
|
-
<
|
|
621
|
-
{selectedStoreys.size
|
|
622
|
-
|
|
623
|
-
|
|
673
|
+
<div className="flex items-center justify-between text-xs font-medium gap-2">
|
|
674
|
+
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
|
675
|
+
{selectedStoreys.size > 0 && (
|
|
676
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
677
|
+
{selectedStoreys.size} {selectedStoreys.size === 1 ? 'Storey' : 'Storeys'}
|
|
678
|
+
<button onClick={clearStoreySelection} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear storey filter">×</button>
|
|
679
|
+
</span>
|
|
680
|
+
)}
|
|
681
|
+
{classFilter !== null && (
|
|
682
|
+
<>
|
|
683
|
+
{selectedStoreys.size > 0 && <span className="text-[10px] opacity-50">+</span>}
|
|
684
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
685
|
+
{classFilter.label}
|
|
686
|
+
<button onClick={clearClassFilter} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear class filter">×</button>
|
|
687
|
+
</span>
|
|
688
|
+
</>
|
|
689
|
+
)}
|
|
690
|
+
{isolatedEntities !== null && (
|
|
691
|
+
<>
|
|
692
|
+
{(selectedStoreys.size > 0 || classFilter !== null) && <span className="text-[10px] opacity-50">+</span>}
|
|
693
|
+
<span className="inline-flex items-center gap-1 bg-white/15 rounded px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
|
694
|
+
{typeIsolationLabel}
|
|
695
|
+
<button onClick={clearIsolation} className="ml-0.5 opacity-60 hover:opacity-100 text-xs leading-none" aria-label="Clear type filter">×</button>
|
|
696
|
+
</span>
|
|
697
|
+
</>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
624
701
|
<span className="opacity-70 text-[10px] font-mono">ESC</span>
|
|
625
702
|
<Button
|
|
626
703
|
variant="ghost"
|
|
627
704
|
size="sm"
|
|
628
705
|
className="h-6 text-[10px] uppercase border border-white/20 hover:bg-white/20 hover:text-white rounded-none px-2"
|
|
629
|
-
onClick={clearStoreySelection}
|
|
706
|
+
onClick={() => { clearStoreySelection(); clearAllFilters(); }}
|
|
630
707
|
>
|
|
631
|
-
Clear
|
|
708
|
+
Clear all
|
|
632
709
|
</Button>
|
|
633
710
|
</div>
|
|
634
711
|
</div>
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
|
|
5
5
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
6
|
-
import { X, Info, Keyboard, Github, ExternalLink, Sparkles, ChevronDown, Zap, Wrench, Plus } from 'lucide-react';
|
|
6
|
+
import { X, Info, Keyboard, Github, ExternalLink, Sparkles, ChevronDown, ChevronRight, Zap, Wrench, Plus, Package } from 'lucide-react';
|
|
7
7
|
import { Button } from '@/components/ui/button';
|
|
8
8
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
9
9
|
import { KEYBOARD_SHORTCUTS } from '@/hooks/useKeyboardShortcuts';
|
|
10
10
|
|
|
11
11
|
const GITHUB_URL = 'https://github.com/louistrue/ifc-lite';
|
|
12
|
-
const INITIAL_RELEASE_COUNT = 5;
|
|
13
12
|
|
|
14
13
|
interface InfoDialogProps {
|
|
15
14
|
open: boolean;
|
|
@@ -35,85 +34,172 @@ const TYPE_CONFIG = {
|
|
|
35
34
|
} as const;
|
|
36
35
|
|
|
37
36
|
function AboutTab() {
|
|
37
|
+
const [showPackages, setShowPackages] = useState(false);
|
|
38
|
+
const packageVersions = __PACKAGE_VERSIONS__;
|
|
39
|
+
|
|
38
40
|
return (
|
|
39
|
-
<div className="space-y-
|
|
41
|
+
<div className="space-y-3">
|
|
40
42
|
{/* Header */}
|
|
41
|
-
<div className="text-center pb-
|
|
43
|
+
<div className="text-center pb-2 border-b">
|
|
42
44
|
<h3 className="text-xl font-bold">ifc-lite</h3>
|
|
43
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
44
|
-
Version {__APP_VERSION__}
|
|
45
|
-
</p>
|
|
46
45
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
47
|
-
|
|
48
|
-
</p>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
{/* Description */}
|
|
52
|
-
<div className="space-y-2">
|
|
53
|
-
<p className="text-sm">
|
|
54
|
-
A high-performance IFC viewer for BIM models, built with WebGPU.
|
|
46
|
+
v{__APP_VERSION__} · {formatBuildDate(__BUILD_DATE__)}
|
|
55
47
|
</p>
|
|
56
48
|
</div>
|
|
57
49
|
|
|
58
|
-
{/* Features */}
|
|
59
|
-
<div className="space-y-2">
|
|
60
|
-
<h4 className="text-sm font-medium">Features</h4>
|
|
61
|
-
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
|
62
|
-
<li>WebGPU-accelerated 3D rendering</li>
|
|
63
|
-
<li>IFC4 and IFC5/IFCX format support</li>
|
|
64
|
-
<li>Multi-model federation</li>
|
|
65
|
-
<li>Spatial hierarchy navigation</li>
|
|
66
|
-
<li>Section planes and measurements</li>
|
|
67
|
-
<li>Property inspection</li>
|
|
68
|
-
</ul>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
50
|
{/* Links */}
|
|
72
|
-
<div className="
|
|
51
|
+
<div className="flex items-center justify-center gap-4 text-xs">
|
|
73
52
|
<a
|
|
74
53
|
href={GITHUB_URL}
|
|
75
54
|
target="_blank"
|
|
76
55
|
rel="noopener noreferrer"
|
|
77
|
-
className="flex items-center gap-
|
|
56
|
+
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
78
57
|
>
|
|
79
|
-
<Github className="h-
|
|
80
|
-
|
|
81
|
-
<ExternalLink className="h-3 w-3" />
|
|
58
|
+
<Github className="h-3.5 w-3.5" />
|
|
59
|
+
GitHub
|
|
82
60
|
</a>
|
|
83
61
|
<a
|
|
84
62
|
href={`${GITHUB_URL}/issues`}
|
|
85
63
|
target="_blank"
|
|
86
64
|
rel="noopener noreferrer"
|
|
87
|
-
className="flex items-center gap-
|
|
65
|
+
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
88
66
|
>
|
|
89
|
-
|
|
90
|
-
<span>Report an issue</span>
|
|
67
|
+
Report issue
|
|
91
68
|
<ExternalLink className="h-3 w-3" />
|
|
92
69
|
</a>
|
|
70
|
+
<span className="text-muted-foreground">MPL-2.0</span>
|
|
93
71
|
</div>
|
|
94
72
|
|
|
95
|
-
{/*
|
|
96
|
-
<div className="pt-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
{/* Feature chips */}
|
|
74
|
+
<div className="flex flex-wrap gap-1 justify-center pt-2 border-t">
|
|
75
|
+
{[
|
|
76
|
+
'WebGPU', 'IFC2x3', 'IFC4', 'IFC4X3', 'IFC5/IFCX',
|
|
77
|
+
'Federation', 'Measurements', 'Sections',
|
|
78
|
+
'Properties', 'Data tables', 'Lens rules', 'IDS',
|
|
79
|
+
'2D drawings', 'BCF', 'Scripting', 'AI assistant',
|
|
80
|
+
'glTF export', 'CSV', 'Parquet',
|
|
81
|
+
].map((tag) => (
|
|
82
|
+
<span
|
|
83
|
+
key={tag}
|
|
84
|
+
className="px-2 py-0.5 text-[11px] rounded-full bg-muted/60 text-muted-foreground"
|
|
85
|
+
>
|
|
86
|
+
{tag}
|
|
87
|
+
</span>
|
|
88
|
+
))}
|
|
100
89
|
</div>
|
|
90
|
+
|
|
91
|
+
{/* Package Versions */}
|
|
92
|
+
{packageVersions.length > 0 && (
|
|
93
|
+
<div className="pt-2 border-t">
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => setShowPackages(!showPackages)}
|
|
96
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
97
|
+
>
|
|
98
|
+
{showPackages ? (
|
|
99
|
+
<ChevronDown className="h-3 w-3" />
|
|
100
|
+
) : (
|
|
101
|
+
<ChevronRight className="h-3 w-3" />
|
|
102
|
+
)}
|
|
103
|
+
<Package className="h-3 w-3" />
|
|
104
|
+
{packageVersions.length} packages
|
|
105
|
+
</button>
|
|
106
|
+
{showPackages && (
|
|
107
|
+
<div className="rounded-md border bg-muted/30 p-2 mt-1.5 max-h-48 overflow-y-auto">
|
|
108
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5">
|
|
109
|
+
{packageVersions.map((pkg) => (
|
|
110
|
+
<div
|
|
111
|
+
key={pkg.name}
|
|
112
|
+
className="flex items-center justify-between text-xs py-0.5 px-1 min-w-0"
|
|
113
|
+
>
|
|
114
|
+
<span className="text-muted-foreground font-mono truncate mr-2">
|
|
115
|
+
{pkg.name.replace('@ifc-lite/', '')}
|
|
116
|
+
</span>
|
|
117
|
+
<span className="font-mono shrink-0 tabular-nums">{pkg.version}</span>
|
|
118
|
+
</div>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
101
125
|
</div>
|
|
102
126
|
);
|
|
103
127
|
}
|
|
104
128
|
|
|
129
|
+
function formatPkgName(name: string): string {
|
|
130
|
+
return name.replace('@ifc-lite/', '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type TimelineEntry = {
|
|
134
|
+
version: string;
|
|
135
|
+
isViewerVersion: boolean;
|
|
136
|
+
entries: Array<{ pkg: string; highlights: typeof __RELEASE_HISTORY__[0]['releases'][0]['highlights'] }>;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const compareSemver = (a: string, b: string) => {
|
|
140
|
+
const pa = a.split('.').map(Number);
|
|
141
|
+
const pb = b.split('.').map(Number);
|
|
142
|
+
for (let i = 0; i < 3; i++) {
|
|
143
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Merge all per-package changelogs into a unified timeline grouped by version. */
|
|
149
|
+
function buildTimeline(
|
|
150
|
+
packageChangelogs: typeof __RELEASE_HISTORY__,
|
|
151
|
+
viewerVersion: string
|
|
152
|
+
): TimelineEntry[] {
|
|
153
|
+
type Highlights = typeof __RELEASE_HISTORY__[0]['releases'][0]['highlights'];
|
|
154
|
+
const versionMap = new Map<string, Map<string, Highlights>>();
|
|
155
|
+
|
|
156
|
+
for (const pkg of packageChangelogs) {
|
|
157
|
+
for (const release of pkg.releases) {
|
|
158
|
+
if (!versionMap.has(release.version)) {
|
|
159
|
+
versionMap.set(release.version, new Map());
|
|
160
|
+
}
|
|
161
|
+
versionMap.get(release.version)!.set(pkg.name, release.highlights);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Array.from(versionMap.entries())
|
|
166
|
+
.sort(([a], [b]) => compareSemver(a, b))
|
|
167
|
+
.map(([version, pkgMap]) => ({
|
|
168
|
+
version,
|
|
169
|
+
isViewerVersion: version === viewerVersion,
|
|
170
|
+
entries: Array.from(pkgMap.entries())
|
|
171
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
172
|
+
.map(([pkg, highlights]) => ({ pkg, highlights })),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
105
176
|
function WhatsNewTab() {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
177
|
+
const packageChangelogs = __RELEASE_HISTORY__;
|
|
178
|
+
const viewerVersion = __APP_VERSION__;
|
|
179
|
+
const [expandedVersions, setExpandedVersions] = useState<Set<string>>(() => new Set());
|
|
108
180
|
|
|
109
|
-
const
|
|
110
|
-
() => (
|
|
111
|
-
[
|
|
181
|
+
const timeline = useMemo(
|
|
182
|
+
() => buildTimeline(packageChangelogs, viewerVersion),
|
|
183
|
+
[packageChangelogs, viewerVersion]
|
|
112
184
|
);
|
|
113
185
|
|
|
114
|
-
|
|
186
|
+
// Auto-expand the first version with actual changes
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (timeline.length > 0 && expandedVersions.size === 0) {
|
|
189
|
+
setExpandedVersions(new Set([timeline[0].version]));
|
|
190
|
+
}
|
|
191
|
+
}, [timeline]);
|
|
115
192
|
|
|
116
|
-
|
|
193
|
+
const toggleVersion = useCallback((version: string) => {
|
|
194
|
+
setExpandedVersions((prev) => {
|
|
195
|
+
const next = new Set(prev);
|
|
196
|
+
if (next.has(version)) next.delete(version);
|
|
197
|
+
else next.add(version);
|
|
198
|
+
return next;
|
|
199
|
+
});
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
if (timeline.length === 0) {
|
|
117
203
|
return (
|
|
118
204
|
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
119
205
|
No release history available.
|
|
@@ -122,43 +208,59 @@ function WhatsNewTab() {
|
|
|
122
208
|
}
|
|
123
209
|
|
|
124
210
|
return (
|
|
125
|
-
<div className="space-y-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
211
|
+
<div className="space-y-1">
|
|
212
|
+
{timeline.map((release) => {
|
|
213
|
+
const isExpanded = expandedVersions.has(release.version);
|
|
214
|
+
const totalHighlights = release.entries.reduce((s, e) => s + e.highlights.length, 0);
|
|
215
|
+
return (
|
|
216
|
+
<div key={release.version}>
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => toggleVersion(release.version)}
|
|
219
|
+
className="flex items-center gap-2 w-full py-1.5 px-1 text-left hover:bg-muted/40 transition-colors rounded"
|
|
220
|
+
>
|
|
221
|
+
{isExpanded ? (
|
|
222
|
+
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
223
|
+
) : (
|
|
224
|
+
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
225
|
+
)}
|
|
226
|
+
<span className="text-sm font-semibold">v{release.version}</span>
|
|
227
|
+
{release.isViewerVersion && (
|
|
228
|
+
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-sky-500/15 text-sky-600 dark:text-sky-400 rounded">
|
|
229
|
+
viewer
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
<span className="text-xs text-muted-foreground ml-auto">
|
|
233
|
+
{totalHighlights} change{totalHighlights !== 1 ? 's' : ''}
|
|
133
234
|
</span>
|
|
235
|
+
</button>
|
|
236
|
+
{isExpanded && (
|
|
237
|
+
<div className="ml-5 pb-2 space-y-2">
|
|
238
|
+
{release.entries.map(({ pkg, highlights }) => (
|
|
239
|
+
<div key={pkg}>
|
|
240
|
+
<span className="text-xs font-medium font-mono text-muted-foreground">
|
|
241
|
+
{formatPkgName(pkg)}
|
|
242
|
+
</span>
|
|
243
|
+
<ul className="space-y-0.5 mt-0.5">
|
|
244
|
+
{highlights.map((h) => {
|
|
245
|
+
const { icon: Icon, className } = TYPE_CONFIG[h.type];
|
|
246
|
+
return (
|
|
247
|
+
<li
|
|
248
|
+
key={h.text}
|
|
249
|
+
className="flex items-start gap-1.5 text-sm text-muted-foreground"
|
|
250
|
+
>
|
|
251
|
+
<Icon className={`h-3 w-3 mt-0.5 shrink-0 ${className}`} />
|
|
252
|
+
<span>{h.text}</span>
|
|
253
|
+
</li>
|
|
254
|
+
);
|
|
255
|
+
})}
|
|
256
|
+
</ul>
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
134
260
|
)}
|
|
135
261
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const { icon: Icon, className } = TYPE_CONFIG[h.type];
|
|
139
|
-
return (
|
|
140
|
-
<li key={h.text} className="flex items-start gap-2 text-sm text-muted-foreground">
|
|
141
|
-
<Icon className={`h-3.5 w-3.5 mt-0.5 shrink-0 ${className}`} />
|
|
142
|
-
<span>{h.text}</span>
|
|
143
|
-
</li>
|
|
144
|
-
);
|
|
145
|
-
})}
|
|
146
|
-
</ul>
|
|
147
|
-
{i < visibleReleases.length - 1 && (
|
|
148
|
-
<div className="border-b mt-3" />
|
|
149
|
-
)}
|
|
150
|
-
</div>
|
|
151
|
-
))}
|
|
152
|
-
|
|
153
|
-
{hasMore && !showAll && (
|
|
154
|
-
<button
|
|
155
|
-
onClick={() => setShowAll(true)}
|
|
156
|
-
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mx-auto"
|
|
157
|
-
>
|
|
158
|
-
<ChevronDown className="h-3.5 w-3.5" />
|
|
159
|
-
Show all {releases.length} releases
|
|
160
|
-
</button>
|
|
161
|
-
)}
|
|
262
|
+
);
|
|
263
|
+
})}
|
|
162
264
|
|
|
163
265
|
{/* Legend */}
|
|
164
266
|
<div className="pt-3 border-t flex items-center justify-center gap-4 text-[11px] text-muted-foreground">
|
|
@@ -9,6 +9,7 @@ import { ToolOverlays } from './ToolOverlays';
|
|
|
9
9
|
import { Section2DPanel } from './Section2DPanel';
|
|
10
10
|
import { BasketPresentationDock } from './BasketPresentationDock';
|
|
11
11
|
import { useViewerStore } from '@/store';
|
|
12
|
+
import { collectIfcBuildingStoreyElementsWithIfcSpace } from '@/store/basketVisibleSet';
|
|
12
13
|
import { useIfc } from '@/hooks/useIfc';
|
|
13
14
|
import { useWebGPU } from '@/hooks/useWebGPU';
|
|
14
15
|
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus } from 'lucide-react';
|
|
@@ -27,6 +28,7 @@ export function ViewportContainer() {
|
|
|
27
28
|
const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
|
|
28
29
|
const typeVisibility = useViewerStore((s) => s.typeVisibility);
|
|
29
30
|
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
31
|
+
const classFilter = useViewerStore((s) => s.classFilter);
|
|
30
32
|
// Multi-model support: get all loaded models from store (for merged geometry)
|
|
31
33
|
const storeModels = useViewerStore((s) => s.models);
|
|
32
34
|
const resetViewerState = useViewerStore((s) => s.resetViewerState);
|
|
@@ -251,15 +253,8 @@ export function ViewportContainer() {
|
|
|
251
253
|
// Now supports multi-model: aggregates elements from all models for selected storeys
|
|
252
254
|
// IMPORTANT: Returns globalIds (meshes use globalIds after federation registry transformation)
|
|
253
255
|
const computedIsolatedIds = useMemo(() => {
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
return isolatedEntities;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// If storeys are selected, compute combined element IDs from all selected storeys
|
|
260
|
-
// across ALL models (multi-model support)
|
|
261
|
-
// NOTE: Storey hierarchy uses original expressIds, but meshes use globalIds
|
|
262
|
-
// We must transform expressIds -> globalIds using the model's offset
|
|
256
|
+
// Compute storey isolation if storeys are selected
|
|
257
|
+
let storeyIsolation: Set<number> | null = null;
|
|
263
258
|
if (selectedStoreys.size > 0) {
|
|
264
259
|
const combinedGlobalIds = new Set<number>();
|
|
265
260
|
|
|
@@ -268,46 +263,56 @@ export function ViewportContainer() {
|
|
|
268
263
|
const hierarchy = model.ifcDataStore?.spatialHierarchy;
|
|
269
264
|
if (!hierarchy) continue;
|
|
270
265
|
|
|
271
|
-
// Get this model's offset directly from the model (no need for registry)
|
|
272
266
|
const offset = model.idOffset ?? 0;
|
|
273
267
|
|
|
274
268
|
for (const storeyId of selectedStoreys) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// For now, try both the storeyId and storeyId - offset
|
|
278
|
-
const storeyElementIds = hierarchy.byStorey.get(storeyId) || hierarchy.byStorey.get(storeyId - offset);
|
|
269
|
+
const localStoreyId = hierarchy.byStorey.has(storeyId) ? storeyId : storeyId - offset;
|
|
270
|
+
const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, localStoreyId);
|
|
279
271
|
if (storeyElementIds) {
|
|
280
272
|
for (const originalExpressId of storeyElementIds) {
|
|
281
|
-
|
|
282
|
-
const globalId = originalExpressId + offset;
|
|
283
|
-
combinedGlobalIds.add(globalId);
|
|
273
|
+
combinedGlobalIds.add(originalExpressId + offset);
|
|
284
274
|
}
|
|
285
275
|
}
|
|
286
276
|
}
|
|
287
277
|
}
|
|
288
278
|
|
|
289
|
-
//
|
|
290
|
-
// In this case, offset is 0, so globalId = expressId
|
|
279
|
+
// Legacy single-model mode (offset = 0)
|
|
291
280
|
if (ifcDataStore?.spatialHierarchy && storeModels.size === 0) {
|
|
292
281
|
const hierarchy = ifcDataStore.spatialHierarchy;
|
|
293
282
|
for (const storeyId of selectedStoreys) {
|
|
294
|
-
const storeyElementIds = hierarchy
|
|
283
|
+
const storeyElementIds = collectIfcBuildingStoreyElementsWithIfcSpace(hierarchy, storeyId);
|
|
295
284
|
if (storeyElementIds) {
|
|
296
285
|
for (const id of storeyElementIds) {
|
|
297
|
-
combinedGlobalIds.add(id);
|
|
286
|
+
combinedGlobalIds.add(id);
|
|
298
287
|
}
|
|
299
288
|
}
|
|
300
289
|
}
|
|
301
290
|
}
|
|
302
291
|
|
|
303
292
|
if (combinedGlobalIds.size > 0) {
|
|
304
|
-
|
|
293
|
+
storeyIsolation = combinedGlobalIds;
|
|
305
294
|
}
|
|
306
295
|
}
|
|
307
296
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
297
|
+
// Collect all active filters and intersect them
|
|
298
|
+
const filters: Set<number>[] = [];
|
|
299
|
+
if (storeyIsolation !== null) filters.push(storeyIsolation);
|
|
300
|
+
if (classFilter !== null) filters.push(classFilter.ids);
|
|
301
|
+
if (isolatedEntities !== null) filters.push(isolatedEntities);
|
|
302
|
+
|
|
303
|
+
if (filters.length === 0) return null;
|
|
304
|
+
if (filters.length === 1) return filters[0];
|
|
305
|
+
|
|
306
|
+
// Intersect all active filters — start from smallest for efficiency
|
|
307
|
+
const sorted = filters.sort((a, b) => a.size - b.size);
|
|
308
|
+
const intersection = new Set<number>();
|
|
309
|
+
for (const id of sorted[0]) {
|
|
310
|
+
if (sorted.every(s => s.has(id))) {
|
|
311
|
+
intersection.add(id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return intersection;
|
|
315
|
+
}, [storeModels, ifcDataStore, selectedStoreys, isolatedEntities, classFilter]);
|
|
311
316
|
|
|
312
317
|
// Grid Pattern
|
|
313
318
|
const GridPattern = () => (
|