@ifc-lite/viewer 1.17.4 → 1.18.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 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- 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/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- 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 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- 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/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -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 +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- 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 +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -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 +20 -2
- 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/main.tsx +1 -10
- 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/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- 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/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- 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/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/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -215,12 +215,12 @@ export function BCFTopicDetail({
|
|
|
215
215
|
<img
|
|
216
216
|
src={vp.snapshot}
|
|
217
217
|
alt="Viewpoint"
|
|
218
|
-
className="w-full
|
|
218
|
+
className="w-full object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
|
219
219
|
onClick={() => onActivateViewpoint(vp)}
|
|
220
220
|
/>
|
|
221
221
|
) : (
|
|
222
222
|
<div
|
|
223
|
-
className="w-full aspect-video bg-muted flex items-center justify-center cursor-pointer hover:bg-muted/80 transition-colors"
|
|
223
|
+
className="w-full aspect-video bg-muted flex items-center justify-center cursor-pointer hover:bg-muted/80 transition-colors min-h-[120px]"
|
|
224
224
|
onClick={() => onActivateViewpoint(vp)}
|
|
225
225
|
>
|
|
226
226
|
<Camera className="h-6 w-6 text-muted-foreground" />
|
|
@@ -294,7 +294,7 @@ export function BCFTopicDetail({
|
|
|
294
294
|
<img
|
|
295
295
|
src={associatedViewpoint.snapshot}
|
|
296
296
|
alt="Associated viewpoint"
|
|
297
|
-
className="w-full h-
|
|
297
|
+
className="w-full max-h-24 object-contain bg-muted"
|
|
298
298
|
/>
|
|
299
299
|
</div>
|
|
300
300
|
)}
|
|
@@ -327,7 +327,7 @@ export function BCFTopicDetail({
|
|
|
327
327
|
<img
|
|
328
328
|
src={selectedViewpoint.snapshot}
|
|
329
329
|
alt="Selected viewpoint"
|
|
330
|
-
className="w-
|
|
330
|
+
className="w-12 h-10 object-contain rounded bg-muted"
|
|
331
331
|
/>
|
|
332
332
|
)}
|
|
333
333
|
<div className="flex-1 min-w-0">
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* ModelSelector — dropdown to pick the LLM model.
|
|
7
|
-
* Free models available to everyone
|
|
7
|
+
* Free models available to everyone via server proxy.
|
|
8
|
+
* BYOK models selectable always — greyed out with lock icon when key is missing,
|
|
9
|
+
* fully styled when key is configured. Selecting a locked model triggers the
|
|
10
|
+
* inline key prompt in ChatPanel.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
import { useCallback } from 'react';
|
|
11
|
-
import {
|
|
13
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
14
|
+
import { Check, Key } from 'lucide-react';
|
|
12
15
|
import {
|
|
13
16
|
Select,
|
|
14
17
|
SelectContent,
|
|
@@ -17,27 +20,43 @@ import {
|
|
|
17
20
|
SelectValue,
|
|
18
21
|
} from '@/components/ui/select';
|
|
19
22
|
import { useViewerStore } from '@/store';
|
|
20
|
-
import { FREE_MODELS,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/** Whether the user has a pro subscription */
|
|
24
|
-
hasPro?: boolean;
|
|
25
|
-
}
|
|
23
|
+
import { FREE_MODELS, getModelById, getByokModelsForSource } from '@/lib/llm/models';
|
|
24
|
+
import type { LLMModel } from '@/lib/llm/types';
|
|
25
|
+
import { hasAnthropicKey, hasOpenaiKey, subscribeApiKeys } from '@/services/api-keys';
|
|
26
26
|
|
|
27
27
|
function formatContextWindow(tokens: number): string {
|
|
28
28
|
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(0)}M`;
|
|
29
29
|
return `${(tokens / 1_000).toFixed(0)}K`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function CostBadge({ cost }: { cost?: LLMModel['cost'] }) {
|
|
33
|
+
if (!cost) return null;
|
|
34
|
+
const color = cost === '$$$' ? 'text-amber-500' : cost === '$$' ? 'text-blue-500' : 'text-emerald-500';
|
|
35
|
+
return <span className={`text-[10px] font-mono ${color}`}>{cost}</span>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ModelSelector() {
|
|
33
39
|
const activeModel = useViewerStore((s) => s.chatActiveModel);
|
|
34
40
|
const setActiveModel = useViewerStore((s) => s.setChatActiveModel);
|
|
35
41
|
|
|
42
|
+
const [hasAnthropic, setHasAnthropic] = useState(hasAnthropicKey);
|
|
43
|
+
const [hasOpenai, setHasOpenai] = useState(hasOpenaiKey);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const refresh = () => {
|
|
47
|
+
setHasAnthropic(hasAnthropicKey());
|
|
48
|
+
setHasOpenai(hasOpenaiKey());
|
|
49
|
+
};
|
|
50
|
+
return subscribeApiKeys(refresh);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
36
53
|
const handleChange = useCallback((value: string) => {
|
|
37
54
|
setActiveModel(value);
|
|
38
55
|
}, [setActiveModel]);
|
|
39
56
|
|
|
40
57
|
const current = getModelById(activeModel);
|
|
58
|
+
const anthropicModels = getByokModelsForSource('anthropic');
|
|
59
|
+
const openaiModels = getByokModelsForSource('openai');
|
|
41
60
|
|
|
42
61
|
return (
|
|
43
62
|
<Select value={activeModel} onValueChange={handleChange}>
|
|
@@ -45,57 +64,74 @@ export function ModelSelector({ hasPro = false }: ModelSelectorProps) {
|
|
|
45
64
|
<SelectValue>
|
|
46
65
|
<span className="truncate flex items-center gap-1">
|
|
47
66
|
{current?.name ?? activeModel}
|
|
48
|
-
{current?.cost
|
|
49
|
-
<span className={`text-[10px] font-mono ${
|
|
50
|
-
current.cost === '$$$' ? 'text-amber-500' : current.cost === '$$' ? 'text-blue-500' : 'text-emerald-500'
|
|
51
|
-
}`}>
|
|
52
|
-
{current.cost}
|
|
53
|
-
</span>
|
|
54
|
-
)}
|
|
67
|
+
<CostBadge cost={current?.cost} />
|
|
55
68
|
</span>
|
|
56
69
|
</SelectValue>
|
|
57
70
|
</SelectTrigger>
|
|
58
71
|
<SelectContent>
|
|
59
72
|
{/* Free tier */}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
{FREE_MODELS.length > 0 && (
|
|
74
|
+
<>
|
|
75
|
+
<div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
76
|
+
Free
|
|
77
|
+
</div>
|
|
78
|
+
{FREE_MODELS.map((m) => (
|
|
79
|
+
<SelectItem key={m.id} value={m.id} className="text-xs">
|
|
80
|
+
<span className="flex items-center gap-1.5">
|
|
81
|
+
<span>{m.name}</span>
|
|
82
|
+
<span className="text-muted-foreground text-[10px]">{m.provider}</span>
|
|
83
|
+
<span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
|
|
84
|
+
</span>
|
|
85
|
+
</SelectItem>
|
|
86
|
+
))}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Anthropic BYOK */}
|
|
91
|
+
{anthropicModels.length > 0 && (
|
|
92
|
+
<>
|
|
93
|
+
<div className="px-2 py-1 mt-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1">
|
|
94
|
+
Anthropic
|
|
95
|
+
{hasAnthropic
|
|
96
|
+
? <Check className="h-2.5 w-2.5 text-emerald-500" />
|
|
97
|
+
: <Key className="h-2.5 w-2.5" />
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
{anthropicModels.map((m) => (
|
|
101
|
+
<SelectItem key={m.id} value={m.id} className={`text-xs ${!hasAnthropic ? 'opacity-50' : ''}`}>
|
|
102
|
+
<span className="flex items-center gap-1.5">
|
|
103
|
+
<span>{m.name}</span>
|
|
104
|
+
<CostBadge cost={m.cost} />
|
|
105
|
+
<span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
|
|
106
|
+
{!hasAnthropic && <Key className="h-3 w-3 text-muted-foreground/50" />}
|
|
107
|
+
</span>
|
|
108
|
+
</SelectItem>
|
|
109
|
+
))}
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
72
112
|
|
|
73
|
-
{/*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}`}>
|
|
91
|
-
{m.cost}
|
|
113
|
+
{/* OpenAI BYOK */}
|
|
114
|
+
{openaiModels.length > 0 && (
|
|
115
|
+
<>
|
|
116
|
+
<div className="px-2 py-1 mt-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-1">
|
|
117
|
+
OpenAI
|
|
118
|
+
{hasOpenai
|
|
119
|
+
? <Check className="h-2.5 w-2.5 text-emerald-500" />
|
|
120
|
+
: <Key className="h-2.5 w-2.5" />
|
|
121
|
+
}
|
|
122
|
+
</div>
|
|
123
|
+
{openaiModels.map((m) => (
|
|
124
|
+
<SelectItem key={m.id} value={m.id} className={`text-xs ${!hasOpenai ? 'opacity-50' : ''}`}>
|
|
125
|
+
<span className="flex items-center gap-1.5">
|
|
126
|
+
<span>{m.name}</span>
|
|
127
|
+
<CostBadge cost={m.cost} />
|
|
128
|
+
<span className="text-muted-foreground/50 text-[10px]">{formatContextWindow(m.contextWindow)}</span>
|
|
129
|
+
{!hasOpenai && <Key className="h-3 w-3 text-muted-foreground/50" />}
|
|
92
130
|
</span>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</SelectItem>
|
|
98
|
-
))}
|
|
131
|
+
</SelectItem>
|
|
132
|
+
))}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
99
135
|
</SelectContent>
|
|
100
136
|
</Select>
|
|
101
137
|
);
|
|
@@ -67,34 +67,27 @@ export function ListPanel({ onClose }: ListPanelProps) {
|
|
|
67
67
|
|
|
68
68
|
const importInputRef = React.useRef<HTMLInputElement>(null);
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// Build the {modelId, provider} pairs in a single pass so the two
|
|
71
|
+
// arrays can never drift out of alignment (skipping a model without
|
|
72
|
+
// an ifcDataStore must not shift every later model's provider index).
|
|
73
|
+
const modelProviderPairs = useMemo(() => {
|
|
74
|
+
const pairs: Array<{ modelId: string; provider: ListDataProvider }> = [];
|
|
73
75
|
if (models.size > 0) {
|
|
74
|
-
for (const [, model] of models) {
|
|
75
|
-
|
|
76
|
+
for (const [modelId, model] of models) {
|
|
77
|
+
// Skip native-metadata models — they don't have a parsed
|
|
78
|
+
// IfcDataStore, so the list provider can't query them.
|
|
79
|
+
if (!model.ifcDataStore) continue;
|
|
80
|
+
pairs.push({ modelId, provider: createListDataProvider(model.ifcDataStore) });
|
|
76
81
|
}
|
|
77
82
|
} else if (ifcDataStore) {
|
|
78
|
-
|
|
83
|
+
pairs.push({ modelId: 'default', provider: createListDataProvider(ifcDataStore) });
|
|
79
84
|
}
|
|
80
|
-
return
|
|
85
|
+
return pairs;
|
|
81
86
|
}, [models, ifcDataStore]);
|
|
82
87
|
|
|
83
|
-
const
|
|
88
|
+
const allProviders = useMemo(() => modelProviderPairs.map((p) => p.provider), [modelProviderPairs]);
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
const modelProviderPairs = useMemo(() => {
|
|
87
|
-
const pairs: Array<{ modelId: string; provider: ListDataProvider }> = [];
|
|
88
|
-
if (models.size > 0) {
|
|
89
|
-
let i = 0;
|
|
90
|
-
for (const [modelId] of models) {
|
|
91
|
-
pairs.push({ modelId, provider: allProviders[i++] });
|
|
92
|
-
}
|
|
93
|
-
} else if (allProviders.length > 0) {
|
|
94
|
-
pairs.push({ modelId: 'default', provider: allProviders[0] });
|
|
95
|
-
}
|
|
96
|
-
return pairs;
|
|
97
|
-
}, [models, allProviders]);
|
|
90
|
+
const hasData = allProviders.length > 0;
|
|
98
91
|
|
|
99
92
|
const handleExecuteList = useCallback((definition: ListDefinition) => {
|
|
100
93
|
if (!hasData) return;
|
|
@@ -16,6 +16,9 @@ import { useViewerStore } from '@/store';
|
|
|
16
16
|
import type { CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
17
17
|
import { EpsgLookupDialog, type EpsgResult } from './EpsgLookupDialog';
|
|
18
18
|
import { LocationMap, type PickedPosition } from './LocationMap';
|
|
19
|
+
import { mergeMapConversion, mergeProjectedCRS } from '@/lib/geo/effective-georef';
|
|
20
|
+
import { useIfc } from '@/hooks/useIfc';
|
|
21
|
+
import { toast } from '@/components/ui/toast';
|
|
19
22
|
|
|
20
23
|
// ── Field-specific assistance data ─────────────────────────────────────
|
|
21
24
|
|
|
@@ -319,9 +322,11 @@ export interface GeoreferencingPanelProps {
|
|
|
319
322
|
coordinateInfo?: CoordinateInfo;
|
|
320
323
|
/** GeometryResult for KMZ export */
|
|
321
324
|
geometryResult?: GeometryResult | null;
|
|
325
|
+
/** IFC project length unit → metres (e.g. 0.001 for mm models). Default 1. */
|
|
326
|
+
lengthUnitScale?: number;
|
|
322
327
|
}
|
|
323
328
|
|
|
324
|
-
export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVersion, coordinateInfo, geometryResult }: GeoreferencingPanelProps) {
|
|
329
|
+
export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVersion, coordinateInfo, geometryResult, lengthUnitScale }: GeoreferencingPanelProps) {
|
|
325
330
|
const georefMutations = useViewerStore(s => s.georefMutations);
|
|
326
331
|
const setGeorefField = useViewerStore(s => s.setGeorefField);
|
|
327
332
|
const setGeorefFields = useViewerStore(s => s.setGeorefFields);
|
|
@@ -330,10 +335,14 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
330
335
|
const setCesiumTerrainClamp = useViewerStore(s => s.setCesiumTerrainClamp);
|
|
331
336
|
const cesiumTerrainHeight = useViewerStore(s => s.cesiumTerrainHeight);
|
|
332
337
|
const cesiumSourceModelId = useViewerStore(s => s.cesiumSourceModelId);
|
|
338
|
+
const models = useViewerStore(s => s.models);
|
|
339
|
+
const loading = useViewerStore(s => s.loading);
|
|
340
|
+
const { addModel, clearAllModels } = useIfc();
|
|
333
341
|
// Only show terrain actions when this panel's model is the one backing the Cesium overlay
|
|
334
342
|
const isActiveCesiumModel = !!modelId && modelId === cesiumSourceModelId;
|
|
335
343
|
const [crsOpen, setCrsOpen] = useState(false);
|
|
336
344
|
const [conversionOpen, setConversionOpen] = useState(false);
|
|
345
|
+
const [showReloadPrompt, setShowReloadPrompt] = useState(false);
|
|
337
346
|
|
|
338
347
|
useViewerStore(s => s.mutationVersion);
|
|
339
348
|
|
|
@@ -341,37 +350,12 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
341
350
|
const supportsStandardGeoreferencing = !schemaVersion?.toUpperCase().includes('2X3');
|
|
342
351
|
|
|
343
352
|
const mergedCRS = useMemo((): ProjectedCRS | undefined => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (!base && !muts) return undefined;
|
|
347
|
-
return {
|
|
348
|
-
id: base?.id ?? 0,
|
|
349
|
-
name: muts?.name ?? base?.name ?? '',
|
|
350
|
-
description: muts?.description ?? base?.description,
|
|
351
|
-
geodeticDatum: muts?.geodeticDatum ?? base?.geodeticDatum,
|
|
352
|
-
verticalDatum: muts?.verticalDatum ?? base?.verticalDatum,
|
|
353
|
-
mapProjection: muts?.mapProjection ?? base?.mapProjection,
|
|
354
|
-
mapZone: muts?.mapZone ?? base?.mapZone,
|
|
355
|
-
mapUnit: muts?.mapUnit ?? base?.mapUnit,
|
|
356
|
-
};
|
|
357
|
-
}, [georef, mutations]);
|
|
353
|
+
return mergeProjectedCRS(georef?.projectedCRS, mutations?.projectedCRS, lengthUnitScale ?? 1);
|
|
354
|
+
}, [georef?.projectedCRS, mutations?.projectedCRS, lengthUnitScale]);
|
|
358
355
|
|
|
359
356
|
const mergedConversion = useMemo((): MapConversion | undefined => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (!base && !muts) return undefined;
|
|
363
|
-
return {
|
|
364
|
-
id: base?.id ?? 0,
|
|
365
|
-
sourceCRS: base?.sourceCRS ?? 0,
|
|
366
|
-
targetCRS: base?.targetCRS ?? 0,
|
|
367
|
-
eastings: muts?.eastings ?? base?.eastings ?? 0,
|
|
368
|
-
northings: muts?.northings ?? base?.northings ?? 0,
|
|
369
|
-
orthogonalHeight: muts?.orthogonalHeight ?? base?.orthogonalHeight ?? 0,
|
|
370
|
-
xAxisAbscissa: muts?.xAxisAbscissa ?? base?.xAxisAbscissa,
|
|
371
|
-
xAxisOrdinate: muts?.xAxisOrdinate ?? base?.xAxisOrdinate,
|
|
372
|
-
scale: muts?.scale ?? base?.scale,
|
|
373
|
-
};
|
|
374
|
-
}, [georef, mutations]);
|
|
357
|
+
return mergeMapConversion(georef?.mapConversion, mutations?.mapConversion);
|
|
358
|
+
}, [georef?.mapConversion, mutations?.mapConversion]);
|
|
375
359
|
|
|
376
360
|
const angleToGridNorth = useMemo(() => {
|
|
377
361
|
return computeAngleToGridNorth(mergedConversion?.xAxisAbscissa, mergedConversion?.xAxisOrdinate);
|
|
@@ -399,22 +383,69 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
399
383
|
return field in entityMuts;
|
|
400
384
|
}, [mutations]);
|
|
401
385
|
|
|
386
|
+
const requestAlignmentReload = useCallback(() => {
|
|
387
|
+
if (models.size > 1) {
|
|
388
|
+
setShowReloadPrompt(true);
|
|
389
|
+
}
|
|
390
|
+
}, [models.size]);
|
|
391
|
+
|
|
392
|
+
const reloadModelsForAlignment = useCallback(async () => {
|
|
393
|
+
const state = useViewerStore.getState();
|
|
394
|
+
const snapshot = Array.from(state.models.values()).sort((a, b) => (a.loadedAt ?? 0) - (b.loadedAt ?? 0));
|
|
395
|
+
const missingSource = snapshot.find(model => !model.sourceFile);
|
|
396
|
+
if (snapshot.length < 2) {
|
|
397
|
+
setShowReloadPrompt(false);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (missingSource) {
|
|
401
|
+
toast.error(`Cannot reload ${missingSource.name}: source file is not available`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
clearAllModels();
|
|
407
|
+
for (const model of snapshot) {
|
|
408
|
+
const sourceFile = model.sourceFile;
|
|
409
|
+
if (!sourceFile) continue;
|
|
410
|
+
const reloadedModelId = await addModel(sourceFile, {
|
|
411
|
+
name: model.name,
|
|
412
|
+
modelId: model.id,
|
|
413
|
+
loadedAt: model.loadedAt,
|
|
414
|
+
visible: model.visible,
|
|
415
|
+
collapsed: model.collapsed,
|
|
416
|
+
});
|
|
417
|
+
if (!reloadedModelId) {
|
|
418
|
+
throw new Error(`Failed to reload ${model.name}`);
|
|
419
|
+
}
|
|
420
|
+
if (model.visible === false) {
|
|
421
|
+
useViewerStore.getState().setModelVisibility(model.id, false);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
setShowReloadPrompt(false);
|
|
425
|
+
toast.success('Reloaded models for edited georeferencing');
|
|
426
|
+
} catch (error) {
|
|
427
|
+
toast.error(error instanceof Error ? error.message : 'Reload failed');
|
|
428
|
+
}
|
|
429
|
+
}, [addModel, clearAllModels]);
|
|
430
|
+
|
|
402
431
|
const handleSave = useCallback((entity: 'projectedCRS' | 'mapConversion', field: string, value: string | number) => {
|
|
403
432
|
if (!modelId || !setGeorefField) return;
|
|
404
433
|
const oldValue = entity === 'projectedCRS'
|
|
405
|
-
?
|
|
406
|
-
:
|
|
434
|
+
? mergedCRS?.[field as keyof ProjectedCRS]
|
|
435
|
+
: mergedConversion?.[field as keyof MapConversion];
|
|
407
436
|
setGeorefField(modelId, entity, field, value, oldValue as string | number | undefined);
|
|
408
|
-
|
|
437
|
+
requestAlignmentReload();
|
|
438
|
+
}, [modelId, setGeorefField, mergedCRS, mergedConversion, requestAlignmentReload]);
|
|
409
439
|
|
|
410
440
|
// Handle angle edit: compute and set both XAxisAbscissa and XAxisOrdinate
|
|
411
441
|
const handleAngleChange = useCallback((abscissa: number, ordinate: number) => {
|
|
412
442
|
if (!modelId || !setGeorefFields) return;
|
|
413
443
|
setGeorefFields(modelId, 'mapConversion', [
|
|
414
|
-
{ field: 'xAxisAbscissa', value: abscissa, oldValue:
|
|
415
|
-
{ field: 'xAxisOrdinate', value: ordinate, oldValue:
|
|
444
|
+
{ field: 'xAxisAbscissa', value: abscissa, oldValue: mergedConversion?.xAxisAbscissa },
|
|
445
|
+
{ field: 'xAxisOrdinate', value: ordinate, oldValue: mergedConversion?.xAxisOrdinate },
|
|
416
446
|
]);
|
|
417
|
-
|
|
447
|
+
requestAlignmentReload();
|
|
448
|
+
}, [modelId, setGeorefFields, mergedConversion, requestAlignmentReload]);
|
|
418
449
|
|
|
419
450
|
// Handle position picked from the map (reverse-projected easting/northing + optional terrain height)
|
|
420
451
|
const handleApplyPosition = useCallback((position: PickedPosition) => {
|
|
@@ -432,35 +463,37 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
432
463
|
}
|
|
433
464
|
setGeorefFields(modelId, 'mapConversion', fields);
|
|
434
465
|
setConversionOpen(true);
|
|
435
|
-
|
|
466
|
+
requestAlignmentReload();
|
|
467
|
+
}, [modelId, setGeorefFields, mergedConversion, requestAlignmentReload]);
|
|
436
468
|
|
|
437
469
|
const initializeMapConversionDefaults = useCallback(() => {
|
|
438
470
|
if (!modelId || !setGeorefFields) return;
|
|
439
471
|
setGeorefFields(modelId, 'mapConversion', [
|
|
440
|
-
{ field: 'eastings', value:
|
|
441
|
-
{ field: 'northings', value:
|
|
442
|
-
{ field: 'orthogonalHeight', value:
|
|
443
|
-
{ field: 'xAxisAbscissa', value:
|
|
444
|
-
{ field: 'xAxisOrdinate', value:
|
|
445
|
-
{ field: 'scale', value:
|
|
472
|
+
{ field: 'eastings', value: mergedConversion?.eastings ?? 0, oldValue: mergedConversion?.eastings },
|
|
473
|
+
{ field: 'northings', value: mergedConversion?.northings ?? 0, oldValue: mergedConversion?.northings },
|
|
474
|
+
{ field: 'orthogonalHeight', value: mergedConversion?.orthogonalHeight ?? 0, oldValue: mergedConversion?.orthogonalHeight },
|
|
475
|
+
{ field: 'xAxisAbscissa', value: mergedConversion?.xAxisAbscissa ?? 1, oldValue: mergedConversion?.xAxisAbscissa },
|
|
476
|
+
{ field: 'xAxisOrdinate', value: mergedConversion?.xAxisOrdinate ?? 0, oldValue: mergedConversion?.xAxisOrdinate },
|
|
477
|
+
{ field: 'scale', value: mergedConversion?.scale ?? 1, oldValue: mergedConversion?.scale },
|
|
446
478
|
]);
|
|
447
479
|
setConversionOpen(true);
|
|
448
|
-
|
|
480
|
+
requestAlignmentReload();
|
|
481
|
+
}, [modelId, setGeorefFields, mergedConversion, requestAlignmentReload]);
|
|
449
482
|
|
|
450
483
|
const handleEpsgSelect = useCallback((result: EpsgResult) => {
|
|
451
484
|
if (!modelId || !setGeorefFields) return;
|
|
452
485
|
const epsgName = `EPSG:${result.code}`;
|
|
453
486
|
const fieldUpdates: Array<{ field: string; value: string | number; oldValue?: string | number }> = [
|
|
454
|
-
{ field: 'name', value: epsgName, oldValue:
|
|
487
|
+
{ field: 'name', value: epsgName, oldValue: mergedCRS?.name },
|
|
455
488
|
];
|
|
456
489
|
if (result.name) {
|
|
457
|
-
fieldUpdates.push({ field: 'description', value: result.name, oldValue:
|
|
490
|
+
fieldUpdates.push({ field: 'description', value: result.name, oldValue: mergedCRS?.description });
|
|
458
491
|
}
|
|
459
492
|
if (result.datum) {
|
|
460
|
-
fieldUpdates.push({ field: 'geodeticDatum', value: result.datum, oldValue:
|
|
493
|
+
fieldUpdates.push({ field: 'geodeticDatum', value: result.datum, oldValue: mergedCRS?.geodeticDatum });
|
|
461
494
|
}
|
|
462
495
|
if (result.projection) {
|
|
463
|
-
fieldUpdates.push({ field: 'mapProjection', value: result.projection, oldValue:
|
|
496
|
+
fieldUpdates.push({ field: 'mapProjection', value: result.projection, oldValue: mergedCRS?.mapProjection });
|
|
464
497
|
}
|
|
465
498
|
if (result.unit) {
|
|
466
499
|
const unitUpper = result.unit.toUpperCase();
|
|
@@ -471,14 +504,15 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
471
504
|
: unitUpper.includes('FOOT') || unitUpper.includes('FEET')
|
|
472
505
|
? 'FOOT'
|
|
473
506
|
: result.unit;
|
|
474
|
-
fieldUpdates.push({ field: 'mapUnit', value: mapUnit, oldValue:
|
|
507
|
+
fieldUpdates.push({ field: 'mapUnit', value: mapUnit, oldValue: mergedCRS?.mapUnit });
|
|
475
508
|
}
|
|
476
509
|
setGeorefFields(modelId, 'projectedCRS', fieldUpdates);
|
|
477
|
-
if (!
|
|
510
|
+
if (!mergedConversion && !mutations?.mapConversion) {
|
|
478
511
|
initializeMapConversionDefaults();
|
|
479
512
|
}
|
|
480
513
|
setCrsOpen(true);
|
|
481
|
-
|
|
514
|
+
requestAlignmentReload();
|
|
515
|
+
}, [modelId, setGeorefFields, mergedCRS, mergedConversion, mutations, initializeMapConversionDefaults, requestAlignmentReload]);
|
|
482
516
|
|
|
483
517
|
const hasData = mergedCRS || mergedConversion;
|
|
484
518
|
const editable = enableEditing && !!modelId && supportsStandardGeoreferencing;
|
|
@@ -513,6 +547,33 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
513
547
|
|
|
514
548
|
return (
|
|
515
549
|
<div>
|
|
550
|
+
{showReloadPrompt && (
|
|
551
|
+
<div className="mx-2 my-2 border border-teal-300 dark:border-teal-700 bg-teal-50 dark:bg-teal-950/40 px-2.5 py-2">
|
|
552
|
+
<div className="flex items-start gap-2">
|
|
553
|
+
<MapPin className="h-3.5 w-3.5 text-teal-600 dark:text-teal-400 shrink-0 mt-0.5" />
|
|
554
|
+
<div className="min-w-0 flex-1">
|
|
555
|
+
<p className="text-[10px] text-zinc-700 dark:text-zinc-300">
|
|
556
|
+
Georeference saved. Reload loaded models to recompute 3D alignment?
|
|
557
|
+
</p>
|
|
558
|
+
<div className="mt-1.5 flex items-center gap-2">
|
|
559
|
+
<button
|
|
560
|
+
onClick={reloadModelsForAlignment}
|
|
561
|
+
disabled={loading}
|
|
562
|
+
className="px-2 py-0.5 text-[10px] font-medium text-white bg-teal-600 hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
563
|
+
>
|
|
564
|
+
Reload models
|
|
565
|
+
</button>
|
|
566
|
+
<button
|
|
567
|
+
onClick={() => setShowReloadPrompt(false)}
|
|
568
|
+
className="px-2 py-0.5 text-[10px] text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200/60 dark:hover:bg-zinc-800"
|
|
569
|
+
>
|
|
570
|
+
Later
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
516
577
|
{/* CRS summary — always visible */}
|
|
517
578
|
<div className="px-2 py-1.5 flex items-center gap-2">
|
|
518
579
|
<Globe className="h-3 w-3 text-teal-500 shrink-0" />
|
|
@@ -663,6 +724,7 @@ export function GeoreferencingPanel({ georef, modelId, enableEditing, schemaVers
|
|
|
663
724
|
projectedCRS={mergedCRS}
|
|
664
725
|
coordinateInfo={coordinateInfo}
|
|
665
726
|
geometryResult={geometryResult}
|
|
727
|
+
lengthUnitScale={lengthUnitScale}
|
|
666
728
|
editable={editable}
|
|
667
729
|
onApplyPosition={editable ? handleApplyPosition : undefined}
|
|
668
730
|
/>
|
|
@@ -49,6 +49,8 @@ export interface LocationMapProps {
|
|
|
49
49
|
coordinateInfo?: CoordinateInfo;
|
|
50
50
|
/** Geometry result for KMZ export (optional — KMZ button hidden if not provided) */
|
|
51
51
|
geometryResult?: GeometryResult | null;
|
|
52
|
+
/** IFC project length unit → metres (e.g. 0.001 for mm models). Default 1 (metres). */
|
|
53
|
+
lengthUnitScale?: number;
|
|
52
54
|
/** Whether the map is in edit mode (allows repositioning) */
|
|
53
55
|
editable?: boolean;
|
|
54
56
|
/** Called when the user applies a new position from the map */
|
|
@@ -138,7 +140,7 @@ function removeFootprintFromMap(map: InstanceType<typeof import('maplibre-gl').M
|
|
|
138
140
|
|
|
139
141
|
export function LocationMap({
|
|
140
142
|
mapConversion, projectedCRS, coordinateInfo, geometryResult,
|
|
141
|
-
editable, onApplyPosition,
|
|
143
|
+
lengthUnitScale = 1, editable, onApplyPosition,
|
|
142
144
|
}: LocationMapProps) {
|
|
143
145
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
144
146
|
const mapRef = useRef<InstanceType<typeof import('maplibre-gl').Map> | null>(null);
|
|
@@ -195,14 +197,14 @@ export function LocationMap({
|
|
|
195
197
|
|
|
196
198
|
let cancelled = false;
|
|
197
199
|
|
|
198
|
-
computeFootprintGeoJSON(mapConversion, projectedCRS, coordinateInfo).then(fp => {
|
|
200
|
+
computeFootprintGeoJSON(mapConversion, projectedCRS, coordinateInfo, lengthUnitScale).then(fp => {
|
|
199
201
|
if (cancelled) return;
|
|
200
202
|
setFootprint(fp);
|
|
201
203
|
footprintRef.current = fp;
|
|
202
204
|
});
|
|
203
205
|
|
|
204
206
|
return () => { cancelled = true; };
|
|
205
|
-
}, [mapConversion, projectedCRS, coordinateInfo]);
|
|
207
|
+
}, [mapConversion, projectedCRS, coordinateInfo, lengthUnitScale]);
|
|
206
208
|
|
|
207
209
|
// Geocode search
|
|
208
210
|
useEffect(() => {
|
|
@@ -234,7 +236,7 @@ export function LocationMap({
|
|
|
234
236
|
setMapState('loading');
|
|
235
237
|
setError(null);
|
|
236
238
|
|
|
237
|
-
reprojectToLatLon(mapConversion, projectedCRS, coordinateInfo).then(result => {
|
|
239
|
+
reprojectToLatLon(mapConversion, projectedCRS, coordinateInfo, lengthUnitScale).then(result => {
|
|
238
240
|
if (cancelled) return;
|
|
239
241
|
if (result) {
|
|
240
242
|
setLatLon(result);
|
|
@@ -247,7 +249,7 @@ export function LocationMap({
|
|
|
247
249
|
});
|
|
248
250
|
|
|
249
251
|
return () => { cancelled = true; };
|
|
250
|
-
}, [mapConversion, projectedCRS, coordinateInfo]);
|
|
252
|
+
}, [mapConversion, projectedCRS, coordinateInfo, lengthUnitScale]);
|
|
251
253
|
|
|
252
254
|
// When a picked position changes, reverse-project and query elevation
|
|
253
255
|
useEffect(() => {
|
|
@@ -262,7 +264,7 @@ export function LocationMap({
|
|
|
262
264
|
|
|
263
265
|
// Reverse-project to get IfcMapConversion eastings/northings
|
|
264
266
|
// Accounts for model local geometry offset, rotation, and scale
|
|
265
|
-
reprojectFromLatLon(pickedLatLon, projectedCRS, mapConversion, coordinateInfo).then(coords => {
|
|
267
|
+
reprojectFromLatLon(pickedLatLon, projectedCRS, mapConversion, coordinateInfo, lengthUnitScale).then(coords => {
|
|
266
268
|
if (!cancelled) setProjectedCoords(coords);
|
|
267
269
|
});
|
|
268
270
|
|
|
@@ -276,7 +278,7 @@ export function LocationMap({
|
|
|
276
278
|
});
|
|
277
279
|
|
|
278
280
|
return () => { cancelled = true; };
|
|
279
|
-
}, [pickedLatLon, projectedCRS, mapConversion, coordinateInfo]);
|
|
281
|
+
}, [pickedLatLon, projectedCRS, mapConversion, coordinateInfo, lengthUnitScale]);
|
|
280
282
|
|
|
281
283
|
// Place or move the picked marker on the map
|
|
282
284
|
const updatePickedMarker = useCallback((pos: LatLon, maplibregl: typeof import('maplibre-gl')) => {
|
|
@@ -212,7 +212,7 @@ export function ModelMetadataPanel({ model }: { model: FederatedModel }) {
|
|
|
212
212
|
</div>
|
|
213
213
|
|
|
214
214
|
{/* Georeferencing */}
|
|
215
|
-
<GeoreferencingPanel georef={georef} modelId={model.id} enableEditing schemaVersion={model.schemaVersion} coordinateInfo={model.geometryResult?.coordinateInfo} geometryResult={model.geometryResult} />
|
|
215
|
+
<GeoreferencingPanel georef={georef} modelId={model.id} enableEditing schemaVersion={model.schemaVersion} coordinateInfo={model.geometryResult?.coordinateInfo} geometryResult={model.geometryResult} lengthUnitScale={unitInfo?.scale} />
|
|
216
216
|
|
|
217
217
|
{/* IfcProject Data */}
|
|
218
218
|
{projectData && (
|