@ifc-lite/viewer 1.16.0 → 1.17.1
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 +42 -0
- package/.turbo/turbo-typecheck.log +44 -0
- package/CHANGELOG.md +25 -0
- package/dist/assets/{Arrow.dom--gdrQd-q.js → Arrow.dom-DuPUrOxJ.js} +1 -1
- package/dist/assets/{basketViewActivator-CI3y6VYQ.js → basketViewActivator-DetjPnvt.js} +1 -1
- package/dist/assets/{browser-vWDubxDI.js → browser-BQdwnOUt.js} +1 -1
- package/dist/assets/geometry.worker-Bjm-ukng.js +1 -0
- package/dist/assets/ifc-lite_bg-DD0A7Yow.wasm +0 -0
- package/dist/assets/{index-RXIK18da.js → index-B3X21yXA.js} +4 -4
- package/dist/assets/index-Ba4eoTe7.css +1 -0
- package/dist/assets/{index-BImINgzG.js → index-BybGZJTW.js} +29281 -27174
- package/dist/assets/{native-bridge-4rLidc3f.js → native-bridge-CN0ZMR2t.js} +1 -1
- package/dist/assets/{wasm-bridge-BkfXfw8O.js → wasm-bridge-D0bALkma.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +14 -13
- 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 +44 -1
- package/src/components/viewer/MainToolbar.tsx +1 -5
- package/src/components/viewer/PropertiesPanel.tsx +6 -7
- 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/treeDataBuilder.test.ts +70 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +35 -10
- package/src/components/viewer/hierarchy/types.ts +24 -2
- 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 +2 -20
- 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 +0 -10
- 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/adapters/visibility-adapter.ts +7 -49
- package/src/sdk/local-backend.ts +2 -0
- package/src/store/basketVisibleSet.test.ts +73 -3
- package/src/store/basketVisibleSet.ts +58 -75
- package/src/store/slices/bcfSlice.ts +9 -0
- package/src/utils/loadingUtils.ts +46 -0
- package/src/utils/serverDataModel.test.ts +90 -0
- package/src/utils/serverDataModel.ts +26 -37
- package/src/utils/spatialHierarchy.test.ts +38 -0
- package/src/utils/spatialHierarchy.ts +13 -23
- package/dist/assets/ifc-lite_bg-CyWQTvp5.wasm +0 -0
- package/dist/assets/index-ax1X2WPd.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,7 +3,7 @@
|
|
|
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, ChevronRight, Zap, Wrench, Plus, Package } 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';
|
|
@@ -33,6 +33,46 @@ const TYPE_CONFIG = {
|
|
|
33
33
|
perf: { icon: Zap, className: 'text-blue-500' },
|
|
34
34
|
} as const;
|
|
35
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
|
+
|
|
36
76
|
function AboutTab() {
|
|
37
77
|
const [showPackages, setShowPackages] = useState(false);
|
|
38
78
|
const packageVersions = __PACKAGE_VERSIONS__;
|
|
@@ -88,6 +128,9 @@ function AboutTab() {
|
|
|
88
128
|
))}
|
|
89
129
|
</div>
|
|
90
130
|
|
|
131
|
+
{/* Privacy & Security */}
|
|
132
|
+
<PrivacyBanner />
|
|
133
|
+
|
|
91
134
|
{/* Package Versions */}
|
|
92
135
|
{packageVersions.length > 0 && (
|
|
93
136
|
<div className="pt-2 border-t">
|
|
@@ -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" />
|
|
@@ -32,7 +32,7 @@ import { configureMutationView } from '@/utils/configureMutationView';
|
|
|
32
32
|
import { IfcQuery } from '@ifc-lite/query';
|
|
33
33
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
34
34
|
import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
35
|
-
import { EntityFlags, RelationshipType } from '@ifc-lite/data';
|
|
35
|
+
import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
|
|
36
36
|
import type { EntityRef, FederatedModel } from '@/store/types';
|
|
37
37
|
|
|
38
38
|
import { CoordVal, CoordRow } from './properties/CoordinateDisplay';
|
|
@@ -573,7 +573,7 @@ export function PropertiesPanel() {
|
|
|
573
573
|
};
|
|
574
574
|
}, [selectedEntity, model, ifcDataStore, mutationViews, mutationVersion]);
|
|
575
575
|
|
|
576
|
-
// Spatial containment info for spatial containers (Project,
|
|
576
|
+
// Spatial containment info for spatial containers (Project, Facility, Part, Storey, Space)
|
|
577
577
|
const spatialContainment = useMemo(() => {
|
|
578
578
|
if (!selectedEntity) return null;
|
|
579
579
|
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
@@ -583,9 +583,8 @@ export function PropertiesPanel() {
|
|
|
583
583
|
const hierarchy = dataStore.spatialHierarchy;
|
|
584
584
|
const typeName = dataStore.entities.getTypeName(expressId);
|
|
585
585
|
|
|
586
|
-
// Only show for spatial
|
|
587
|
-
|
|
588
|
-
if (!spatialTypes.includes(typeName)) return null;
|
|
586
|
+
// Only show for spatial structure elements.
|
|
587
|
+
if (!isSpatialStructureTypeName(typeName)) return null;
|
|
589
588
|
|
|
590
589
|
const stats: Array<{ label: string; value: string | number }> = [];
|
|
591
590
|
|
|
@@ -621,7 +620,7 @@ export function PropertiesPanel() {
|
|
|
621
620
|
// Also count from containment maps
|
|
622
621
|
const mapSources: Array<[string, Map<number, number[]> | undefined]> = [
|
|
623
622
|
['Elements (Site)', hierarchy.bySite],
|
|
624
|
-
['Elements (Building)', hierarchy.byBuilding],
|
|
623
|
+
['Elements (Building-like)', hierarchy.byBuilding],
|
|
625
624
|
['Elements (Storey)', hierarchy.byStorey],
|
|
626
625
|
['Elements (Space)', hierarchy.bySpace],
|
|
627
626
|
];
|
|
@@ -633,7 +632,7 @@ export function PropertiesPanel() {
|
|
|
633
632
|
}
|
|
634
633
|
|
|
635
634
|
// Elevation for storeys
|
|
636
|
-
if (typeName
|
|
635
|
+
if (isStoreyLikeSpatialTypeName(typeName)) {
|
|
637
636
|
const elevation = hierarchy.storeyElevations.get(expressId);
|
|
638
637
|
if (elevation !== undefined) {
|
|
639
638
|
stats.push({ label: 'Elevation', value: `${elevation.toFixed(2)} m` });
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
useIfcDataState,
|
|
24
24
|
} from '../../hooks/useViewerSelectors.js';
|
|
25
25
|
import { useModelSelection } from '../../hooks/useModelSelection.js';
|
|
26
|
+
import { useLatestRef } from '../../hooks/useLatestRef.js';
|
|
26
27
|
import {
|
|
27
28
|
getEntityBounds,
|
|
28
|
-
getEntityCenter,
|
|
29
29
|
getThemeClearColor,
|
|
30
30
|
type ViewportStateRefs,
|
|
31
31
|
} from '../../utils/viewportUtils.js';
|
|
@@ -127,6 +127,10 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
127
127
|
const handlePickForSelectionRef = useRef(handlePickForSelection);
|
|
128
128
|
useEffect(() => { handlePickForSelectionRef.current = handlePickForSelection; }, [handlePickForSelection]);
|
|
129
129
|
|
|
130
|
+
// Orbit pivot is now set dynamically at the start of each orbit drag by
|
|
131
|
+
// raycasting under the cursor (see useMouseControls/useTouchControls).
|
|
132
|
+
// No need for selection-based orbit center — cursor-based is always better.
|
|
133
|
+
|
|
130
134
|
// Multi-select handler: Ctrl+Click adds/removes from multi-selection
|
|
131
135
|
// Properly populates both selectedEntitiesSet (multi-model) and selectedEntityIds (legacy)
|
|
132
136
|
const handleMultiSelect = useCallback((globalId: number) => {
|
|
@@ -287,8 +291,6 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
287
291
|
separationLinesIntensity,
|
|
288
292
|
separationLinesRadius,
|
|
289
293
|
]);
|
|
290
|
-
const visualEnhancementRef = useRef<VisualEnhancementOptions>(visualEnhancement);
|
|
291
|
-
|
|
292
294
|
// Animation frame ref
|
|
293
295
|
const animationFrameRef = useRef<number | null>(null);
|
|
294
296
|
const lastFrameTimeRef = useRef<number>(0);
|
|
@@ -337,29 +339,29 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
337
339
|
max: { x: 100, y: 100, z: 100 },
|
|
338
340
|
});
|
|
339
341
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const selectedModelIndexRef = useRef<number | undefined>(selectedModelIndex);
|
|
342
|
+
// Refs that stay in sync with props/state automatically (no useEffect needed).
|
|
343
|
+
// Event handlers and the animation loop read .current to get the latest value.
|
|
344
|
+
const coordinateInfoRef = useLatestRef(coordinateInfo);
|
|
345
|
+
const hiddenEntitiesRef = useLatestRef(hiddenEntities);
|
|
346
|
+
const isolatedEntitiesRef = useLatestRef(isolatedEntities);
|
|
347
|
+
const selectedEntityIdRef = useLatestRef(selectedEntityId);
|
|
348
|
+
const selectedEntityIdsRef = useLatestRef(selectedEntityIds);
|
|
349
|
+
const selectedModelIndexRef = useLatestRef(selectedModelIndex);
|
|
349
350
|
const activeToolRef = useRef<string>(activeTool);
|
|
350
|
-
const pendingMeasurePointRef =
|
|
351
|
-
const activeMeasurementRef =
|
|
352
|
-
const snapEnabledRef =
|
|
353
|
-
const edgeLockStateRef =
|
|
354
|
-
const measurementConstraintEdgeRef =
|
|
355
|
-
const sectionPlaneRef =
|
|
356
|
-
const sectionRangeRef =
|
|
357
|
-
const
|
|
351
|
+
const pendingMeasurePointRef = useLatestRef(pendingMeasurePoint);
|
|
352
|
+
const activeMeasurementRef = useLatestRef(activeMeasurement);
|
|
353
|
+
const snapEnabledRef = useLatestRef(snapEnabled);
|
|
354
|
+
const edgeLockStateRef = useLatestRef(edgeLockState);
|
|
355
|
+
const measurementConstraintEdgeRef = useLatestRef(measurementConstraintEdge);
|
|
356
|
+
const sectionPlaneRef = useLatestRef(sectionPlane);
|
|
357
|
+
const sectionRangeRef = useLatestRef(sectionRange);
|
|
358
|
+
const visualEnhancementRef = useLatestRef(visualEnhancement);
|
|
359
|
+
const geometryRef = useLatestRef(geometry);
|
|
358
360
|
|
|
359
361
|
// Hover throttling
|
|
360
362
|
const lastHoverCheckRef = useRef<number>(0);
|
|
361
363
|
const hoverThrottleMs = 50; // Check hover every 50ms
|
|
362
|
-
const hoverTooltipsEnabledRef =
|
|
364
|
+
const hoverTooltipsEnabledRef = useLatestRef(hoverTooltipsEnabled);
|
|
363
365
|
|
|
364
366
|
// Measure tool throttling (adaptive based on raycast performance)
|
|
365
367
|
const measureRaycastPendingRef = useRef(false);
|
|
@@ -388,29 +390,18 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
388
390
|
canvasHeight: number;
|
|
389
391
|
} | null>(null);
|
|
390
392
|
|
|
391
|
-
//
|
|
392
|
-
useEffect(() => { coordinateInfoRef.current = coordinateInfo; }, [coordinateInfo]);
|
|
393
|
-
useEffect(() => { hiddenEntitiesRef.current = hiddenEntities; }, [hiddenEntities]);
|
|
394
|
-
useEffect(() => { isolatedEntitiesRef.current = isolatedEntities; }, [isolatedEntities]);
|
|
395
|
-
useEffect(() => { selectedEntityIdRef.current = selectedEntityId; }, [selectedEntityId]);
|
|
396
|
-
useEffect(() => { selectedEntityIdsRef.current = selectedEntityIds; }, [selectedEntityIds]);
|
|
397
|
-
useEffect(() => { selectedModelIndexRef.current = selectedModelIndex; }, [selectedModelIndex]);
|
|
398
|
-
useEffect(() => { activeToolRef.current = activeTool; }, [activeTool]);
|
|
399
|
-
useEffect(() => { pendingMeasurePointRef.current = pendingMeasurePoint; }, [pendingMeasurePoint]);
|
|
400
|
-
useEffect(() => { activeMeasurementRef.current = activeMeasurement; }, [activeMeasurement]);
|
|
401
|
-
useEffect(() => { snapEnabledRef.current = snapEnabled; }, [snapEnabled]);
|
|
402
|
-
useEffect(() => { edgeLockStateRef.current = edgeLockState; }, [edgeLockState]);
|
|
403
|
-
useEffect(() => { measurementConstraintEdgeRef.current = measurementConstraintEdge; }, [measurementConstraintEdge]);
|
|
404
|
-
useEffect(() => { sectionPlaneRef.current = sectionPlane; }, [sectionPlane]);
|
|
405
|
-
useEffect(() => { sectionRangeRef.current = sectionRange; }, [sectionRange]);
|
|
406
|
-
useEffect(() => { visualEnhancementRef.current = visualEnhancement; }, [visualEnhancement]);
|
|
393
|
+
// activeTool has a side effect (first-person mode), so keep as useEffect
|
|
407
394
|
useEffect(() => {
|
|
408
|
-
|
|
409
|
-
|
|
395
|
+
activeToolRef.current = activeTool;
|
|
396
|
+
const renderer = rendererRef.current;
|
|
397
|
+
if (renderer) {
|
|
398
|
+
const isWalk = activeTool === 'walk';
|
|
399
|
+
firstPersonModeRef.current = isWalk;
|
|
400
|
+
renderer.getCamera().enableFirstPersonMode(isWalk);
|
|
401
|
+
}
|
|
402
|
+
}, [activeTool]);
|
|
410
403
|
useEffect(() => {
|
|
411
|
-
hoverTooltipsEnabledRef.current = hoverTooltipsEnabled;
|
|
412
404
|
if (!hoverTooltipsEnabled) {
|
|
413
|
-
// Clear hover state when disabled
|
|
414
405
|
clearHover();
|
|
415
406
|
}
|
|
416
407
|
}, [hoverTooltipsEnabled, clearHover]);
|
|
@@ -436,8 +427,6 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
436
427
|
// Set cursor based on active tool
|
|
437
428
|
if (activeTool === 'measure') {
|
|
438
429
|
canvas.style.cursor = 'crosshair';
|
|
439
|
-
} else if (activeTool === 'pan' || activeTool === 'orbit') {
|
|
440
|
-
canvas.style.cursor = 'grab';
|
|
441
430
|
} else {
|
|
442
431
|
canvas.style.cursor = 'default';
|
|
443
432
|
}
|
|
@@ -520,19 +509,7 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
520
509
|
|
|
521
510
|
const camera = renderer.getCamera();
|
|
522
511
|
const renderCurrent = () => {
|
|
523
|
-
renderer.
|
|
524
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
525
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
526
|
-
selectedId: selectedEntityIdRef.current,
|
|
527
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
528
|
-
clearColor: clearColorRef.current,
|
|
529
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
530
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
531
|
-
...sectionPlaneRef.current,
|
|
532
|
-
min: sectionRangeRef.current?.min,
|
|
533
|
-
max: sectionRangeRef.current?.max,
|
|
534
|
-
} : undefined,
|
|
535
|
-
});
|
|
512
|
+
renderer.requestRender();
|
|
536
513
|
};
|
|
537
514
|
|
|
538
515
|
// Register camera callbacks for ViewCube and other controls
|
|
@@ -689,6 +666,10 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
689
666
|
// Sync on every render since mouseState is mutated directly by event handlers
|
|
690
667
|
mouseIsDraggingRef.current = mouseStateRef.current.isDragging;
|
|
691
668
|
|
|
669
|
+
// isInteracting: set by mouse/touch controls during drag, cleared on mouseup/touchend.
|
|
670
|
+
// The animation loop reads this to skip post-processing during rapid camera movement.
|
|
671
|
+
const isInteractingRef = useRef(false);
|
|
672
|
+
|
|
692
673
|
// ===== Extracted hooks =====
|
|
693
674
|
useMouseControls({
|
|
694
675
|
canvasRef,
|
|
@@ -716,6 +697,7 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
716
697
|
hoverTooltipsEnabledRef,
|
|
717
698
|
lastRenderTimeRef,
|
|
718
699
|
renderPendingRef,
|
|
700
|
+
isInteractingRef,
|
|
719
701
|
lastClickTimeRef,
|
|
720
702
|
lastClickPosRef,
|
|
721
703
|
lastCameraStateRef,
|
|
@@ -762,6 +744,7 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
762
744
|
sectionPlaneRef,
|
|
763
745
|
sectionRangeRef,
|
|
764
746
|
geometryRef,
|
|
747
|
+
isInteractingRef,
|
|
765
748
|
handlePickForSelection: (pickResult) => handlePickForSelectionRef.current(pickResult),
|
|
766
749
|
getPickOptions,
|
|
767
750
|
});
|
|
@@ -802,6 +785,9 @@ export function Viewport({ geometry, geometryVersion, coordinateInfo, computedIs
|
|
|
802
785
|
sectionPlaneRef,
|
|
803
786
|
sectionRangeRef,
|
|
804
787
|
visualEnhancementRef,
|
|
788
|
+
selectedEntityIdsRef,
|
|
789
|
+
coordinateInfoRef,
|
|
790
|
+
isInteractingRef,
|
|
805
791
|
lastCameraStateRef,
|
|
806
792
|
updateCameraRotationRealtime,
|
|
807
793
|
calculateScale,
|
|
@@ -8,6 +8,7 @@ import { ViewportOverlays } from './ViewportOverlays';
|
|
|
8
8
|
import { ToolOverlays } from './ToolOverlays';
|
|
9
9
|
import { Section2DPanel } from './Section2DPanel';
|
|
10
10
|
import { BasketPresentationDock } from './BasketPresentationDock';
|
|
11
|
+
import { BCFOverlay } from './bcf/BCFOverlay';
|
|
11
12
|
import { useViewerStore } from '@/store';
|
|
12
13
|
import { collectIfcBuildingStoreyElementsWithIfcSpace } from '@/store/basketVisibleSet';
|
|
13
14
|
import { useIfc } from '@/hooks/useIfc';
|
|
@@ -32,6 +33,7 @@ export function ViewportContainer() {
|
|
|
32
33
|
// Multi-model support: get all loaded models from store (for merged geometry)
|
|
33
34
|
const storeModels = useViewerStore((s) => s.models);
|
|
34
35
|
const resetViewerState = useViewerStore((s) => s.resetViewerState);
|
|
36
|
+
const bcfOverlayVisible = useViewerStore((s) => s.bcfOverlayVisible);
|
|
35
37
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
36
38
|
const [isDragging, setIsDragging] = useState(false);
|
|
37
39
|
const [showTroubleshooting, setShowTroubleshooting] = useState(false);
|
|
@@ -611,6 +613,7 @@ export function ViewportContainer() {
|
|
|
611
613
|
computedIsolatedIds={computedIsolatedIds}
|
|
612
614
|
modelIdToIndex={modelIdToIndex}
|
|
613
615
|
/>
|
|
616
|
+
{bcfOverlayVisible && <BCFOverlay />}
|
|
614
617
|
<ViewportOverlays />
|
|
615
618
|
<ToolOverlays />
|
|
616
619
|
<BasketPresentationDock />
|
|
@@ -18,7 +18,7 @@ import { cn } from '@/lib/utils';
|
|
|
18
18
|
import { ViewCube, type ViewCubeRef } from './ViewCube';
|
|
19
19
|
import { AxisHelper, type AxisHelperRef } from './AxisHelper';
|
|
20
20
|
|
|
21
|
-
export function ViewportOverlays() {
|
|
21
|
+
export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: boolean } = {}) {
|
|
22
22
|
const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
|
|
23
23
|
const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
|
|
24
24
|
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
@@ -180,15 +180,17 @@ export function ViewportOverlays() {
|
|
|
180
180
|
)}
|
|
181
181
|
|
|
182
182
|
{/* ViewCube (top-right) */}
|
|
183
|
-
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
{!hideViewCube && (
|
|
184
|
+
<div className="absolute top-6 right-6">
|
|
185
|
+
<ViewCube
|
|
186
|
+
ref={viewCubeRef}
|
|
187
|
+
onViewChange={handleViewChange}
|
|
188
|
+
onDrag={(deltaX, deltaY) => cameraCallbacks.orbit?.(deltaX, deltaY)}
|
|
189
|
+
rotationX={initialRotationX}
|
|
190
|
+
rotationY={initialRotationY}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
192
194
|
|
|
193
195
|
{/* Axis Helper (bottom-left, above scale bar) - IFC Z-up convention */}
|
|
194
196
|
<div className="absolute bottom-16 left-4">
|