@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
|
@@ -18,8 +18,11 @@ import {
|
|
|
18
18
|
Maximize2,
|
|
19
19
|
Building2,
|
|
20
20
|
Save,
|
|
21
|
+
Trash2,
|
|
22
|
+
CopyPlus,
|
|
21
23
|
} from 'lucide-react';
|
|
22
24
|
import { useViewerStore, resolveEntityRef } from '@/store';
|
|
25
|
+
import type { DuplicateDirection } from '@/store/slices/mutationSlice';
|
|
23
26
|
import { resetVisibilityForHomeFromStore } from '@/store/homeView';
|
|
24
27
|
import {
|
|
25
28
|
executeBasketSet,
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
executeBasketSaveView,
|
|
29
32
|
} from '@/store/basket/basketCommands';
|
|
30
33
|
import { useIfc } from '@/hooks/useIfc';
|
|
34
|
+
import { toast } from '@/components/ui/toast';
|
|
31
35
|
|
|
32
36
|
export function EntityContextMenu() {
|
|
33
37
|
const contextMenu = useViewerStore((s) => s.contextMenu);
|
|
@@ -36,6 +40,10 @@ export function EntityContextMenu() {
|
|
|
36
40
|
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
37
41
|
const setSelectedEntityIds = useViewerStore((s) => s.setSelectedEntityIds);
|
|
38
42
|
const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
|
|
43
|
+
// Store-level mutations
|
|
44
|
+
const removeEntity = useViewerStore((s) => s.removeEntity);
|
|
45
|
+
const duplicateEntity = useViewerStore((s) => s.duplicateEntity);
|
|
46
|
+
const getMutationView = useViewerStore((s) => s.getMutationView);
|
|
39
47
|
// Basket actions
|
|
40
48
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
41
49
|
const { ifcDataStore, models } = useIfc();
|
|
@@ -206,6 +214,63 @@ export function EntityContextMenu() {
|
|
|
206
214
|
closeContextMenu();
|
|
207
215
|
}, [resolvedExpressId, activeDataStore, closeContextMenu]);
|
|
208
216
|
|
|
217
|
+
// Right-clicked entity's type — used in the toast message.
|
|
218
|
+
const contextEntityType = useMemo(() => {
|
|
219
|
+
if (!resolvedExpressId || !activeDataStore) return '';
|
|
220
|
+
return activeDataStore.entities.getTypeName(resolvedExpressId) || '';
|
|
221
|
+
}, [resolvedExpressId, activeDataStore]);
|
|
222
|
+
|
|
223
|
+
// Mutation view is required to drive bim.store.* — native-metadata-only
|
|
224
|
+
// models don't have one, so the Delete option stays hidden there.
|
|
225
|
+
const canEdit = useMemo(() => {
|
|
226
|
+
if (!contextEntityRef) return false;
|
|
227
|
+
return getMutationView(contextEntityRef.modelId) !== null;
|
|
228
|
+
}, [contextEntityRef, getMutationView]);
|
|
229
|
+
|
|
230
|
+
const handleDuplicate = useCallback(
|
|
231
|
+
(direction: DuplicateDirection = '+X') => {
|
|
232
|
+
if (!contextEntityRef || !canEdit) {
|
|
233
|
+
closeContextMenu();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const result = duplicateEntity(contextEntityRef.modelId, contextEntityRef.expressId, direction);
|
|
237
|
+
if ('error' in result) {
|
|
238
|
+
toast.error(`Couldn't duplicate: ${result.error}`);
|
|
239
|
+
} else {
|
|
240
|
+
// Move selection onto the new entity so the property panel
|
|
241
|
+
// refreshes and the user can keep iterating (Cmd+D again
|
|
242
|
+
// duplicates the duplicate, like a stamp tool).
|
|
243
|
+
setSelectedEntityId(result.globalId);
|
|
244
|
+
toast.success(`Duplicated as #${result.expressId} (${direction}) — undo to remove`);
|
|
245
|
+
}
|
|
246
|
+
closeContextMenu();
|
|
247
|
+
},
|
|
248
|
+
[contextEntityRef, canEdit, duplicateEntity, setSelectedEntityId, closeContextMenu],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const handleDeleteEntity = useCallback(() => {
|
|
252
|
+
if (!contextEntityRef || !canEdit || !contextMenu.entityId) {
|
|
253
|
+
closeContextMenu();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const ok = removeEntity(contextEntityRef.modelId, contextEntityRef.expressId);
|
|
257
|
+
if (ok) {
|
|
258
|
+
// Tombstoning only affects export — the rendered mesh is still
|
|
259
|
+
// in the GPU buffers. Hide it via the existing visibility system
|
|
260
|
+
// so the entity disappears from the scene and stops being
|
|
261
|
+
// pickable. `Show all` from the empty-space menu restores it
|
|
262
|
+
// (along with re-running undo to bring back the overlay).
|
|
263
|
+
hideEntity(contextMenu.entityId);
|
|
264
|
+
// Drop the selection so the right panel doesn't cling to a
|
|
265
|
+
// tombstoned id.
|
|
266
|
+
setSelectedEntityId(null);
|
|
267
|
+
toast.success(`${contextEntityType || 'Entity'} #${contextEntityRef.expressId} deleted — undo to restore`);
|
|
268
|
+
} else {
|
|
269
|
+
toast.error('Delete failed — entity not found in store overlay');
|
|
270
|
+
}
|
|
271
|
+
closeContextMenu();
|
|
272
|
+
}, [contextEntityRef, canEdit, contextEntityType, contextMenu.entityId, removeEntity, hideEntity, setSelectedEntityId, closeContextMenu]);
|
|
273
|
+
|
|
209
274
|
if (!contextMenu.isOpen) {
|
|
210
275
|
return null;
|
|
211
276
|
}
|
|
@@ -257,6 +322,22 @@ export function EntityContextMenu() {
|
|
|
257
322
|
<div className="h-px bg-border my-1" />
|
|
258
323
|
|
|
259
324
|
<MenuItem icon={Copy} label="Copy GlobalId" onClick={handleCopyId} />
|
|
325
|
+
|
|
326
|
+
{/* Store-level mutations (bim.store.*). Only surfaced when there's
|
|
327
|
+
a live mutation view on the model — otherwise these would
|
|
328
|
+
silently no-op and confuse users. */}
|
|
329
|
+
{canEdit && (
|
|
330
|
+
<>
|
|
331
|
+
<div className="h-px bg-border my-1" />
|
|
332
|
+
<DuplicateRow onDuplicate={handleDuplicate} />
|
|
333
|
+
<MenuItem
|
|
334
|
+
icon={Trash2}
|
|
335
|
+
label="Delete entity"
|
|
336
|
+
tone="destructive"
|
|
337
|
+
onClick={handleDeleteEntity}
|
|
338
|
+
/>
|
|
339
|
+
</>
|
|
340
|
+
)}
|
|
260
341
|
</>
|
|
261
342
|
)}
|
|
262
343
|
|
|
@@ -269,22 +350,105 @@ export function EntityContextMenu() {
|
|
|
269
350
|
);
|
|
270
351
|
}
|
|
271
352
|
|
|
353
|
+
type MenuItemTone = 'default' | 'destructive';
|
|
354
|
+
|
|
272
355
|
interface MenuItemProps {
|
|
273
356
|
icon: React.ComponentType<{ className?: string }>;
|
|
274
357
|
label: string;
|
|
275
358
|
onClick: () => void;
|
|
276
359
|
disabled?: boolean;
|
|
360
|
+
/** Right-aligned keyboard hint (e.g. `'⌘D'`). */
|
|
361
|
+
shortcut?: string;
|
|
362
|
+
/**
|
|
363
|
+
* Visual tone:
|
|
364
|
+
* - `default` muted icon, neutral hover
|
|
365
|
+
* - `destructive` red-toned icon and red-tinted hover (Delete entity)
|
|
366
|
+
*/
|
|
367
|
+
tone?: MenuItemTone;
|
|
277
368
|
}
|
|
278
369
|
|
|
279
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Inline directional duplicate row — primary label on the left
|
|
372
|
+
* (clickable, fires the default +X duplicate), six axis chips on
|
|
373
|
+
* the right for explicit direction control. Mirrors the column
|
|
374
|
+
* placement axes the user already sees on the Raw STEP tab.
|
|
375
|
+
*
|
|
376
|
+
* Why six chips and not a sub-menu: a flyout for six options is
|
|
377
|
+
* wasted real estate, and the chip arrows let the user "see and
|
|
378
|
+
* pick" in one motion.
|
|
379
|
+
*/
|
|
380
|
+
function DuplicateRow({ onDuplicate }: { onDuplicate: (dir: DuplicateDirection) => void }) {
|
|
381
|
+
return (
|
|
382
|
+
<div className="px-3 py-1.5 flex items-center gap-2 hover:bg-muted/40">
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
onClick={() => onDuplicate('+X')}
|
|
386
|
+
className="flex items-center gap-2 text-sm text-left flex-1 min-w-0 hover:text-foreground"
|
|
387
|
+
title="Duplicate one bbox-width along +X (default)"
|
|
388
|
+
>
|
|
389
|
+
<CopyPlus className="h-4 w-4 text-muted-foreground" />
|
|
390
|
+
<span>Duplicate</span>
|
|
391
|
+
<span className="ml-auto text-[10px] font-mono text-muted-foreground/70">⌘D</span>
|
|
392
|
+
</button>
|
|
393
|
+
<div className="flex items-center gap-0.5 shrink-0 border-l border-border/60 pl-2">
|
|
394
|
+
<DirectionChip dir="+X" label="→" tooltip="Duplicate +X (east)" onClick={() => onDuplicate('+X')} />
|
|
395
|
+
<DirectionChip dir="-X" label="←" tooltip="Duplicate −X (west)" onClick={() => onDuplicate('-X')} />
|
|
396
|
+
<DirectionChip dir="+Y" label="↗" tooltip="Duplicate +Y (north)" onClick={() => onDuplicate('+Y')} />
|
|
397
|
+
<DirectionChip dir="-Y" label="↙" tooltip="Duplicate −Y (south)" onClick={() => onDuplicate('-Y')} />
|
|
398
|
+
<DirectionChip dir="+Z" label="↑" tooltip="Duplicate +Z (up)" onClick={() => onDuplicate('+Z')} />
|
|
399
|
+
<DirectionChip dir="-Z" label="↓" tooltip="Duplicate −Z (down)" onClick={() => onDuplicate('-Z')} />
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function DirectionChip({
|
|
406
|
+
dir,
|
|
407
|
+
label,
|
|
408
|
+
tooltip,
|
|
409
|
+
onClick,
|
|
410
|
+
}: {
|
|
411
|
+
dir: DuplicateDirection;
|
|
412
|
+
label: string;
|
|
413
|
+
tooltip: string;
|
|
414
|
+
onClick: () => void;
|
|
415
|
+
}) {
|
|
416
|
+
return (
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
onClick={onClick}
|
|
420
|
+
title={tooltip}
|
|
421
|
+
aria-label={tooltip}
|
|
422
|
+
className="h-5 w-5 flex items-center justify-center rounded text-[11px] font-mono leading-none text-muted-foreground hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-foreground transition-colors"
|
|
423
|
+
data-direction={dir}
|
|
424
|
+
>
|
|
425
|
+
{label}
|
|
426
|
+
</button>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function MenuItem({ icon: Icon, label, onClick, disabled, shortcut, tone = 'default' }: MenuItemProps) {
|
|
431
|
+
const iconClass =
|
|
432
|
+
tone === 'destructive'
|
|
433
|
+
? 'h-4 w-4 text-red-500 dark:text-red-400'
|
|
434
|
+
: 'h-4 w-4 text-muted-foreground';
|
|
435
|
+
const hoverClass =
|
|
436
|
+
tone === 'destructive'
|
|
437
|
+
? 'hover:bg-red-50 dark:hover:bg-red-950/40 hover:text-red-700 dark:hover:text-red-300'
|
|
438
|
+
: 'hover:bg-muted';
|
|
280
439
|
return (
|
|
281
440
|
<button
|
|
282
|
-
className=
|
|
441
|
+
className={`w-full px-3 py-1.5 text-sm text-left flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed ${hoverClass}`}
|
|
283
442
|
onClick={onClick}
|
|
284
443
|
disabled={disabled}
|
|
285
444
|
>
|
|
286
|
-
<Icon className=
|
|
287
|
-
<span>{label}</span>
|
|
445
|
+
<Icon className={iconClass} />
|
|
446
|
+
<span className="flex-1 min-w-0">{label}</span>
|
|
447
|
+
{shortcut && (
|
|
448
|
+
<span className="text-[10px] font-mono text-muted-foreground/70 shrink-0">
|
|
449
|
+
{shortcut}
|
|
450
|
+
</span>
|
|
451
|
+
)}
|
|
288
452
|
</button>
|
|
289
453
|
);
|
|
290
454
|
}
|
|
@@ -12,13 +12,14 @@ import { Download, Loader2, Check, AlertCircle } from 'lucide-react';
|
|
|
12
12
|
import { Button } from '@/components/ui/button';
|
|
13
13
|
import { Badge } from '@/components/ui/badge';
|
|
14
14
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
15
|
-
import { useViewerStore } from '@/store';
|
|
15
|
+
import { useViewerStore, countGeneratedTasks } from '@/store';
|
|
16
16
|
import { configureMutationView } from '@/utils/configureMutationView';
|
|
17
17
|
import { StepExporter } from '@ifc-lite/export';
|
|
18
18
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
19
19
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
20
20
|
import { toast } from '@/components/ui/toast';
|
|
21
21
|
import { ensureModelExportReady } from '@/services/desktop-export';
|
|
22
|
+
import { spliceScheduleIntoExport } from '@/sdk/adapters/export-schedule-splice';
|
|
22
23
|
|
|
23
24
|
interface ExportChangesButtonProps {
|
|
24
25
|
/** Optional custom class name */
|
|
@@ -71,16 +72,25 @@ export function ExportChangesButton({ className }: ExportChangesButtonProps) {
|
|
|
71
72
|
return null;
|
|
72
73
|
}, [models, legacyIfcDataStore, legacyGeometryResult]);
|
|
73
74
|
|
|
74
|
-
// Count mutations (includes georef mutations)
|
|
75
|
+
// Count mutations (includes georef mutations + pending generated schedule tasks)
|
|
75
76
|
const mutationCount = useMemo(() => {
|
|
76
77
|
if (!modelInfo) return 0;
|
|
77
78
|
const mutationView = getMutationView(modelInfo.id);
|
|
78
79
|
let count = mutationView?.getMutations().length || 0;
|
|
79
|
-
const
|
|
80
|
+
const state = useViewerStore.getState();
|
|
81
|
+
const gm = state.georefMutations?.get(modelInfo.id);
|
|
80
82
|
if (gm) {
|
|
81
83
|
if (gm.projectedCRS) count += Object.keys(gm.projectedCRS).length;
|
|
82
84
|
if (gm.mapConversion) count += Object.keys(gm.mapConversion).length;
|
|
83
85
|
}
|
|
86
|
+
// Generated schedule tasks are first-class pending edits — they get
|
|
87
|
+
// spliced into the STEP on export (see injectScheduleIntoStep), so
|
|
88
|
+
// they belong in the same badge that tells users "you have unsaved
|
|
89
|
+
// work." Attribution: only count when this is the schedule's source
|
|
90
|
+
// model, so the badge doesn't inflate on every federated model.
|
|
91
|
+
if (state.scheduleSourceModelId === modelInfo.id) {
|
|
92
|
+
count += countGeneratedTasks(state.scheduleData);
|
|
93
|
+
}
|
|
84
94
|
return count;
|
|
85
95
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
96
|
}, [modelInfo, getMutationView, mutationVersion]);
|
|
@@ -137,7 +147,8 @@ export function ExportChangesButton({ className }: ExportChangesButtonProps) {
|
|
|
137
147
|
: 'IFC4';
|
|
138
148
|
|
|
139
149
|
const exporter = new StepExporter(exportDataStore, mutationView || undefined);
|
|
140
|
-
const
|
|
150
|
+
const state = useViewerStore.getState();
|
|
151
|
+
const georefMutations = state.georefMutations?.get(modelInfo.id) ?? undefined;
|
|
141
152
|
const result = exporter.export({
|
|
142
153
|
schema: schema as 'IFC2X3' | 'IFC4' | 'IFC4X3',
|
|
143
154
|
includeGeometry: true,
|
|
@@ -148,8 +159,17 @@ export function ExportChangesButton({ className }: ExportChangesButtonProps) {
|
|
|
148
159
|
application: 'ifc-lite',
|
|
149
160
|
});
|
|
150
161
|
|
|
162
|
+
// Splice any pending schedule into the STEP via the shared
|
|
163
|
+
// helper. Same contract every export surface uses so bugs can't
|
|
164
|
+
// differ between the quick button, the dialog, and the SDK.
|
|
165
|
+
const spliced = spliceScheduleIntoExport(result, modelInfo.id, exportDataStore, {
|
|
166
|
+
scheduleData: state.scheduleData ?? null,
|
|
167
|
+
scheduleIsEdited: state.scheduleIsEdited === true,
|
|
168
|
+
scheduleSourceModelId: state.scheduleSourceModelId ?? null,
|
|
169
|
+
});
|
|
170
|
+
|
|
151
171
|
// Download the file
|
|
152
|
-
const blob = new Blob([toBlobPart(
|
|
172
|
+
const blob = new Blob([toBlobPart(spliced.content)], { type: 'text/plain' });
|
|
153
173
|
const url = URL.createObjectURL(blob);
|
|
154
174
|
const a = document.createElement('a');
|
|
155
175
|
a.href = url;
|
|
@@ -56,6 +56,7 @@ import { ensureModelExportReady } from '@/services/desktop-export';
|
|
|
56
56
|
import { StepExporter, MergedExporter, Ifc5Exporter, IFC5_KNOWN_PROP_NAMES, type MergeModelInput, type ExportProgress, type StepExportProgress } from '@ifc-lite/export';
|
|
57
57
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
58
58
|
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
59
|
+
import { spliceScheduleIntoExport } from '@/sdk/adapters/export-schedule-splice';
|
|
59
60
|
|
|
60
61
|
type ExportScope = 'single' | 'merged';
|
|
61
62
|
type SchemaVersion = 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5';
|
|
@@ -387,6 +388,12 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
if (!selectedModel) return;
|
|
391
|
+
// IFC5 export needs a parsed data store + geometry. Native-metadata
|
|
392
|
+
// models don't carry these, so bail with a descriptive error rather
|
|
393
|
+
// than passing nulls through.
|
|
394
|
+
if (!selectedModel.ifcDataStore) {
|
|
395
|
+
throw new Error('Selected model has no parsed IFC data store available for export');
|
|
396
|
+
}
|
|
390
397
|
const mutationView = getMutationView(selectedModelId);
|
|
391
398
|
const baseName = selectedModel.name.replace(/\.[^.]+$/, '');
|
|
392
399
|
|
|
@@ -515,7 +522,18 @@ export function ExportDialog({ trigger }: ExportDialogProps) {
|
|
|
515
522
|
|
|
516
523
|
setExportProgress(null);
|
|
517
524
|
|
|
518
|
-
|
|
525
|
+
// Splice pending schedule tasks into the STEP via the shared
|
|
526
|
+
// helper. Same contract every export surface uses so bugs
|
|
527
|
+
// can't differ between the dialog, the quick button, and the
|
|
528
|
+
// SDK adapter.
|
|
529
|
+
const state = useViewerStore.getState();
|
|
530
|
+
const spliced = spliceScheduleIntoExport(result, selectedModelId, selectedModel.ifcDataStore as IfcDataStore, {
|
|
531
|
+
scheduleData: state.scheduleData ?? null,
|
|
532
|
+
scheduleIsEdited: state.scheduleIsEdited === true,
|
|
533
|
+
scheduleSourceModelId: state.scheduleSourceModelId ?? null,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const blob = new Blob([toBlobPart(spliced.content)], { type: 'text/plain' });
|
|
519
537
|
const url = URL.createObjectURL(blob);
|
|
520
538
|
const a = document.createElement('a');
|
|
521
539
|
a.href = url;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PersonStanding,
|
|
11
11
|
Ruler,
|
|
12
12
|
Scissors,
|
|
13
|
+
MapPin,
|
|
13
14
|
Eye,
|
|
14
15
|
EyeOff,
|
|
15
16
|
Equal,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
Layout,
|
|
38
39
|
LayoutTemplate,
|
|
39
40
|
FileCode2,
|
|
41
|
+
CalendarClock,
|
|
40
42
|
Globe2,
|
|
41
43
|
Settings,
|
|
42
44
|
} from 'lucide-react';
|
|
@@ -66,6 +68,7 @@ import { ExportDialog } from './ExportDialog';
|
|
|
66
68
|
import { BulkPropertyEditor } from './BulkPropertyEditor';
|
|
67
69
|
import { DataConnector } from './DataConnector';
|
|
68
70
|
import { ExportChangesButton } from './ExportChangesButton';
|
|
71
|
+
import { SearchInline } from './SearchInline';
|
|
69
72
|
// CesiumSettingsDialog removed — settings now shown as overlay on Cesium viewer
|
|
70
73
|
import { useFloorplanView } from '@/hooks/useFloorplanView';
|
|
71
74
|
import { buildDesktopUpgradeUrl, hasDesktopFeatureAccess, type DesktopFeature } from '@/lib/desktop-product';
|
|
@@ -84,7 +87,7 @@ import {
|
|
|
84
87
|
subscribeAnalysisExtensions,
|
|
85
88
|
} from '@/services/analysis-extensions';
|
|
86
89
|
|
|
87
|
-
type Tool = 'select' | 'walk' | 'measure' | 'section';
|
|
90
|
+
type Tool = 'select' | 'walk' | 'measure' | 'section' | 'annotate';
|
|
88
91
|
type WorkspacePanel = 'script' | 'list' | 'bcf' | 'ids' | 'lens' | string;
|
|
89
92
|
|
|
90
93
|
function isNativeFileHandle(file: File | NativeFileHandle): file is NativeFileHandle {
|
|
@@ -100,21 +103,39 @@ interface ToolButtonProps {
|
|
|
100
103
|
shortcut?: string;
|
|
101
104
|
activeTool: string;
|
|
102
105
|
onToolChange: (tool: Tool) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Tailwind classes applied when this tool is active. Defaults to the
|
|
108
|
+
* shared `bg-primary text-primary-foreground` shape; pass a per-tool
|
|
109
|
+
* accent (e.g. amber for Annotate) to set tools apart visually
|
|
110
|
+
* without breaking the toolbar's tool-button rhythm.
|
|
111
|
+
*/
|
|
112
|
+
activeAccentClass?: string;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
function ToolButton({
|
|
115
|
+
function ToolButton({
|
|
116
|
+
tool,
|
|
117
|
+
icon: Icon,
|
|
118
|
+
label,
|
|
119
|
+
shortcut,
|
|
120
|
+
activeTool,
|
|
121
|
+
onToolChange,
|
|
122
|
+
activeAccentClass,
|
|
123
|
+
}: ToolButtonProps) {
|
|
124
|
+
const isActive = activeTool === tool;
|
|
106
125
|
return (
|
|
107
126
|
<Tooltip>
|
|
108
127
|
<TooltipTrigger asChild>
|
|
109
128
|
<Button
|
|
110
|
-
variant={
|
|
129
|
+
variant={isActive ? 'default' : 'ghost'}
|
|
111
130
|
size="icon-sm"
|
|
112
131
|
onClick={(e) => {
|
|
113
132
|
// Blur button to close tooltip after click
|
|
114
133
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
115
134
|
onToolChange(tool);
|
|
116
135
|
}}
|
|
117
|
-
className={cn(
|
|
136
|
+
className={cn(
|
|
137
|
+
isActive && (activeAccentClass ?? 'bg-primary text-primary-foreground'),
|
|
138
|
+
)}
|
|
118
139
|
>
|
|
119
140
|
<Icon className="h-4 w-4" />
|
|
120
141
|
</Button>
|
|
@@ -305,7 +326,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
305
326
|
const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
|
|
306
327
|
const scriptPanelVisible = useViewerStore((state) => state.scriptPanelVisible);
|
|
307
328
|
const setScriptPanelVisible = useViewerStore((state) => state.setScriptPanelVisible);
|
|
329
|
+
const ganttPanelVisible = useViewerStore((state) => state.ganttPanelVisible);
|
|
330
|
+
const setGanttPanelVisible = useViewerStore((state) => state.setGanttPanelVisible);
|
|
308
331
|
// Cesium 3D overlay state
|
|
332
|
+
const cesiumAvailable = useViewerStore((state) => state.cesiumAvailable);
|
|
309
333
|
const cesiumEnabled = useViewerStore((state) => state.cesiumEnabled);
|
|
310
334
|
const toggleCesium = useViewerStore((state) => state.toggleCesium);
|
|
311
335
|
const storeModels = useViewerStore((state) => state.models);
|
|
@@ -507,21 +531,31 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
507
531
|
return false;
|
|
508
532
|
}, [desktopEntitlement, promptDesktopUpgrade]);
|
|
509
533
|
|
|
510
|
-
const handleToggleBottomPanel = useCallback((panel: 'script' | 'list') => {
|
|
534
|
+
const handleToggleBottomPanel = useCallback((panel: 'script' | 'list' | 'gantt') => {
|
|
511
535
|
if (activeAnalysisExtension?.placement === 'bottom') {
|
|
512
536
|
closeActiveAnalysisExtension();
|
|
513
537
|
}
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
const
|
|
538
|
+
const nextScriptVisible = panel === 'script' ? !scriptPanelVisible : false;
|
|
539
|
+
const nextListVisible = panel === 'list' ? !listPanelVisible : false;
|
|
540
|
+
const nextGanttVisible = panel === 'gantt' ? !ganttPanelVisible : false;
|
|
517
541
|
|
|
518
542
|
setScriptPanelVisible(nextScriptVisible);
|
|
519
543
|
setListPanelVisible(nextListVisible);
|
|
544
|
+
setGanttPanelVisible(nextGanttVisible);
|
|
520
545
|
|
|
521
|
-
if (nextScriptVisible || nextListVisible) {
|
|
546
|
+
if (nextScriptVisible || nextListVisible || nextGanttVisible) {
|
|
522
547
|
setRightPanelCollapsed(false);
|
|
523
548
|
}
|
|
524
|
-
}, [
|
|
549
|
+
}, [
|
|
550
|
+
activeAnalysisExtension?.placement,
|
|
551
|
+
ganttPanelVisible,
|
|
552
|
+
listPanelVisible,
|
|
553
|
+
scriptPanelVisible,
|
|
554
|
+
setGanttPanelVisible,
|
|
555
|
+
setListPanelVisible,
|
|
556
|
+
setRightPanelCollapsed,
|
|
557
|
+
setScriptPanelVisible,
|
|
558
|
+
]);
|
|
525
559
|
|
|
526
560
|
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens') => {
|
|
527
561
|
if (activeAnalysisExtension?.placement !== 'bottom') {
|
|
@@ -576,6 +610,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
576
610
|
if ((extension.placement ?? 'right') === 'bottom') {
|
|
577
611
|
setScriptPanelVisible(false);
|
|
578
612
|
setListPanelVisible(false);
|
|
613
|
+
setGanttPanelVisible(false);
|
|
579
614
|
setRightPanelCollapsed(false);
|
|
580
615
|
return;
|
|
581
616
|
}
|
|
@@ -588,6 +623,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
588
623
|
analysisExtensionState.activeId,
|
|
589
624
|
analysisExtensionState.extensions,
|
|
590
625
|
setBcfPanelVisible,
|
|
626
|
+
setGanttPanelVisible,
|
|
591
627
|
setIdsPanelVisible,
|
|
592
628
|
setLensPanelVisible,
|
|
593
629
|
setListPanelVisible,
|
|
@@ -599,6 +635,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
599
635
|
const panels = new Set<WorkspacePanel>();
|
|
600
636
|
if (scriptPanelVisible) panels.add('script');
|
|
601
637
|
if (listPanelVisible) panels.add('list');
|
|
638
|
+
if (ganttPanelVisible) panels.add('gantt');
|
|
602
639
|
if (bcfPanelVisible) panels.add('bcf');
|
|
603
640
|
if (idsPanelVisible) panels.add('ids');
|
|
604
641
|
if (lensPanelVisible) panels.add('lens');
|
|
@@ -607,6 +644,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
607
644
|
}, [
|
|
608
645
|
analysisExtensionState.activeId,
|
|
609
646
|
bcfPanelVisible,
|
|
647
|
+
ganttPanelVisible,
|
|
610
648
|
idsPanelVisible,
|
|
611
649
|
lensPanelVisible,
|
|
612
650
|
listPanelVisible,
|
|
@@ -618,6 +656,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
618
656
|
if (activeWorkspacePanels.size > 1) return 'Multiple Panels';
|
|
619
657
|
if (activeWorkspacePanels.has('script')) return 'Script Editor';
|
|
620
658
|
if (activeWorkspacePanels.has('list')) return 'Lists';
|
|
659
|
+
if (activeWorkspacePanels.has('gantt')) return 'Schedule';
|
|
621
660
|
if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
|
|
622
661
|
if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
|
|
623
662
|
if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
|
|
@@ -947,6 +986,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
947
986
|
<FileSpreadsheet className="h-4 w-4 mr-2" />
|
|
948
987
|
Lists
|
|
949
988
|
</DropdownMenuCheckboxItem>
|
|
989
|
+
<DropdownMenuCheckboxItem
|
|
990
|
+
checked={activeWorkspacePanels.has('gantt')}
|
|
991
|
+
onCheckedChange={() => handleToggleBottomPanel('gantt')}
|
|
992
|
+
>
|
|
993
|
+
<CalendarClock className="h-4 w-4 mr-2" />
|
|
994
|
+
Schedule (Gantt)
|
|
995
|
+
</DropdownMenuCheckboxItem>
|
|
950
996
|
<DropdownMenuSeparator />
|
|
951
997
|
<DropdownMenuCheckboxItem
|
|
952
998
|
checked={activeWorkspacePanels.has('bcf')}
|
|
@@ -1010,6 +1056,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1010
1056
|
|
|
1011
1057
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
1012
1058
|
|
|
1059
|
+
{/* ── Search (Tier-0 inline; ⌘F or / to focus) ── */}
|
|
1060
|
+
<SearchInline />
|
|
1061
|
+
|
|
1062
|
+
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
1063
|
+
|
|
1013
1064
|
{/* ── Navigation Tools ── */}
|
|
1014
1065
|
<ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
1015
1066
|
<ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
@@ -1019,6 +1070,15 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1019
1070
|
{/* ── Measurement & Section ── */}
|
|
1020
1071
|
<ToolButton tool="measure" icon={Ruler} label="Measure" shortcut="M" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
1021
1072
|
<ToolButton tool="section" icon={Scissors} label="Section" shortcut="X" activeTool={activeTool} onToolChange={setActiveTool} />
|
|
1073
|
+
<ToolButton
|
|
1074
|
+
tool="annotate"
|
|
1075
|
+
icon={MapPin}
|
|
1076
|
+
label="Annotate"
|
|
1077
|
+
shortcut="P"
|
|
1078
|
+
activeTool={activeTool}
|
|
1079
|
+
onToolChange={setActiveTool}
|
|
1080
|
+
activeAccentClass="bg-amber-500 text-white hover:bg-amber-500/90"
|
|
1081
|
+
/>
|
|
1022
1082
|
|
|
1023
1083
|
{/* Floorplan dropdown */}
|
|
1024
1084
|
{availableStoreys.length > 0 && (
|
|
@@ -1157,8 +1217,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1157
1217
|
</TooltipContent>
|
|
1158
1218
|
</Tooltip>
|
|
1159
1219
|
|
|
1160
|
-
{/* Cesium 3D Context toggle + settings */}
|
|
1161
|
-
{
|
|
1220
|
+
{/* Cesium 3D Context toggle + settings — web only, only when model has georeferencing */}
|
|
1221
|
+
{cesiumAvailable && !desktopShell && (
|
|
1162
1222
|
<div className="flex items-center">
|
|
1163
1223
|
<Tooltip>
|
|
1164
1224
|
<TooltipTrigger asChild>
|
|
@@ -1293,7 +1353,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1293
1353
|
<ThemeSwitch />
|
|
1294
1354
|
</div>
|
|
1295
1355
|
</TooltipTrigger>
|
|
1296
|
-
<TooltipContent>Toggle theme</TooltipContent>
|
|
1356
|
+
<TooltipContent>Toggle theme (Shift+click for secret mode)</TooltipContent>
|
|
1297
1357
|
</Tooltip>
|
|
1298
1358
|
|
|
1299
1359
|
<Tooltip>
|