@ifc-lite/viewer 1.15.0 → 1.17.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 +46 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +35 -0
- package/dist/assets/{Arrow.dom-OVBBPqOB.js → Arrow.dom-CcoDLP6E.js} +1 -1
- package/dist/assets/{basketViewActivator-Bx6QU4ma.js → basketViewActivator-FtbS__bG.js} +1 -1
- package/dist/assets/{browser-BMqEoJw4.js → browser-CXd3z0DO.js} +1 -1
- package/dist/assets/ifc-lite-TI3u_Zyw.js +7 -0
- package/dist/assets/ifc-lite_bg-DeZrXTKQ.wasm +0 -0
- package/dist/assets/index-Ba4eoTe7.css +1 -0
- package/dist/assets/{index-DZY6uD8A.js → index-D99fzcwI.js} +32109 -28671
- package/dist/assets/{index-DsX-NCtx.js → index-DqNiuQep.js} +4 -4
- package/dist/assets/{native-bridge-D6tKFqGO.js → native-bridge-DjDj2M6p.js} +1 -1
- package/dist/assets/{wasm-bridge-D4kvZVDw.js → wasm-bridge-CDTF4ZQc.js} +1 -1
- package/dist/assets/workerHelpers-G7llXNMi.js +36 -0
- package/dist/index.html +7 -2
- package/index.html +5 -0
- package/package.json +15 -14
- package/src/components/viewer/BCFPanel.tsx +12 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +315 -154
- package/src/components/viewer/CommandPalette.tsx +0 -6
- package/src/components/viewer/DataConnector.tsx +489 -284
- package/src/components/viewer/ExportDialog.tsx +66 -6
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +227 -82
- package/src/components/viewer/MainToolbar.tsx +1 -5
- package/src/components/viewer/Viewport.tsx +42 -56
- package/src/components/viewer/ViewportContainer.tsx +3 -0
- package/src/components/viewer/ViewportOverlays.tsx +12 -10
- package/src/components/viewer/bcf/BCFOverlay.tsx +254 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +26 -20
- package/src/components/viewer/hierarchy/ifc-icons.ts +90 -0
- package/src/components/viewer/lists/ListPanel.tsx +0 -21
- package/src/components/viewer/lists/ListResultsTable.tsx +93 -5
- package/src/components/viewer/measureHandlers.ts +558 -0
- package/src/components/viewer/mouseHandlerTypes.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +86 -0
- package/src/components/viewer/useAnimationLoop.ts +116 -44
- package/src/components/viewer/useGeometryStreaming.ts +155 -367
- package/src/components/viewer/useKeyboardControls.ts +30 -46
- package/src/components/viewer/useMouseControls.ts +169 -695
- package/src/components/viewer/useRenderUpdates.ts +9 -59
- package/src/components/viewer/useTouchControls.ts +55 -40
- package/src/hooks/bcfIdLookup.ts +70 -0
- package/src/hooks/useBCF.ts +12 -31
- package/src/hooks/useIfcCache.ts +11 -29
- package/src/hooks/useIfcFederation.ts +5 -11
- package/src/hooks/useIfcLoader.ts +47 -56
- package/src/hooks/useIfcServer.ts +9 -1
- package/src/hooks/useKeyboardShortcuts.ts +28 -12
- package/src/hooks/useLatestRef.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +2 -2
- package/src/sdk/adapters/model-adapter.ts +1 -0
- package/src/sdk/local-backend.ts +2 -0
- package/src/store/basketVisibleSet.ts +12 -0
- package/src/store/slices/bcfSlice.ts +9 -0
- package/src/store/slices/pinboardSlice.ts +46 -45
- package/src/utils/loadingUtils.ts +46 -0
- package/src/utils/serverDataModel.ts +4 -3
- 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-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-CJr7Itua.css +0 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - IFC5 → .ifcx
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
17
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
18
18
|
import {
|
|
19
19
|
Download,
|
|
20
20
|
AlertCircle,
|
|
@@ -48,10 +48,11 @@ import {
|
|
|
48
48
|
AlertDescription,
|
|
49
49
|
AlertTitle,
|
|
50
50
|
} from '@/components/ui/alert';
|
|
51
|
+
import { Progress } from '@/components/ui/progress';
|
|
51
52
|
import { useViewerStore } from '@/store';
|
|
52
53
|
import { configureMutationView } from '@/utils/configureMutationView';
|
|
53
54
|
import { toast } from '@/components/ui/toast';
|
|
54
|
-
import { StepExporter, MergedExporter, Ifc5Exporter, IFC5_KNOWN_PROP_NAMES, type MergeModelInput } from '@ifc-lite/export';
|
|
55
|
+
import { StepExporter, MergedExporter, Ifc5Exporter, IFC5_KNOWN_PROP_NAMES, type MergeModelInput, type ExportProgress, type StepExportProgress } from '@ifc-lite/export';
|
|
55
56
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
56
57
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
57
58
|
|
|
@@ -87,6 +88,27 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
87
88
|
const [onlyKnownProperties, setOnlyKnownProperties] = useState(true);
|
|
88
89
|
const [isExporting, setIsExporting] = useState(false);
|
|
89
90
|
const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
91
|
+
const [exportProgress, setExportProgress] = useState<{
|
|
92
|
+
phase: string;
|
|
93
|
+
percent: number;
|
|
94
|
+
entitiesProcessed: number;
|
|
95
|
+
entitiesTotal: number;
|
|
96
|
+
currentModel?: string;
|
|
97
|
+
} | null>(null);
|
|
98
|
+
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
99
|
+
const prevProgressRef = useRef<typeof exportProgress>(null);
|
|
100
|
+
|
|
101
|
+
const scrollToBottom = useCallback(() => {
|
|
102
|
+
if (scrollAreaRef.current) {
|
|
103
|
+
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight;
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
// Auto-scroll when progress first appears
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (exportProgress && !prevProgressRef.current) scrollToBottom();
|
|
110
|
+
prevProgressRef.current = exportProgress;
|
|
111
|
+
}, [exportProgress, scrollToBottom]);
|
|
90
112
|
|
|
91
113
|
// Derived: is this an IFC5/IFCX export?
|
|
92
114
|
const isIfc5 = schema === 'IFC5';
|
|
@@ -286,6 +308,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
286
308
|
|
|
287
309
|
setIsExporting(true);
|
|
288
310
|
setExportResult(null);
|
|
311
|
+
setExportProgress(null);
|
|
289
312
|
|
|
290
313
|
try {
|
|
291
314
|
// Handle merged export of all models (STEP only, not IFC5)
|
|
@@ -308,7 +331,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
308
331
|
}
|
|
309
332
|
}
|
|
310
333
|
|
|
311
|
-
const result = mergedExporter.
|
|
334
|
+
const result = await mergedExporter.exportAsync({
|
|
312
335
|
schema,
|
|
313
336
|
projectStrategy: 'keep-first',
|
|
314
337
|
visibleOnly,
|
|
@@ -316,8 +339,19 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
316
339
|
isolatedEntityIdsByModel: isolatedByModel,
|
|
317
340
|
description: `Merged export of ${mergeInputs.length} models from ifc-lite`,
|
|
318
341
|
application: 'ifc-lite',
|
|
342
|
+
onProgress: (p: ExportProgress) => setExportProgress({
|
|
343
|
+
phase: p.phase === 'preparing' ? 'Preparing models...'
|
|
344
|
+
: p.phase === 'entities' ? `Processing entities${p.currentModel ? ` (${p.currentModel})` : ''}...`
|
|
345
|
+
: 'Assembling file...',
|
|
346
|
+
percent: p.percent,
|
|
347
|
+
entitiesProcessed: p.entitiesProcessed,
|
|
348
|
+
entitiesTotal: p.entitiesTotal,
|
|
349
|
+
currentModel: p.currentModel,
|
|
350
|
+
}),
|
|
319
351
|
});
|
|
320
352
|
|
|
353
|
+
setExportProgress(null);
|
|
354
|
+
|
|
321
355
|
const blob = new Blob([result.content], { type: 'text/plain' });
|
|
322
356
|
const url = URL.createObjectURL(blob);
|
|
323
357
|
const a = document.createElement('a');
|
|
@@ -328,7 +362,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
328
362
|
document.body.removeChild(a);
|
|
329
363
|
URL.revokeObjectURL(url);
|
|
330
364
|
|
|
331
|
-
const msg = `Merged ${result.stats.modelCount} models, ${result.stats.totalEntityCount} entities`;
|
|
365
|
+
const msg = `Merged ${result.stats.modelCount} models, ${result.stats.totalEntityCount.toLocaleString()} entities`;
|
|
332
366
|
setExportResult({ success: true, message: msg });
|
|
333
367
|
toast.success(msg);
|
|
334
368
|
return;
|
|
@@ -431,7 +465,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
431
465
|
const localHidden = visibleOnly ? getLocalHiddenIds(selectedModelId) : undefined;
|
|
432
466
|
const localIsolated = visibleOnly ? getLocalIsolatedIds(selectedModelId) : undefined;
|
|
433
467
|
|
|
434
|
-
const result = exporter.
|
|
468
|
+
const result = await exporter.exportAsync({
|
|
435
469
|
schema,
|
|
436
470
|
includeGeometry,
|
|
437
471
|
applyMutations,
|
|
@@ -440,8 +474,18 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
440
474
|
isolatedEntityIds: localIsolated,
|
|
441
475
|
description: `Exported from ifc-lite with ${modifiedCount} modifications`,
|
|
442
476
|
application: 'ifc-lite',
|
|
477
|
+
onProgress: (p: StepExportProgress) => setExportProgress({
|
|
478
|
+
phase: p.phase === 'preparing' ? 'Preparing export...'
|
|
479
|
+
: p.phase === 'entities' ? 'Processing entities...'
|
|
480
|
+
: 'Assembling file...',
|
|
481
|
+
percent: p.percent,
|
|
482
|
+
entitiesProcessed: p.entitiesProcessed,
|
|
483
|
+
entitiesTotal: p.entitiesTotal,
|
|
484
|
+
}),
|
|
443
485
|
});
|
|
444
486
|
|
|
487
|
+
setExportProgress(null);
|
|
488
|
+
|
|
445
489
|
const blob = new Blob([result.content], { type: 'text/plain' });
|
|
446
490
|
const url = URL.createObjectURL(blob);
|
|
447
491
|
const a = document.createElement('a');
|
|
@@ -488,7 +532,7 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
488
532
|
</DialogDescription>
|
|
489
533
|
</DialogHeader>
|
|
490
534
|
|
|
491
|
-
<div className="grid gap-4 py-4">
|
|
535
|
+
<div ref={scrollAreaRef} className="grid gap-4 py-4 max-h-[60vh] overflow-y-auto">
|
|
492
536
|
{/* Scope selector (only for STEP schemas with multiple models) */}
|
|
493
537
|
{!isIfc5 && !changesOnly && modelList.length > 1 && (
|
|
494
538
|
<div className="flex items-center gap-4">
|
|
@@ -632,6 +676,22 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
632
676
|
</Alert>
|
|
633
677
|
)}
|
|
634
678
|
|
|
679
|
+
{/* Export Progress */}
|
|
680
|
+
{isExporting && exportProgress && (
|
|
681
|
+
<div className="space-y-2">
|
|
682
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
683
|
+
<span className="flex items-center gap-2">
|
|
684
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
685
|
+
{exportProgress.phase}
|
|
686
|
+
</span>
|
|
687
|
+
<span>
|
|
688
|
+
{exportProgress.entitiesProcessed.toLocaleString()} / {exportProgress.entitiesTotal.toLocaleString()} entities
|
|
689
|
+
</span>
|
|
690
|
+
</div>
|
|
691
|
+
<Progress value={exportProgress.percent * 100} />
|
|
692
|
+
</div>
|
|
693
|
+
)}
|
|
694
|
+
|
|
635
695
|
{/* Export result */}
|
|
636
696
|
{exportResult && (
|
|
637
697
|
<Alert variant={exportResult.success ? 'default' : 'destructive'}>
|
|
@@ -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, ShieldCheck } 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;
|
|
@@ -34,86 +33,216 @@ const TYPE_CONFIG = {
|
|
|
34
33
|
perf: { icon: Zap, className: 'text-blue-500' },
|
|
35
34
|
} as const;
|
|
36
35
|
|
|
36
|
+
function PrivacyBanner() {
|
|
37
|
+
const [expanded, setExpanded] = useState(false);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="pt-2 border-t">
|
|
41
|
+
<button
|
|
42
|
+
onClick={() => setExpanded(!expanded)}
|
|
43
|
+
className="flex items-center gap-2 w-full rounded-md bg-emerald-500/10 px-2.5 py-1.5 text-left transition-colors hover:bg-emerald-500/15"
|
|
44
|
+
>
|
|
45
|
+
<ShieldCheck className="h-3.5 w-3.5 text-emerald-500 shrink-0" />
|
|
46
|
+
<span className="text-xs font-medium">Your IFC data never leaves your device.</span>
|
|
47
|
+
{expanded ? (
|
|
48
|
+
<ChevronDown className="h-3 w-3 ml-auto shrink-0 text-muted-foreground" />
|
|
49
|
+
) : (
|
|
50
|
+
<ChevronRight className="h-3 w-3 ml-auto shrink-0 text-muted-foreground" />
|
|
51
|
+
)}
|
|
52
|
+
</button>
|
|
53
|
+
{expanded && (
|
|
54
|
+
<div className="mt-1.5 ml-1 space-y-1 text-xs text-muted-foreground">
|
|
55
|
+
<p>
|
|
56
|
+
All files are processed locally in the browser with{' '}
|
|
57
|
+
<a
|
|
58
|
+
href="https://webassembly.org/"
|
|
59
|
+
target="_blank"
|
|
60
|
+
rel="noopener noreferrer"
|
|
61
|
+
className="underline hover:text-foreground transition-colors"
|
|
62
|
+
>
|
|
63
|
+
WebAssembly (WASM)
|
|
64
|
+
</a>
|
|
65
|
+
{' '}– no server upload, near-native speed.
|
|
66
|
+
</p>
|
|
67
|
+
<p className="text-[11px] italic">
|
|
68
|
+
Verify: press <kbd className="px-1 py-0.5 bg-muted rounded border font-mono text-[10px]">F12</kbd> → Network tab → no IFC data transmitted.
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
function AboutTab() {
|
|
77
|
+
const [showPackages, setShowPackages] = useState(false);
|
|
78
|
+
const packageVersions = __PACKAGE_VERSIONS__;
|
|
79
|
+
|
|
38
80
|
return (
|
|
39
|
-
<div className="space-y-
|
|
81
|
+
<div className="space-y-3">
|
|
40
82
|
{/* Header */}
|
|
41
|
-
<div className="text-center pb-
|
|
83
|
+
<div className="text-center pb-2 border-b">
|
|
42
84
|
<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
85
|
<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.
|
|
86
|
+
v{__APP_VERSION__} · {formatBuildDate(__BUILD_DATE__)}
|
|
55
87
|
</p>
|
|
56
88
|
</div>
|
|
57
89
|
|
|
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
90
|
{/* Links */}
|
|
72
|
-
<div className="
|
|
91
|
+
<div className="flex items-center justify-center gap-4 text-xs">
|
|
73
92
|
<a
|
|
74
93
|
href={GITHUB_URL}
|
|
75
94
|
target="_blank"
|
|
76
95
|
rel="noopener noreferrer"
|
|
77
|
-
className="flex items-center gap-
|
|
96
|
+
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
78
97
|
>
|
|
79
|
-
<Github className="h-
|
|
80
|
-
|
|
81
|
-
<ExternalLink className="h-3 w-3" />
|
|
98
|
+
<Github className="h-3.5 w-3.5" />
|
|
99
|
+
GitHub
|
|
82
100
|
</a>
|
|
83
101
|
<a
|
|
84
102
|
href={`${GITHUB_URL}/issues`}
|
|
85
103
|
target="_blank"
|
|
86
104
|
rel="noopener noreferrer"
|
|
87
|
-
className="flex items-center gap-
|
|
105
|
+
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
88
106
|
>
|
|
89
|
-
|
|
90
|
-
<span>Report an issue</span>
|
|
107
|
+
Report issue
|
|
91
108
|
<ExternalLink className="h-3 w-3" />
|
|
92
109
|
</a>
|
|
110
|
+
<span className="text-muted-foreground">MPL-2.0</span>
|
|
93
111
|
</div>
|
|
94
112
|
|
|
95
|
-
{/*
|
|
96
|
-
<div className="pt-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
{/* Feature chips */}
|
|
114
|
+
<div className="flex flex-wrap gap-1 justify-center pt-2 border-t">
|
|
115
|
+
{[
|
|
116
|
+
'WebGPU', 'IFC2x3', 'IFC4', 'IFC4X3', 'IFC5/IFCX',
|
|
117
|
+
'Federation', 'Measurements', 'Sections',
|
|
118
|
+
'Properties', 'Data tables', 'Lens rules', 'IDS',
|
|
119
|
+
'2D drawings', 'BCF', 'Scripting', 'AI assistant',
|
|
120
|
+
'glTF export', 'CSV', 'Parquet',
|
|
121
|
+
].map((tag) => (
|
|
122
|
+
<span
|
|
123
|
+
key={tag}
|
|
124
|
+
className="px-2 py-0.5 text-[11px] rounded-full bg-muted/60 text-muted-foreground"
|
|
125
|
+
>
|
|
126
|
+
{tag}
|
|
127
|
+
</span>
|
|
128
|
+
))}
|
|
100
129
|
</div>
|
|
130
|
+
|
|
131
|
+
{/* Privacy & Security */}
|
|
132
|
+
<PrivacyBanner />
|
|
133
|
+
|
|
134
|
+
{/* Package Versions */}
|
|
135
|
+
{packageVersions.length > 0 && (
|
|
136
|
+
<div className="pt-2 border-t">
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => setShowPackages(!showPackages)}
|
|
139
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
140
|
+
>
|
|
141
|
+
{showPackages ? (
|
|
142
|
+
<ChevronDown className="h-3 w-3" />
|
|
143
|
+
) : (
|
|
144
|
+
<ChevronRight className="h-3 w-3" />
|
|
145
|
+
)}
|
|
146
|
+
<Package className="h-3 w-3" />
|
|
147
|
+
{packageVersions.length} packages
|
|
148
|
+
</button>
|
|
149
|
+
{showPackages && (
|
|
150
|
+
<div className="rounded-md border bg-muted/30 p-2 mt-1.5 max-h-48 overflow-y-auto">
|
|
151
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5">
|
|
152
|
+
{packageVersions.map((pkg) => (
|
|
153
|
+
<div
|
|
154
|
+
key={pkg.name}
|
|
155
|
+
className="flex items-center justify-between text-xs py-0.5 px-1 min-w-0"
|
|
156
|
+
>
|
|
157
|
+
<span className="text-muted-foreground font-mono truncate mr-2">
|
|
158
|
+
{pkg.name.replace('@ifc-lite/', '')}
|
|
159
|
+
</span>
|
|
160
|
+
<span className="font-mono shrink-0 tabular-nums">{pkg.version}</span>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
101
168
|
</div>
|
|
102
169
|
);
|
|
103
170
|
}
|
|
104
171
|
|
|
172
|
+
function formatPkgName(name: string): string {
|
|
173
|
+
return name.replace('@ifc-lite/', '');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type TimelineEntry = {
|
|
177
|
+
version: string;
|
|
178
|
+
isViewerVersion: boolean;
|
|
179
|
+
entries: Array<{ pkg: string; highlights: typeof __RELEASE_HISTORY__[0]['releases'][0]['highlights'] }>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const compareSemver = (a: string, b: string) => {
|
|
183
|
+
const pa = a.split('.').map(Number);
|
|
184
|
+
const pb = b.split('.').map(Number);
|
|
185
|
+
for (let i = 0; i < 3; i++) {
|
|
186
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0);
|
|
187
|
+
}
|
|
188
|
+
return 0;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** Merge all per-package changelogs into a unified timeline grouped by version. */
|
|
192
|
+
function buildTimeline(
|
|
193
|
+
packageChangelogs: typeof __RELEASE_HISTORY__,
|
|
194
|
+
viewerVersion: string
|
|
195
|
+
): TimelineEntry[] {
|
|
196
|
+
type Highlights = typeof __RELEASE_HISTORY__[0]['releases'][0]['highlights'];
|
|
197
|
+
const versionMap = new Map<string, Map<string, Highlights>>();
|
|
198
|
+
|
|
199
|
+
for (const pkg of packageChangelogs) {
|
|
200
|
+
for (const release of pkg.releases) {
|
|
201
|
+
if (!versionMap.has(release.version)) {
|
|
202
|
+
versionMap.set(release.version, new Map());
|
|
203
|
+
}
|
|
204
|
+
versionMap.get(release.version)!.set(pkg.name, release.highlights);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return Array.from(versionMap.entries())
|
|
209
|
+
.sort(([a], [b]) => compareSemver(a, b))
|
|
210
|
+
.map(([version, pkgMap]) => ({
|
|
211
|
+
version,
|
|
212
|
+
isViewerVersion: version === viewerVersion,
|
|
213
|
+
entries: Array.from(pkgMap.entries())
|
|
214
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
215
|
+
.map(([pkg, highlights]) => ({ pkg, highlights })),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
105
219
|
function WhatsNewTab() {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
220
|
+
const packageChangelogs = __RELEASE_HISTORY__;
|
|
221
|
+
const viewerVersion = __APP_VERSION__;
|
|
222
|
+
const [expandedVersions, setExpandedVersions] = useState<Set<string>>(() => new Set());
|
|
108
223
|
|
|
109
|
-
const
|
|
110
|
-
() => (
|
|
111
|
-
[
|
|
224
|
+
const timeline = useMemo(
|
|
225
|
+
() => buildTimeline(packageChangelogs, viewerVersion),
|
|
226
|
+
[packageChangelogs, viewerVersion]
|
|
112
227
|
);
|
|
113
228
|
|
|
114
|
-
|
|
229
|
+
// Auto-expand the first version with actual changes
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (timeline.length > 0 && expandedVersions.size === 0) {
|
|
232
|
+
setExpandedVersions(new Set([timeline[0].version]));
|
|
233
|
+
}
|
|
234
|
+
}, [timeline]);
|
|
235
|
+
|
|
236
|
+
const toggleVersion = useCallback((version: string) => {
|
|
237
|
+
setExpandedVersions((prev) => {
|
|
238
|
+
const next = new Set(prev);
|
|
239
|
+
if (next.has(version)) next.delete(version);
|
|
240
|
+
else next.add(version);
|
|
241
|
+
return next;
|
|
242
|
+
});
|
|
243
|
+
}, []);
|
|
115
244
|
|
|
116
|
-
if (
|
|
245
|
+
if (timeline.length === 0) {
|
|
117
246
|
return (
|
|
118
247
|
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
119
248
|
No release history available.
|
|
@@ -122,43 +251,59 @@ function WhatsNewTab() {
|
|
|
122
251
|
}
|
|
123
252
|
|
|
124
253
|
return (
|
|
125
|
-
<div className="space-y-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
254
|
+
<div className="space-y-1">
|
|
255
|
+
{timeline.map((release) => {
|
|
256
|
+
const isExpanded = expandedVersions.has(release.version);
|
|
257
|
+
const totalHighlights = release.entries.reduce((s, e) => s + e.highlights.length, 0);
|
|
258
|
+
return (
|
|
259
|
+
<div key={release.version}>
|
|
260
|
+
<button
|
|
261
|
+
onClick={() => toggleVersion(release.version)}
|
|
262
|
+
className="flex items-center gap-2 w-full py-1.5 px-1 text-left hover:bg-muted/40 transition-colors rounded"
|
|
263
|
+
>
|
|
264
|
+
{isExpanded ? (
|
|
265
|
+
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
266
|
+
) : (
|
|
267
|
+
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
268
|
+
)}
|
|
269
|
+
<span className="text-sm font-semibold">v{release.version}</span>
|
|
270
|
+
{release.isViewerVersion && (
|
|
271
|
+
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-sky-500/15 text-sky-600 dark:text-sky-400 rounded">
|
|
272
|
+
viewer
|
|
273
|
+
</span>
|
|
274
|
+
)}
|
|
275
|
+
<span className="text-xs text-muted-foreground ml-auto">
|
|
276
|
+
{totalHighlights} change{totalHighlights !== 1 ? 's' : ''}
|
|
133
277
|
</span>
|
|
278
|
+
</button>
|
|
279
|
+
{isExpanded && (
|
|
280
|
+
<div className="ml-5 pb-2 space-y-2">
|
|
281
|
+
{release.entries.map(({ pkg, highlights }) => (
|
|
282
|
+
<div key={pkg}>
|
|
283
|
+
<span className="text-xs font-medium font-mono text-muted-foreground">
|
|
284
|
+
{formatPkgName(pkg)}
|
|
285
|
+
</span>
|
|
286
|
+
<ul className="space-y-0.5 mt-0.5">
|
|
287
|
+
{highlights.map((h) => {
|
|
288
|
+
const { icon: Icon, className } = TYPE_CONFIG[h.type];
|
|
289
|
+
return (
|
|
290
|
+
<li
|
|
291
|
+
key={h.text}
|
|
292
|
+
className="flex items-start gap-1.5 text-sm text-muted-foreground"
|
|
293
|
+
>
|
|
294
|
+
<Icon className={`h-3 w-3 mt-0.5 shrink-0 ${className}`} />
|
|
295
|
+
<span>{h.text}</span>
|
|
296
|
+
</li>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</ul>
|
|
300
|
+
</div>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
134
303
|
)}
|
|
135
304
|
</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
|
-
)}
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
162
307
|
|
|
163
308
|
{/* Legend */}
|
|
164
309
|
<div className="pt-3 border-t flex items-center justify-center gap-4 text-[11px] text-muted-foreground">
|
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
FolderOpen,
|
|
8
8
|
Download,
|
|
9
9
|
MousePointer2,
|
|
10
|
-
Hand,
|
|
11
|
-
Rotate3d,
|
|
12
10
|
PersonStanding,
|
|
13
11
|
Ruler,
|
|
14
12
|
Scissors,
|
|
@@ -71,7 +69,7 @@ import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
|
|
|
71
69
|
import { ThemeSwitch } from './ThemeSwitch';
|
|
72
70
|
import { toast } from '@/components/ui/toast';
|
|
73
71
|
|
|
74
|
-
type Tool = 'select' | '
|
|
72
|
+
type Tool = 'select' | 'walk' | 'measure' | 'section';
|
|
75
73
|
type WorkspacePanel = 'script' | 'list' | 'bcf' | 'ids' | 'lens';
|
|
76
74
|
|
|
77
75
|
// #region FIX: Move ToolButton OUTSIDE MainToolbar to prevent recreation on every render
|
|
@@ -741,8 +739,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
741
739
|
|
|
742
740
|
{/* ── Navigation Tools ── */}
|
|
743
741
|
<ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
744
|
-
<ToolButton tool="pan" icon={Hand} label="Pan" shortcut="P" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
745
|
-
<ToolButton tool="orbit" icon={Rotate3d} label="Orbit" shortcut="O" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
746
742
|
<ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
747
743
|
|
|
748
744
|
<Separator orientation="vertical" className="h-6 mx-1" />
|