@ifc-lite/viewer 1.17.6 → 1.19.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 +20 -15
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +949 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/sandbox-Baez7n-t.js +9682 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +11 -10
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/ChatPanel.tsx +64 -2
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -12
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +29 -2
- package/src/components/viewer/ViewportContainer.tsx +45 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +581 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +89 -13
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +6 -0
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +8 -3
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +79 -1
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/pointCloudSlice.ts +102 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/types.ts +7 -0
- package/src/store.ts +14 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
|
@@ -9,19 +9,23 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
|
|
9
9
|
import { MainToolbar } from './MainToolbar';
|
|
10
10
|
import { HierarchyPanel } from './HierarchyPanel';
|
|
11
11
|
import { PropertiesPanel } from './PropertiesPanel';
|
|
12
|
+
import { AddElementPanel } from './AddElementPanel';
|
|
12
13
|
import { StatusBar } from './StatusBar';
|
|
13
14
|
import { ViewportContainer } from './ViewportContainer';
|
|
14
15
|
import { KeyboardShortcutsDialog, useKeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
|
15
16
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
|
16
17
|
import { useViewerStore } from '@/store';
|
|
17
18
|
import { EntityContextMenu } from './EntityContextMenu';
|
|
19
|
+
import { useDuplicateShortcut } from './useDuplicateShortcut';
|
|
18
20
|
import { HoverTooltip } from './HoverTooltip';
|
|
19
21
|
import { BCFPanel } from './BCFPanel';
|
|
20
22
|
import { IDSPanel } from './IDSPanel';
|
|
21
23
|
import { LensPanel } from './LensPanel';
|
|
22
24
|
import { ListPanel } from './lists/ListPanel';
|
|
23
25
|
import { ScriptPanel } from './ScriptPanel';
|
|
26
|
+
import { GanttPanel } from './schedule/GanttPanel';
|
|
24
27
|
import { CommandPalette } from './CommandPalette';
|
|
28
|
+
import { SearchModal } from './SearchModal';
|
|
25
29
|
import { DesktopEntitlementBanner } from './DesktopEntitlementBanner';
|
|
26
30
|
import {
|
|
27
31
|
closeActiveAnalysisExtension,
|
|
@@ -37,6 +41,8 @@ const BOTTOM_PANEL_MAX_RATIO = 0.7; // max 70% of container
|
|
|
37
41
|
export function ViewerLayout() {
|
|
38
42
|
// Initialize keyboard shortcuts
|
|
39
43
|
useKeyboardShortcuts();
|
|
44
|
+
// ⌘D / Ctrl+D to duplicate the current selection.
|
|
45
|
+
useDuplicateShortcut();
|
|
40
46
|
const shortcutsDialog = useKeyboardShortcutsDialog();
|
|
41
47
|
|
|
42
48
|
// Command palette state
|
|
@@ -76,6 +82,8 @@ export function ViewerLayout() {
|
|
|
76
82
|
const setRightPanelCollapsed = useViewerStore((s) => s.setRightPanelCollapsed);
|
|
77
83
|
const bcfPanelVisible = useViewerStore((s) => s.bcfPanelVisible);
|
|
78
84
|
const setBcfPanelVisible = useViewerStore((s) => s.setBcfPanelVisible);
|
|
85
|
+
const activeTool = useViewerStore((s) => s.activeTool);
|
|
86
|
+
const setActiveTool = useViewerStore((s) => s.setActiveTool);
|
|
79
87
|
const idsPanelVisible = useViewerStore((s) => s.idsPanelVisible);
|
|
80
88
|
const setIdsPanelVisible = useViewerStore((s) => s.setIdsPanelVisible);
|
|
81
89
|
const listPanelVisible = useViewerStore((s) => s.listPanelVisible);
|
|
@@ -84,6 +92,8 @@ export function ViewerLayout() {
|
|
|
84
92
|
const setLensPanelVisible = useViewerStore((s) => s.setLensPanelVisible);
|
|
85
93
|
const scriptPanelVisible = useViewerStore((s) => s.scriptPanelVisible);
|
|
86
94
|
const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible);
|
|
95
|
+
const ganttPanelVisible = useViewerStore((s) => s.ganttPanelVisible);
|
|
96
|
+
const setGanttPanelVisible = useViewerStore((s) => s.setGanttPanelVisible);
|
|
87
97
|
const analysisExtensionState = useSyncExternalStore(
|
|
88
98
|
subscribeAnalysisExtensions,
|
|
89
99
|
getAnalysisExtensionsSnapshot,
|
|
@@ -200,6 +210,7 @@ export function ViewerLayout() {
|
|
|
200
210
|
<EntityContextMenu />
|
|
201
211
|
<HoverTooltip />
|
|
202
212
|
<CommandPalette open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen} />
|
|
213
|
+
<SearchModal />
|
|
203
214
|
|
|
204
215
|
{/* Main Toolbar */}
|
|
205
216
|
<MainToolbar onShowShortcuts={shortcutsDialog.toggle} />
|
|
@@ -256,6 +267,8 @@ export function ViewerLayout() {
|
|
|
256
267
|
<div className="h-full w-full overflow-hidden panel-container">
|
|
257
268
|
{activeRightAnalysisExtension ? (
|
|
258
269
|
activeRightAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
270
|
+
) : activeTool === 'addElement' ? (
|
|
271
|
+
<AddElementPanel onClose={() => setActiveTool('select')} />
|
|
259
272
|
) : lensPanelVisible ? (
|
|
260
273
|
<LensPanel onClose={() => setLensPanelVisible(false)} />
|
|
261
274
|
) : idsPanelVisible ? (
|
|
@@ -270,8 +283,8 @@ export function ViewerLayout() {
|
|
|
270
283
|
</PanelGroup>
|
|
271
284
|
</div>
|
|
272
285
|
|
|
273
|
-
{/* Bottom Panel - Lists
|
|
274
|
-
{(listPanelVisible || scriptPanelVisible || !!activeBottomAnalysisExtension) && (
|
|
286
|
+
{/* Bottom Panel - Lists / Script / Gantt / analysis ext (custom resizable) */}
|
|
287
|
+
{(listPanelVisible || scriptPanelVisible || ganttPanelVisible || !!activeBottomAnalysisExtension) && (
|
|
275
288
|
<div style={{ height: bottomHeight, flexShrink: 0 }} className="relative">
|
|
276
289
|
{/* Drag handle */}
|
|
277
290
|
<div
|
|
@@ -281,6 +294,8 @@ export function ViewerLayout() {
|
|
|
281
294
|
<div className="h-full w-full overflow-hidden border-t pt-1.5">
|
|
282
295
|
{activeBottomAnalysisExtension ? (
|
|
283
296
|
activeBottomAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
297
|
+
) : ganttPanelVisible ? (
|
|
298
|
+
<GanttPanel onClose={() => setGanttPanelVisible(false)} />
|
|
284
299
|
) : scriptPanelVisible ? (
|
|
285
300
|
<ScriptPanel onClose={() => setScriptPanelVisible(false)} />
|
|
286
301
|
) : (
|
|
@@ -326,7 +341,7 @@ export function ViewerLayout() {
|
|
|
326
341
|
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-background border-t rounded-t-xl shadow-xl z-40 animate-in slide-in-from-bottom">
|
|
327
342
|
<div className="flex items-center justify-between p-2 border-b">
|
|
328
343
|
<span className="font-medium text-sm">
|
|
329
|
-
{activeAnalysisExtension ? activeAnalysisExtension.label : scriptPanelVisible ? 'Script' : listPanelVisible ? 'Lists' : lensPanelVisible ? 'Lens' : idsPanelVisible ? 'IDS Validation' : bcfPanelVisible ? 'BCF Issues' : '
|
|
344
|
+
{activeAnalysisExtension ? activeAnalysisExtension.label : ganttPanelVisible ? 'Schedule' : scriptPanelVisible ? 'Script' : listPanelVisible ? 'Lists' : lensPanelVisible ? 'Lens' : idsPanelVisible ? 'IDS Validation' : bcfPanelVisible ? 'BCF Issues' : 'Inspector'}
|
|
330
345
|
</span>
|
|
331
346
|
<button
|
|
332
347
|
className="p-1 hover:bg-muted rounded"
|
|
@@ -334,6 +349,7 @@ export function ViewerLayout() {
|
|
|
334
349
|
setRightPanelCollapsed(true);
|
|
335
350
|
if (scriptPanelVisible) setScriptPanelVisible(false);
|
|
336
351
|
if (listPanelVisible) setListPanelVisible(false);
|
|
352
|
+
if (ganttPanelVisible) setGanttPanelVisible(false);
|
|
337
353
|
if (bcfPanelVisible) setBcfPanelVisible(false);
|
|
338
354
|
if (lensPanelVisible) setLensPanelVisible(false);
|
|
339
355
|
if (idsPanelVisible) setIdsPanelVisible(false);
|
|
@@ -351,10 +367,14 @@ export function ViewerLayout() {
|
|
|
351
367
|
activeBottomAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
352
368
|
) : activeRightAnalysisExtension ? (
|
|
353
369
|
activeRightAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
370
|
+
) : ganttPanelVisible ? (
|
|
371
|
+
<GanttPanel onClose={() => setGanttPanelVisible(false)} />
|
|
354
372
|
) : scriptPanelVisible ? (
|
|
355
373
|
<ScriptPanel onClose={() => setScriptPanelVisible(false)} />
|
|
356
374
|
) : listPanelVisible ? (
|
|
357
375
|
<ListPanel onClose={() => setListPanelVisible(false)} />
|
|
376
|
+
) : activeTool === 'addElement' ? (
|
|
377
|
+
<AddElementPanel onClose={() => setActiveTool('select')} />
|
|
358
378
|
) : lensPanelVisible ? (
|
|
359
379
|
<LensPanel onClose={() => setLensPanelVisible(false)} />
|
|
360
380
|
) : idsPanelVisible ? (
|
|
@@ -386,7 +406,7 @@ export function ViewerLayout() {
|
|
|
386
406
|
setRightPanelCollapsed(!rightPanelCollapsed);
|
|
387
407
|
}}
|
|
388
408
|
>
|
|
389
|
-
|
|
409
|
+
Inspector
|
|
390
410
|
</button>
|
|
391
411
|
</div>
|
|
392
412
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
10
10
|
import { Renderer, type VisualEnhancementOptions } from '@ifc-lite/renderer';
|
|
11
|
-
import type { MeshData, CoordinateInfo } from '@ifc-lite/geometry';
|
|
11
|
+
import type { MeshData, CoordinateInfo, PointCloudAsset } from '@ifc-lite/geometry';
|
|
12
12
|
import { useViewerStore, resolveEntityRef, type MeasurePoint, type SnapVisualization } from '@/store';
|
|
13
13
|
import {
|
|
14
14
|
useSelectionState,
|
|
@@ -36,6 +36,8 @@ import { useTouchControls, type TouchState } from './useTouchControls.js';
|
|
|
36
36
|
import { useKeyboardControls } from './useKeyboardControls.js';
|
|
37
37
|
import { useAnimationLoop } from './useAnimationLoop.js';
|
|
38
38
|
import { useGeometryStreaming } from './useGeometryStreaming.js';
|
|
39
|
+
import { usePointCloudSync } from './usePointCloudSync.js';
|
|
40
|
+
import { usePointCloudLifecycle } from './usePointCloudLifecycle.js';
|
|
39
41
|
import { useRenderUpdates } from './useRenderUpdates.js';
|
|
40
42
|
|
|
41
43
|
interface ViewportProps {
|
|
@@ -43,6 +45,8 @@ interface ViewportProps {
|
|
|
43
45
|
/** Monotonic counter that increments when geometry changes — used to trigger
|
|
44
46
|
* streaming effects even when the geometry array reference is stable. */
|
|
45
47
|
geometryVersion?: number;
|
|
48
|
+
/** Point cloud assets aggregated across visible federated models. */
|
|
49
|
+
pointClouds?: ReadonlyArray<PointCloudAsset> | null;
|
|
46
50
|
coordinateInfo?: CoordinateInfo;
|
|
47
51
|
computedIsolatedIds?: Set<number> | null;
|
|
48
52
|
modelIdToIndex?: Map<string, number>;
|
|
@@ -56,6 +60,7 @@ interface ViewportProps {
|
|
|
56
60
|
export function Viewport({
|
|
57
61
|
geometry,
|
|
58
62
|
geometryVersion,
|
|
63
|
+
pointClouds,
|
|
59
64
|
coordinateInfo,
|
|
60
65
|
computedIsolatedIds,
|
|
61
66
|
modelIdToIndex,
|
|
@@ -456,11 +461,21 @@ export function Viewport({
|
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
// Set cursor based on active tool
|
|
459
|
-
if (activeTool === 'measure') {
|
|
464
|
+
if (activeTool === 'measure' || activeTool === 'annotate' || activeTool === 'addElement') {
|
|
460
465
|
canvas.style.cursor = 'crosshair';
|
|
461
466
|
} else {
|
|
462
467
|
canvas.style.cursor = 'default';
|
|
463
468
|
}
|
|
469
|
+
|
|
470
|
+
// Clear add-element pending state + hover point when leaving the
|
|
471
|
+
// tool so the SVG overlay doesn't paint stale geometry from a
|
|
472
|
+
// previous session.
|
|
473
|
+
if (activeTool !== 'addElement') {
|
|
474
|
+
const state = useViewerStore.getState();
|
|
475
|
+
if (state.addElementPendingPoints.length > 0 || state.addElementHoverPoint !== null) {
|
|
476
|
+
state.clearAddElementPending();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
464
479
|
}, [activeTool, activeMeasurement, cancelMeasurement]);
|
|
465
480
|
|
|
466
481
|
// Helper: calculate scale bar value (world-space size for 96px scale bar)
|
|
@@ -846,6 +861,18 @@ export function Viewport({
|
|
|
846
861
|
onGeometryReleased,
|
|
847
862
|
});
|
|
848
863
|
|
|
864
|
+
usePointCloudSync({
|
|
865
|
+
rendererRef,
|
|
866
|
+
isInitialized,
|
|
867
|
+
pointClouds,
|
|
868
|
+
hasMeshes: (geometry?.length ?? 0) > 0,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
usePointCloudLifecycle({
|
|
872
|
+
rendererRef,
|
|
873
|
+
isInitialized,
|
|
874
|
+
});
|
|
875
|
+
|
|
849
876
|
useRenderUpdates({
|
|
850
877
|
rendererRef,
|
|
851
878
|
isInitialized,
|
|
@@ -6,6 +6,7 @@ import { useMemo, useRef, useState, useCallback, useEffect, useSyncExternalStore
|
|
|
6
6
|
import { Viewport } from './Viewport';
|
|
7
7
|
import { ViewportOverlays } from './ViewportOverlays';
|
|
8
8
|
import { ToolOverlays } from './ToolOverlays';
|
|
9
|
+
import { AnnotationLayer } from './annotations/AnnotationLayer';
|
|
9
10
|
import { Section2DPanel } from './Section2DPanel';
|
|
10
11
|
import { BasketPresentationDock } from './BasketPresentationDock';
|
|
11
12
|
import { BCFOverlay } from './bcf/BCFOverlay';
|
|
@@ -19,8 +20,10 @@ import { openIfcFileDialog } from '@/services/file-dialog';
|
|
|
19
20
|
import { logToDesktopTerminal } from '@/services/desktop-logger';
|
|
20
21
|
import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRecentFiles, type RecentFileEntry } from '@/lib/recent-files';
|
|
21
22
|
import { isTauri } from '@/lib/platform';
|
|
23
|
+
import { toast } from '@/components/ui/toast';
|
|
24
|
+
import { describeUnsupportedFormat } from '@/hooks/ingest/pointCloudIngest';
|
|
22
25
|
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3 } from 'lucide-react';
|
|
23
|
-
import type { MeshData, CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
26
|
+
import type { MeshData, CoordinateInfo, GeometryResult, PointCloudAsset } from '@ifc-lite/geometry';
|
|
24
27
|
import { type IfcDataStore } from '@ifc-lite/parser';
|
|
25
28
|
import { getEffectiveGeoreference } from '@/lib/geo/effective-georef';
|
|
26
29
|
|
|
@@ -173,6 +176,30 @@ export function ViewportContainer() {
|
|
|
173
176
|
return geometryResult;
|
|
174
177
|
}, [storeModels, geometryResult, modelIdToIndex]);
|
|
175
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Aggregate point clouds across visible models.
|
|
181
|
+
*
|
|
182
|
+
* Phase 0: identity-stamping with modelIndex. Returns the same array
|
|
183
|
+
* reference when nothing has changed so the consumer effect skips work.
|
|
184
|
+
*/
|
|
185
|
+
const mergedPointClouds = useMemo(() => {
|
|
186
|
+
const collected: PointCloudAsset[] = [];
|
|
187
|
+
if (storeModels.size > 0) {
|
|
188
|
+
for (const [modelId, model] of storeModels) {
|
|
189
|
+
if (!model.visible) continue;
|
|
190
|
+
const assets = model.geometryResult?.pointClouds;
|
|
191
|
+
if (!assets || assets.length === 0) continue;
|
|
192
|
+
const modelIndex = modelIdToIndex.get(modelId) ?? 0;
|
|
193
|
+
for (const asset of assets) {
|
|
194
|
+
collected.push(asset.modelIndex === modelIndex ? asset : { ...asset, modelIndex });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (geometryResult?.pointClouds) {
|
|
198
|
+
collected.push(...geometryResult.pointClouds);
|
|
199
|
+
}
|
|
200
|
+
return collected;
|
|
201
|
+
}, [storeModels, geometryResult, modelIdToIndex]);
|
|
202
|
+
|
|
176
203
|
// Extract georeferencing info merged with any live mutations (for Cesium overlay).
|
|
177
204
|
// Reacts to: model load, Cesium toggle, and every georef field edit.
|
|
178
205
|
const georef = useMemo(() => {
|
|
@@ -280,12 +307,22 @@ export function ViewportContainer() {
|
|
|
280
307
|
return;
|
|
281
308
|
}
|
|
282
309
|
|
|
283
|
-
// Filter to supported files (IFC, IFCX, GLB)
|
|
284
|
-
const
|
|
310
|
+
// Filter to supported files (IFC, IFCX, GLB, point clouds)
|
|
311
|
+
const allDropped = Array.from(e.dataTransfer.files);
|
|
312
|
+
const supportedFiles = allDropped.filter(
|
|
285
313
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
314
|
+
|| 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')
|
|
286
315
|
);
|
|
287
316
|
|
|
288
|
-
if (supportedFiles.length === 0)
|
|
317
|
+
if (supportedFiles.length === 0) {
|
|
318
|
+
// Tell the user *why* — common case is a Recap project / SketchUp
|
|
319
|
+
// file dropped because they assumed our viewer would understand it.
|
|
320
|
+
const explained = allDropped.find((f) => describeUnsupportedFormat(f.name));
|
|
321
|
+
if (explained) {
|
|
322
|
+
toast.error(`${explained.name}: ${describeUnsupportedFormat(explained.name)}`);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
289
326
|
|
|
290
327
|
recordRecentFiles(supportedFiles.map((file) => ({ name: file.name, size: file.size })));
|
|
291
328
|
void cacheFileBlobs(supportedFiles);
|
|
@@ -317,6 +354,7 @@ export function ViewportContainer() {
|
|
|
317
354
|
// Filter to supported files (IFC, IFCX, GLB)
|
|
318
355
|
const supportedFiles = Array.from(files).filter(
|
|
319
356
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
357
|
+
|| 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')
|
|
320
358
|
);
|
|
321
359
|
|
|
322
360
|
if (supportedFiles.length === 0) return;
|
|
@@ -528,7 +566,7 @@ export function ViewportContainer() {
|
|
|
528
566
|
<input
|
|
529
567
|
ref={fileInputRef}
|
|
530
568
|
type="file"
|
|
531
|
-
accept=".ifc,.ifcx,.glb"
|
|
569
|
+
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
|
|
532
570
|
multiple
|
|
533
571
|
onChange={handleFileSelect}
|
|
534
572
|
className="hidden"
|
|
@@ -844,6 +882,7 @@ export function ViewportContainer() {
|
|
|
844
882
|
<Viewport
|
|
845
883
|
geometry={filteredGeometry}
|
|
846
884
|
geometryVersion={geometryVersion}
|
|
885
|
+
pointClouds={mergedPointClouds}
|
|
847
886
|
coordinateInfo={mergedGeometryResult?.coordinateInfo}
|
|
848
887
|
computedIsolatedIds={computedIsolatedIds}
|
|
849
888
|
modelIdToIndex={modelIdToIndex}
|
|
@@ -851,6 +890,7 @@ export function ViewportContainer() {
|
|
|
851
890
|
releaseGeometryAfterStream={false}
|
|
852
891
|
onGeometryReleased={releaseGeometryMemory}
|
|
853
892
|
/>
|
|
893
|
+
<AnnotationLayer />
|
|
854
894
|
{bcfOverlayVisible && <BCFOverlay />}
|
|
855
895
|
<ViewportOverlays />
|
|
856
896
|
<ToolOverlays />
|
|
@@ -22,10 +22,11 @@ import { goHomeFromStore } from '@/store/homeView';
|
|
|
22
22
|
import { useIfc } from '@/hooks/useIfc';
|
|
23
23
|
import { cn } from '@/lib/utils';
|
|
24
24
|
import { isTauri } from '@/lib/platform';
|
|
25
|
-
|
|
26
|
-
const isDesktop = isTauri();
|
|
27
25
|
import { ViewCube, type ViewCubeRef } from './ViewCube';
|
|
28
26
|
import { AxisHelper, type AxisHelperRef } from './AxisHelper';
|
|
27
|
+
import { PointCloudPanel } from './PointCloudPanel';
|
|
28
|
+
|
|
29
|
+
const isDesktop = isTauri();
|
|
29
30
|
|
|
30
31
|
export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: boolean } = {}) {
|
|
31
32
|
const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
|
|
@@ -149,6 +150,7 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
149
150
|
|
|
150
151
|
return (
|
|
151
152
|
<>
|
|
153
|
+
<PointCloudPanelMount />
|
|
152
154
|
{/* Bottom-right: Cesium settings overlay OR Navigation controls (Cesium is web-only) */}
|
|
153
155
|
{cesiumEnabled && !isDesktop ? (
|
|
154
156
|
<CesiumSettingsOverlay
|
|
@@ -314,3 +316,12 @@ function CesiumSettingsOverlay({
|
|
|
314
316
|
</div>
|
|
315
317
|
);
|
|
316
318
|
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Tiny indirection so the panel can subscribe to its own slice without
|
|
322
|
+
* pulling extra state into the parent overlay component.
|
|
323
|
+
*/
|
|
324
|
+
function PointCloudPanelMount() {
|
|
325
|
+
const count = useViewerStore((s) => s.pointCloudAssetCount);
|
|
326
|
+
return <PointCloudPanel assetCount={count} />;
|
|
327
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
* The inline note input that appears at the click site when the user
|
|
7
|
+
* drops a fresh pin with the Annotate tool. Shape mirrors the popover's
|
|
8
|
+
* edit mode so muscle memory carries over, but the chrome is lighter
|
|
9
|
+
* (a guiding label, no entity-context header) since this is a
|
|
10
|
+
* commit-or-cancel surface.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { Check, X } from 'lucide-react';
|
|
15
|
+
import { Button } from '@/components/ui/button';
|
|
16
|
+
import { cn } from '@/lib/utils';
|
|
17
|
+
|
|
18
|
+
const MAX_NOTE_LEN = 2000;
|
|
19
|
+
const SOFT_NOTE_LIMIT = 200;
|
|
20
|
+
const INPUT_WIDTH = 280;
|
|
21
|
+
const INPUT_OFFSET_X = 16;
|
|
22
|
+
|
|
23
|
+
export interface AnnotationDropInputProps {
|
|
24
|
+
anchorX: number;
|
|
25
|
+
anchorY: number;
|
|
26
|
+
canvasWidth: number;
|
|
27
|
+
canvasHeight: number;
|
|
28
|
+
/** Resolved entity type when the drop landed on a known mesh. */
|
|
29
|
+
entityType?: string | null;
|
|
30
|
+
entityExpressId?: number | null;
|
|
31
|
+
onSave: (note: string) => void;
|
|
32
|
+
onCancel: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AnnotationDropInput({
|
|
36
|
+
anchorX,
|
|
37
|
+
anchorY,
|
|
38
|
+
canvasWidth,
|
|
39
|
+
canvasHeight,
|
|
40
|
+
entityType,
|
|
41
|
+
entityExpressId,
|
|
42
|
+
onSave,
|
|
43
|
+
onCancel,
|
|
44
|
+
}: AnnotationDropInputProps) {
|
|
45
|
+
const [draft, setDraft] = useState('');
|
|
46
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
47
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
textareaRef.current?.focus();
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Cancel on outside click, but defer registration so the click that
|
|
54
|
+
// dropped the pin doesn't immediately close the input.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const handler = (e: MouseEvent) => {
|
|
57
|
+
const node = containerRef.current;
|
|
58
|
+
if (!node) return;
|
|
59
|
+
if (node.contains(e.target as Node)) return;
|
|
60
|
+
// Empty draft on outside-click → silent cancel; non-empty
|
|
61
|
+
// → commit the draft (matches "blur to save" feel without
|
|
62
|
+
// destroying typed content). An over-limit draft is rejected
|
|
63
|
+
// consistently with the disabled save button.
|
|
64
|
+
if (draft.trim().length === 0 || draft.length > MAX_NOTE_LEN) {
|
|
65
|
+
onCancel();
|
|
66
|
+
} else {
|
|
67
|
+
onSave(draft);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const id = window.setTimeout(() => {
|
|
71
|
+
document.addEventListener('mousedown', handler);
|
|
72
|
+
}, 0);
|
|
73
|
+
return () => {
|
|
74
|
+
window.clearTimeout(id);
|
|
75
|
+
document.removeEventListener('mousedown', handler);
|
|
76
|
+
};
|
|
77
|
+
}, [draft, onSave, onCancel]);
|
|
78
|
+
|
|
79
|
+
const handleKeyDown = useCallback(
|
|
80
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
81
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
if (draft.trim().length === 0 || draft.length > MAX_NOTE_LEN) {
|
|
84
|
+
// Over-limit Enter does nothing — match the disabled button.
|
|
85
|
+
if (draft.trim().length === 0) onCancel();
|
|
86
|
+
} else {
|
|
87
|
+
onSave(draft);
|
|
88
|
+
}
|
|
89
|
+
} else if (e.key === 'Escape') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
onCancel();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[draft, onSave, onCancel],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const wantsLeft = anchorX + INPUT_OFFSET_X + INPUT_WIDTH > canvasWidth;
|
|
98
|
+
const left = wantsLeft
|
|
99
|
+
? Math.max(8, anchorX - INPUT_OFFSET_X - INPUT_WIDTH)
|
|
100
|
+
: Math.min(anchorX + INPUT_OFFSET_X, canvasWidth - INPUT_WIDTH - 8);
|
|
101
|
+
const top = Math.min(Math.max(8, anchorY - 8), canvasHeight - 140);
|
|
102
|
+
|
|
103
|
+
const charCountVisible = draft.length >= SOFT_NOTE_LIMIT;
|
|
104
|
+
const overSoftLimit = draft.length > SOFT_NOTE_LIMIT;
|
|
105
|
+
const overHardLimit = draft.length > MAX_NOTE_LEN;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
ref={containerRef}
|
|
110
|
+
role="dialog"
|
|
111
|
+
aria-label="New annotation"
|
|
112
|
+
style={{ left, top, width: INPUT_WIDTH }}
|
|
113
|
+
className={cn(
|
|
114
|
+
'absolute z-[60] pointer-events-auto',
|
|
115
|
+
'rounded-md border border-amber-400/70 dark:border-amber-600/40',
|
|
116
|
+
'bg-white/95 dark:bg-zinc-950/95 backdrop-blur-md',
|
|
117
|
+
'shadow-[0_8px_32px_rgba(0,0,0,0.18)]',
|
|
118
|
+
'overflow-hidden',
|
|
119
|
+
'animate-in fade-in-0 zoom-in-95 duration-150',
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{/* Guiding label — explicit so the user knows what to type and
|
|
123
|
+
establishes "this is for capturing intent, not chat". */}
|
|
124
|
+
<div className="px-3 py-1.5 border-b border-zinc-200 dark:border-zinc-800 bg-amber-50/40 dark:bg-amber-950/20">
|
|
125
|
+
<span className="font-mono text-[10px] uppercase tracking-wider text-amber-700 dark:text-amber-300">
|
|
126
|
+
What's worth noting?
|
|
127
|
+
{entityType && (
|
|
128
|
+
<span className="ml-1.5 text-zinc-500 dark:text-zinc-400">
|
|
129
|
+
· {entityType}
|
|
130
|
+
{entityExpressId !== null && entityExpressId !== undefined && ` #${entityExpressId}`}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="px-3 py-2.5">
|
|
137
|
+
<textarea
|
|
138
|
+
ref={textareaRef}
|
|
139
|
+
value={draft}
|
|
140
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
141
|
+
onKeyDown={handleKeyDown}
|
|
142
|
+
placeholder="A short note — flag a defect, ask a question, leave context…"
|
|
143
|
+
rows={3}
|
|
144
|
+
maxLength={MAX_NOTE_LEN + 100}
|
|
145
|
+
className={cn(
|
|
146
|
+
'w-full resize-none font-mono text-[11px] leading-relaxed',
|
|
147
|
+
'bg-zinc-50 dark:bg-zinc-900/60 text-zinc-800 dark:text-zinc-200',
|
|
148
|
+
'border border-zinc-200 dark:border-zinc-800 rounded-sm',
|
|
149
|
+
'px-2 py-1.5 outline-none focus:ring-1',
|
|
150
|
+
overHardLimit
|
|
151
|
+
? 'focus:ring-red-400 border-red-300 dark:border-red-700/60'
|
|
152
|
+
: 'focus:ring-amber-400/50 focus:border-amber-300/60',
|
|
153
|
+
)}
|
|
154
|
+
spellCheck
|
|
155
|
+
autoCorrect="on"
|
|
156
|
+
/>
|
|
157
|
+
<div className="mt-1.5 flex items-center justify-between gap-2 text-[10px] font-mono">
|
|
158
|
+
<span className="text-zinc-400 dark:text-zinc-500">
|
|
159
|
+
⏎ save · ⇧⏎ newline · esc cancel
|
|
160
|
+
</span>
|
|
161
|
+
{charCountVisible && (
|
|
162
|
+
<span
|
|
163
|
+
className={cn(
|
|
164
|
+
'tabular-nums',
|
|
165
|
+
overHardLimit
|
|
166
|
+
? 'text-red-500'
|
|
167
|
+
: overSoftLimit
|
|
168
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
169
|
+
: 'text-zinc-400',
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{draft.length}/{MAX_NOTE_LEN}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
<div className="mt-2 flex items-center justify-end gap-1">
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="sm"
|
|
180
|
+
className="h-7 px-2 text-[11px]"
|
|
181
|
+
onClick={onCancel}
|
|
182
|
+
>
|
|
183
|
+
<X className="h-3 w-3 mr-1" />
|
|
184
|
+
Cancel
|
|
185
|
+
</Button>
|
|
186
|
+
<Button
|
|
187
|
+
size="sm"
|
|
188
|
+
className="h-7 px-2 text-[11px] bg-amber-500 hover:bg-amber-500/90 text-white"
|
|
189
|
+
onClick={() => {
|
|
190
|
+
if (overHardLimit) return;
|
|
191
|
+
if (draft.trim().length === 0) onCancel();
|
|
192
|
+
else onSave(draft);
|
|
193
|
+
}}
|
|
194
|
+
disabled={overHardLimit}
|
|
195
|
+
>
|
|
196
|
+
<Check className="h-3 w-3 mr-1" />
|
|
197
|
+
Drop pin
|
|
198
|
+
</Button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|