@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
|
@@ -0,0 +1,406 @@
|
|
|
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
|
+
* GanttToolbar — play/pause, timeline scrubber, speed control,
|
|
7
|
+
* work-schedule selector, and animation toggle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useMemo } from 'react';
|
|
11
|
+
import {
|
|
12
|
+
Play,
|
|
13
|
+
Pause,
|
|
14
|
+
SkipBack,
|
|
15
|
+
SkipForward,
|
|
16
|
+
Repeat,
|
|
17
|
+
Repeat2,
|
|
18
|
+
Gauge,
|
|
19
|
+
Calendar,
|
|
20
|
+
CalendarPlus,
|
|
21
|
+
Plus,
|
|
22
|
+
X,
|
|
23
|
+
Trash2,
|
|
24
|
+
Undo2,
|
|
25
|
+
Redo2,
|
|
26
|
+
} from 'lucide-react';
|
|
27
|
+
import { Button } from '@/components/ui/button';
|
|
28
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
29
|
+
import {
|
|
30
|
+
Select,
|
|
31
|
+
SelectContent,
|
|
32
|
+
SelectItem,
|
|
33
|
+
SelectTrigger,
|
|
34
|
+
SelectValue,
|
|
35
|
+
} from '@/components/ui/select';
|
|
36
|
+
import { useViewerStore, countGeneratedTasks } from '@/store';
|
|
37
|
+
import type { GanttTimeScale } from '@/store';
|
|
38
|
+
import { toast } from '@/components/ui/toast';
|
|
39
|
+
import { formatDateTime } from './schedule-utils';
|
|
40
|
+
import { AnimationSettingsPopover } from './AnimationSettingsPopover';
|
|
41
|
+
|
|
42
|
+
interface GanttToolbarProps {
|
|
43
|
+
onClose?: () => void;
|
|
44
|
+
onOpenGenerate?: () => void;
|
|
45
|
+
canGenerate?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SPEED_OPTIONS: Array<{ value: number; label: string }> = [
|
|
49
|
+
{ value: 0.5, label: '0.5 d/s' },
|
|
50
|
+
{ value: 1, label: '1 d/s' },
|
|
51
|
+
{ value: 3, label: '3 d/s' },
|
|
52
|
+
{ value: 7, label: '1 w/s' },
|
|
53
|
+
{ value: 30, label: '1 mo/s' },
|
|
54
|
+
{ value: 90, label: '3 mo/s' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const SCALE_OPTIONS: Array<{ value: GanttTimeScale; label: string }> = [
|
|
58
|
+
{ value: 'hour', label: 'Hour' },
|
|
59
|
+
{ value: 'day', label: 'Day' },
|
|
60
|
+
{ value: 'week', label: 'Week' },
|
|
61
|
+
{ value: 'month', label: 'Month' },
|
|
62
|
+
{ value: 'year', label: 'Year' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Radix Select rejects '' as a SelectItem value — use a sentinel for the
|
|
66
|
+
// "All tasks" option and translate at the API boundary.
|
|
67
|
+
const ALL_SCHEDULES_SENTINEL = '__all__';
|
|
68
|
+
|
|
69
|
+
export function GanttToolbar({ onClose, onOpenGenerate, canGenerate }: GanttToolbarProps) {
|
|
70
|
+
const scheduleData = useViewerStore(s => s.scheduleData);
|
|
71
|
+
const scheduleRange = useViewerStore(s => s.scheduleRange);
|
|
72
|
+
const activeWorkScheduleId = useViewerStore(s => s.activeWorkScheduleId);
|
|
73
|
+
const setActiveWorkScheduleId = useViewerStore(s => s.setActiveWorkScheduleId);
|
|
74
|
+
const isPlaying = useViewerStore(s => s.playbackIsPlaying);
|
|
75
|
+
const playbackTime = useViewerStore(s => s.playbackTime);
|
|
76
|
+
const playbackSpeed = useViewerStore(s => s.playbackSpeed);
|
|
77
|
+
const playbackLoop = useViewerStore(s => s.playbackLoop);
|
|
78
|
+
const animationEnabled = useViewerStore(s => s.animationEnabled);
|
|
79
|
+
const pendingGeneratedCount = useViewerStore(s => countGeneratedTasks(s.scheduleData));
|
|
80
|
+
const clearGeneratedSchedule = useViewerStore(s => s.clearGeneratedSchedule);
|
|
81
|
+
const undoDepth = useViewerStore(s => s.scheduleUndoStack.length);
|
|
82
|
+
const redoDepth = useViewerStore(s => s.scheduleRedoStack.length);
|
|
83
|
+
const undoScheduleEdit = useViewerStore(s => s.undoScheduleEdit);
|
|
84
|
+
const redoScheduleEdit = useViewerStore(s => s.redoScheduleEdit);
|
|
85
|
+
const addTaskAction = useViewerStore(s => s.addTask);
|
|
86
|
+
const selectedTaskGlobalIds = useViewerStore(s => s.selectedTaskGlobalIds);
|
|
87
|
+
const scale = useViewerStore(s => s.ganttTimeScale);
|
|
88
|
+
const togglePlay = useViewerStore(s => s.togglePlaySchedule);
|
|
89
|
+
const pause = useViewerStore(s => s.pauseSchedule);
|
|
90
|
+
const seek = useViewerStore(s => s.seekSchedule);
|
|
91
|
+
const setSpeed = useViewerStore(s => s.setPlaybackSpeed);
|
|
92
|
+
const setLoop = useViewerStore(s => s.setPlaybackLoop);
|
|
93
|
+
const setAnimationEnabled = useViewerStore(s => s.setAnimationEnabled);
|
|
94
|
+
const setScale = useViewerStore(s => s.setGanttTimeScale);
|
|
95
|
+
|
|
96
|
+
const hasData = !!scheduleData && scheduleData.tasks.length > 0;
|
|
97
|
+
const hasDates = !!scheduleRange && !scheduleRange.synthetic;
|
|
98
|
+
|
|
99
|
+
const scheduleOptions = useMemo(() => {
|
|
100
|
+
if (!scheduleData) return [];
|
|
101
|
+
return [
|
|
102
|
+
{ value: ALL_SCHEDULES_SENTINEL, label: 'All tasks' },
|
|
103
|
+
...scheduleData.workSchedules.map(s => ({
|
|
104
|
+
value: s.globalId,
|
|
105
|
+
label: s.name || s.globalId,
|
|
106
|
+
})),
|
|
107
|
+
];
|
|
108
|
+
}, [scheduleData]);
|
|
109
|
+
|
|
110
|
+
const selectedScheduleValue = activeWorkScheduleId || ALL_SCHEDULES_SENTINEL;
|
|
111
|
+
const handleScheduleChange = useCallback((value: string) => {
|
|
112
|
+
setActiveWorkScheduleId(value === ALL_SCHEDULES_SENTINEL ? '' : value);
|
|
113
|
+
}, [setActiveWorkScheduleId]);
|
|
114
|
+
|
|
115
|
+
const scrubPercent = useMemo(() => {
|
|
116
|
+
if (!scheduleRange) return 0;
|
|
117
|
+
const span = scheduleRange.end - scheduleRange.start;
|
|
118
|
+
if (span <= 0) return 0;
|
|
119
|
+
return Math.min(100, Math.max(0, ((playbackTime - scheduleRange.start) / span) * 100));
|
|
120
|
+
}, [scheduleRange, playbackTime]);
|
|
121
|
+
|
|
122
|
+
const onScrubInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
123
|
+
if (!scheduleRange) return;
|
|
124
|
+
const pct = parseFloat(e.target.value) / 100;
|
|
125
|
+
seek(scheduleRange.start + pct * (scheduleRange.end - scheduleRange.start));
|
|
126
|
+
}, [scheduleRange, seek]);
|
|
127
|
+
|
|
128
|
+
const onScrubPointerDown = useCallback(() => {
|
|
129
|
+
if (isPlaying) pause();
|
|
130
|
+
}, [isPlaying, pause]);
|
|
131
|
+
|
|
132
|
+
const goStart = useCallback(() => {
|
|
133
|
+
if (scheduleRange) seek(scheduleRange.start);
|
|
134
|
+
}, [scheduleRange, seek]);
|
|
135
|
+
|
|
136
|
+
const goEnd = useCallback(() => {
|
|
137
|
+
if (scheduleRange) seek(scheduleRange.end);
|
|
138
|
+
}, [scheduleRange, seek]);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b bg-card/40 text-sm">
|
|
142
|
+
<div className="flex items-center gap-1">
|
|
143
|
+
<Tooltip>
|
|
144
|
+
<TooltipTrigger asChild>
|
|
145
|
+
<Button
|
|
146
|
+
size="icon-sm"
|
|
147
|
+
variant="ghost"
|
|
148
|
+
onClick={goStart}
|
|
149
|
+
disabled={!hasData}
|
|
150
|
+
aria-label="Jump to start"
|
|
151
|
+
>
|
|
152
|
+
<SkipBack className="h-4 w-4" />
|
|
153
|
+
</Button>
|
|
154
|
+
</TooltipTrigger>
|
|
155
|
+
<TooltipContent>Jump to start</TooltipContent>
|
|
156
|
+
</Tooltip>
|
|
157
|
+
|
|
158
|
+
<Tooltip>
|
|
159
|
+
<TooltipTrigger asChild>
|
|
160
|
+
<Button
|
|
161
|
+
size="icon-sm"
|
|
162
|
+
variant={isPlaying ? 'default' : 'ghost'}
|
|
163
|
+
onClick={togglePlay}
|
|
164
|
+
disabled={!hasData}
|
|
165
|
+
aria-label={isPlaying ? 'Pause' : 'Play'}
|
|
166
|
+
>
|
|
167
|
+
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
|
168
|
+
</Button>
|
|
169
|
+
</TooltipTrigger>
|
|
170
|
+
<TooltipContent>{isPlaying ? 'Pause' : 'Play'} construction sequence</TooltipContent>
|
|
171
|
+
</Tooltip>
|
|
172
|
+
|
|
173
|
+
<Tooltip>
|
|
174
|
+
<TooltipTrigger asChild>
|
|
175
|
+
<Button
|
|
176
|
+
size="icon-sm"
|
|
177
|
+
variant="ghost"
|
|
178
|
+
onClick={goEnd}
|
|
179
|
+
disabled={!hasData}
|
|
180
|
+
aria-label="Jump to finish"
|
|
181
|
+
>
|
|
182
|
+
<SkipForward className="h-4 w-4" />
|
|
183
|
+
</Button>
|
|
184
|
+
</TooltipTrigger>
|
|
185
|
+
<TooltipContent>Jump to finish</TooltipContent>
|
|
186
|
+
</Tooltip>
|
|
187
|
+
|
|
188
|
+
<Tooltip>
|
|
189
|
+
<TooltipTrigger asChild>
|
|
190
|
+
<Button
|
|
191
|
+
size="icon-sm"
|
|
192
|
+
variant={playbackLoop ? 'default' : 'ghost'}
|
|
193
|
+
onClick={() => setLoop(!playbackLoop)}
|
|
194
|
+
aria-label={playbackLoop ? 'Disable loop' : 'Enable loop'}
|
|
195
|
+
>
|
|
196
|
+
{playbackLoop ? <Repeat className="h-4 w-4" /> : <Repeat2 className="h-4 w-4" />}
|
|
197
|
+
</Button>
|
|
198
|
+
</TooltipTrigger>
|
|
199
|
+
<TooltipContent>{playbackLoop ? 'Looping' : 'One-shot'}</TooltipContent>
|
|
200
|
+
</Tooltip>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Scrub bar */}
|
|
204
|
+
<div className="flex-1 flex items-center gap-2 min-w-[240px]">
|
|
205
|
+
<input
|
|
206
|
+
type="range"
|
|
207
|
+
min={0}
|
|
208
|
+
max={100}
|
|
209
|
+
step={0.01}
|
|
210
|
+
value={scrubPercent}
|
|
211
|
+
onChange={onScrubInput}
|
|
212
|
+
onPointerDown={onScrubPointerDown}
|
|
213
|
+
disabled={!hasData}
|
|
214
|
+
className="flex-1 accent-primary cursor-pointer h-1 appearance-none bg-muted rounded-full"
|
|
215
|
+
aria-label="Playback position"
|
|
216
|
+
/>
|
|
217
|
+
<span className="text-xs text-muted-foreground font-mono whitespace-nowrap">
|
|
218
|
+
{hasData ? formatDateTime(playbackTime) : '—'}
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{/* Work schedule dropdown */}
|
|
223
|
+
<div className="flex items-center gap-1">
|
|
224
|
+
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
225
|
+
<Select value={selectedScheduleValue} onValueChange={handleScheduleChange}>
|
|
226
|
+
<SelectTrigger className="h-8 w-[180px] text-xs">
|
|
227
|
+
<SelectValue placeholder="All tasks" />
|
|
228
|
+
</SelectTrigger>
|
|
229
|
+
<SelectContent>
|
|
230
|
+
{scheduleOptions.map(opt => (
|
|
231
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
232
|
+
{opt.label}
|
|
233
|
+
</SelectItem>
|
|
234
|
+
))}
|
|
235
|
+
</SelectContent>
|
|
236
|
+
</Select>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Speed */}
|
|
240
|
+
<div className="flex items-center gap-1">
|
|
241
|
+
<Tooltip>
|
|
242
|
+
<TooltipTrigger asChild>
|
|
243
|
+
<Gauge className="h-4 w-4 text-muted-foreground" />
|
|
244
|
+
</TooltipTrigger>
|
|
245
|
+
<TooltipContent>Simulation speed</TooltipContent>
|
|
246
|
+
</Tooltip>
|
|
247
|
+
<Select
|
|
248
|
+
value={String(playbackSpeed)}
|
|
249
|
+
onValueChange={(v) => setSpeed(parseFloat(v))}
|
|
250
|
+
>
|
|
251
|
+
<SelectTrigger className="h-8 w-[110px] text-xs">
|
|
252
|
+
<SelectValue />
|
|
253
|
+
</SelectTrigger>
|
|
254
|
+
<SelectContent>
|
|
255
|
+
{SPEED_OPTIONS.map(opt => (
|
|
256
|
+
<SelectItem key={opt.value} value={String(opt.value)}>
|
|
257
|
+
{opt.label}
|
|
258
|
+
</SelectItem>
|
|
259
|
+
))}
|
|
260
|
+
</SelectContent>
|
|
261
|
+
</Select>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* Scale */}
|
|
265
|
+
<Select value={scale} onValueChange={(v) => setScale(v as GanttTimeScale)}>
|
|
266
|
+
<SelectTrigger className="h-8 w-[90px] text-xs">
|
|
267
|
+
<SelectValue />
|
|
268
|
+
</SelectTrigger>
|
|
269
|
+
<SelectContent>
|
|
270
|
+
{SCALE_OPTIONS.map(opt => (
|
|
271
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
272
|
+
{opt.label}
|
|
273
|
+
</SelectItem>
|
|
274
|
+
))}
|
|
275
|
+
</SelectContent>
|
|
276
|
+
</Select>
|
|
277
|
+
|
|
278
|
+
{/* Generate from spatial hierarchy */}
|
|
279
|
+
{onOpenGenerate && (
|
|
280
|
+
<Tooltip>
|
|
281
|
+
<TooltipTrigger asChild>
|
|
282
|
+
<Button
|
|
283
|
+
size="icon-sm"
|
|
284
|
+
variant="ghost"
|
|
285
|
+
onClick={onOpenGenerate}
|
|
286
|
+
disabled={!canGenerate}
|
|
287
|
+
aria-label="Generate construction schedule"
|
|
288
|
+
>
|
|
289
|
+
<CalendarPlus className="h-4 w-4" />
|
|
290
|
+
</Button>
|
|
291
|
+
</TooltipTrigger>
|
|
292
|
+
<TooltipContent>
|
|
293
|
+
{canGenerate ? 'Generate schedule…' : 'No spatial hierarchy or geometry to generate from'}
|
|
294
|
+
</TooltipContent>
|
|
295
|
+
</Tooltip>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{/* + Task — insert a new task after the currently-selected row
|
|
299
|
+
(or at the end when none is selected). Auto-selects the new
|
|
300
|
+
task so the Inspector's Task card lights up for rename. */}
|
|
301
|
+
{hasData && (
|
|
302
|
+
<Tooltip>
|
|
303
|
+
<TooltipTrigger asChild>
|
|
304
|
+
<Button
|
|
305
|
+
size="icon-sm"
|
|
306
|
+
variant="ghost"
|
|
307
|
+
onClick={() => {
|
|
308
|
+
const afterGlobalId = selectedTaskGlobalIds.size === 1
|
|
309
|
+
? selectedTaskGlobalIds.values().next().value
|
|
310
|
+
: undefined;
|
|
311
|
+
addTaskAction({ afterGlobalId });
|
|
312
|
+
}}
|
|
313
|
+
aria-label="Add task"
|
|
314
|
+
>
|
|
315
|
+
<Plus className="h-4 w-4" />
|
|
316
|
+
</Button>
|
|
317
|
+
</TooltipTrigger>
|
|
318
|
+
<TooltipContent>Add task (after selection or at end)</TooltipContent>
|
|
319
|
+
</Tooltip>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Undo / Redo for schedule edits. Gated on stack depth so the
|
|
323
|
+
buttons only appear when there's actually something to undo —
|
|
324
|
+
avoids a persistent greyed-out pair on clean schedules. */}
|
|
325
|
+
{(undoDepth > 0 || redoDepth > 0) && (
|
|
326
|
+
<div className="flex items-center gap-1">
|
|
327
|
+
<Tooltip>
|
|
328
|
+
<TooltipTrigger asChild>
|
|
329
|
+
<Button
|
|
330
|
+
size="icon-sm"
|
|
331
|
+
variant="ghost"
|
|
332
|
+
onClick={undoScheduleEdit}
|
|
333
|
+
disabled={undoDepth === 0}
|
|
334
|
+
aria-label="Undo schedule edit"
|
|
335
|
+
>
|
|
336
|
+
<Undo2 className="h-4 w-4" />
|
|
337
|
+
</Button>
|
|
338
|
+
</TooltipTrigger>
|
|
339
|
+
<TooltipContent>Undo (Ctrl+Z)</TooltipContent>
|
|
340
|
+
</Tooltip>
|
|
341
|
+
<Tooltip>
|
|
342
|
+
<TooltipTrigger asChild>
|
|
343
|
+
<Button
|
|
344
|
+
size="icon-sm"
|
|
345
|
+
variant="ghost"
|
|
346
|
+
onClick={redoScheduleEdit}
|
|
347
|
+
disabled={redoDepth === 0}
|
|
348
|
+
aria-label="Redo schedule edit"
|
|
349
|
+
>
|
|
350
|
+
<Redo2 className="h-4 w-4" />
|
|
351
|
+
</Button>
|
|
352
|
+
</TooltipTrigger>
|
|
353
|
+
<TooltipContent>Redo (Ctrl+Shift+Z)</TooltipContent>
|
|
354
|
+
</Tooltip>
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
|
|
358
|
+
{/* Discard pending generated schedule — only visible when at least
|
|
359
|
+
one locally-generated task exists. Keeps extracted tasks intact
|
|
360
|
+
so partial-authoring workflows can still revert just the
|
|
361
|
+
pending tail. */}
|
|
362
|
+
{pendingGeneratedCount > 0 && (
|
|
363
|
+
<Tooltip>
|
|
364
|
+
<TooltipTrigger asChild>
|
|
365
|
+
<Button
|
|
366
|
+
size="icon-sm"
|
|
367
|
+
variant="ghost"
|
|
368
|
+
onClick={() => {
|
|
369
|
+
const removed = clearGeneratedSchedule();
|
|
370
|
+
if (removed > 0) {
|
|
371
|
+
toast.success(`Discarded ${removed} pending task${removed === 1 ? '' : 's'}.`);
|
|
372
|
+
}
|
|
373
|
+
}}
|
|
374
|
+
aria-label={`Discard ${pendingGeneratedCount} pending generated task${pendingGeneratedCount === 1 ? '' : 's'}`}
|
|
375
|
+
className="text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300"
|
|
376
|
+
>
|
|
377
|
+
<Trash2 className="h-4 w-4" />
|
|
378
|
+
</Button>
|
|
379
|
+
</TooltipTrigger>
|
|
380
|
+
<TooltipContent>
|
|
381
|
+
Discard {pendingGeneratedCount} pending schedule task{pendingGeneratedCount === 1 ? '' : 's'}
|
|
382
|
+
</TooltipContent>
|
|
383
|
+
</Tooltip>
|
|
384
|
+
)}
|
|
385
|
+
|
|
386
|
+
{/* Animation settings popover (replaces the bare toggle — gives the
|
|
387
|
+
user access to lifecycle colour / palette / preparation window). */}
|
|
388
|
+
<AnimationSettingsPopover
|
|
389
|
+
animationEnabled={animationEnabled}
|
|
390
|
+
onToggleAnimation={() => setAnimationEnabled(!animationEnabled)}
|
|
391
|
+
/>
|
|
392
|
+
|
|
393
|
+
{onClose && (
|
|
394
|
+
<Button size="icon-sm" variant="ghost" onClick={onClose} aria-label="Close Gantt panel">
|
|
395
|
+
<X className="h-4 w-4" />
|
|
396
|
+
</Button>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{hasData && !hasDates && (
|
|
400
|
+
<span className="text-xs text-amber-500 whitespace-nowrap" title="No real dates — using synthetic range">
|
|
401
|
+
No dates
|
|
402
|
+
</span>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
* GenerateAdvancedPanel — disclosure-expanded "Advanced" section of the
|
|
7
|
+
* Generate dialog. Holds rarely-touched fields (lag, PredefinedType,
|
|
8
|
+
* schedule name, sequence linking toggle, skip-empty toggle). Extracted
|
|
9
|
+
* so the main dialog stays readable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
13
|
+
import { Input } from '@/components/ui/input';
|
|
14
|
+
import { Label } from '@/components/ui/label';
|
|
15
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
16
|
+
import { Switch } from '@/components/ui/switch';
|
|
17
|
+
import type { GenerateScheduleOptions, SpatialGroupStrategy } from './generate-schedule';
|
|
18
|
+
|
|
19
|
+
const TASK_TYPES = [
|
|
20
|
+
'CONSTRUCTION', 'INSTALLATION', 'DEMOLITION', 'DISMANTLE',
|
|
21
|
+
'DISPOSAL', 'LOGISTIC', 'MAINTENANCE', 'MOVE',
|
|
22
|
+
'OPERATION', 'REMOVAL', 'RENOVATION', 'ATTENDANCE',
|
|
23
|
+
'USERDEFINED', 'NOTDEFINED',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export interface GenerateAdvancedPanelProps {
|
|
27
|
+
open: boolean;
|
|
28
|
+
onOpenChange: (next: boolean) => void;
|
|
29
|
+
strategy: SpatialGroupStrategy;
|
|
30
|
+
lagDays: number;
|
|
31
|
+
predefinedType: string;
|
|
32
|
+
scheduleName: string;
|
|
33
|
+
linkSequences: boolean;
|
|
34
|
+
skipEmptyGroups: boolean;
|
|
35
|
+
onChange: <K extends keyof GenerateScheduleOptions>(
|
|
36
|
+
key: K,
|
|
37
|
+
value: GenerateScheduleOptions[K],
|
|
38
|
+
) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function GenerateAdvancedPanel({
|
|
42
|
+
open,
|
|
43
|
+
onOpenChange,
|
|
44
|
+
strategy,
|
|
45
|
+
lagDays,
|
|
46
|
+
predefinedType,
|
|
47
|
+
scheduleName,
|
|
48
|
+
linkSequences,
|
|
49
|
+
skipEmptyGroups,
|
|
50
|
+
onChange,
|
|
51
|
+
}: GenerateAdvancedPanelProps) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="rounded-md border">
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-sm font-medium text-left hover:bg-muted/40 transition-colors"
|
|
57
|
+
onClick={() => onOpenChange(!open)}
|
|
58
|
+
aria-expanded={open}
|
|
59
|
+
>
|
|
60
|
+
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
61
|
+
Advanced
|
|
62
|
+
</button>
|
|
63
|
+
{open && (
|
|
64
|
+
<div className="grid gap-3 border-t p-3">
|
|
65
|
+
<div className="grid grid-cols-2 gap-3">
|
|
66
|
+
<div className="grid gap-1.5">
|
|
67
|
+
<Label htmlFor="gen-lag">Lag days (between groups)</Label>
|
|
68
|
+
<Input
|
|
69
|
+
id="gen-lag"
|
|
70
|
+
type="number"
|
|
71
|
+
min={0}
|
|
72
|
+
step={1}
|
|
73
|
+
value={lagDays}
|
|
74
|
+
onChange={(e) => {
|
|
75
|
+
const v = parseFloat(e.target.value);
|
|
76
|
+
onChange('lagDays', Number.isFinite(v) && v >= 0 ? v : 0);
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="grid gap-1.5">
|
|
81
|
+
<Label htmlFor="gen-type">PredefinedType</Label>
|
|
82
|
+
<Select
|
|
83
|
+
value={predefinedType}
|
|
84
|
+
onValueChange={(v) => onChange('predefinedType', v)}
|
|
85
|
+
>
|
|
86
|
+
<SelectTrigger id="gen-type">
|
|
87
|
+
<SelectValue />
|
|
88
|
+
</SelectTrigger>
|
|
89
|
+
<SelectContent>
|
|
90
|
+
{TASK_TYPES.map(t => (
|
|
91
|
+
<SelectItem key={t} value={t}>{t}</SelectItem>
|
|
92
|
+
))}
|
|
93
|
+
</SelectContent>
|
|
94
|
+
</Select>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="grid gap-1.5">
|
|
99
|
+
<Label htmlFor="gen-name">Work schedule name</Label>
|
|
100
|
+
<Input
|
|
101
|
+
id="gen-name"
|
|
102
|
+
value={scheduleName}
|
|
103
|
+
onChange={(e) => onChange('scheduleName', e.target.value)}
|
|
104
|
+
placeholder="Construction schedule"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<ToggleRow
|
|
109
|
+
label="Link tasks with FS dependencies"
|
|
110
|
+
description="Adds IfcRelSequence edges between consecutive groups."
|
|
111
|
+
checked={linkSequences}
|
|
112
|
+
onChange={(v) => onChange('linkSequences', v)}
|
|
113
|
+
/>
|
|
114
|
+
<ToggleRow
|
|
115
|
+
label="Skip empty groups"
|
|
116
|
+
description={
|
|
117
|
+
strategy === 'IfcElement'
|
|
118
|
+
? 'Ignore Z slices with no elements.'
|
|
119
|
+
: 'Ignore storeys or buildings with no contained products.'
|
|
120
|
+
}
|
|
121
|
+
checked={skipEmptyGroups}
|
|
122
|
+
onChange={(v) => onChange('skipEmptyGroups', v)}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ToggleRowProps {
|
|
131
|
+
label: string;
|
|
132
|
+
description: string;
|
|
133
|
+
checked: boolean;
|
|
134
|
+
onChange: (next: boolean) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ToggleRow({ label, description, checked, onChange }: ToggleRowProps) {
|
|
138
|
+
return (
|
|
139
|
+
<label className="flex items-center justify-between gap-3 cursor-pointer">
|
|
140
|
+
<span className="grid gap-0.5">
|
|
141
|
+
<span className="text-sm font-medium">{label}</span>
|
|
142
|
+
<span className="text-xs text-muted-foreground">{description}</span>
|
|
143
|
+
</span>
|
|
144
|
+
<Switch checked={checked} onCheckedChange={onChange} />
|
|
145
|
+
</label>
|
|
146
|
+
);
|
|
147
|
+
}
|