@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.
Files changed (196) hide show
  1. package/.turbo/turbo-build.log +20 -17
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +630 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -1
  5. package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  6. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  7. package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
  8. package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
  9. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  10. package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
  11. package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
  12. package/dist/assets/index-COnQRuqY.css +1 -0
  13. package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
  14. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  15. package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
  16. package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
  17. package/dist/index.html +8 -7
  18. package/index.html +1 -0
  19. package/package.json +13 -13
  20. package/src/App.tsx +16 -2
  21. package/src/apache-arrow.d.ts +30 -0
  22. package/src/components/viewer/AddElementPanel.tsx +758 -0
  23. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  24. package/src/components/viewer/CesiumOverlay.tsx +62 -19
  25. package/src/components/viewer/ChatPanel.tsx +259 -93
  26. package/src/components/viewer/CommandPalette.tsx +56 -7
  27. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  28. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  29. package/src/components/viewer/ExportDialog.tsx +19 -1
  30. package/src/components/viewer/MainToolbar.tsx +73 -13
  31. package/src/components/viewer/PropertiesPanel.tsx +237 -23
  32. package/src/components/viewer/SearchInline.tsx +669 -0
  33. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  34. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  35. package/src/components/viewer/SearchModal.text.tsx +388 -0
  36. package/src/components/viewer/SearchModal.tsx +235 -0
  37. package/src/components/viewer/SettingsPage.tsx +252 -101
  38. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  39. package/src/components/viewer/ToolOverlays.tsx +5 -0
  40. package/src/components/viewer/ViewerLayout.tsx +25 -4
  41. package/src/components/viewer/Viewport.tsx +25 -3
  42. package/src/components/viewer/ViewportContainer.tsx +51 -64
  43. package/src/components/viewer/ViewportOverlays.tsx +5 -2
  44. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  45. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  46. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  47. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  48. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  49. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  50. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  51. package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
  52. package/src/components/viewer/properties/LocationMap.tsx +9 -7
  53. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  54. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  55. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  56. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  57. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  58. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  59. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  60. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  61. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  62. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  63. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  64. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  65. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  66. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  67. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  68. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  69. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  70. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  71. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  72. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  73. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  74. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  75. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  76. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  77. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  78. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  79. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  80. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  81. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  82. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  83. package/src/components/viewer/selectionHandlers.ts +446 -0
  84. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  85. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  86. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  87. package/src/components/viewer/useAnimationLoop.ts +9 -1
  88. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  89. package/src/components/viewer/useMouseControls.ts +9 -1
  90. package/src/components/viewer/useRenderUpdates.ts +1 -1
  91. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  92. package/src/hooks/ingest/viewerModelIngest.ts +7 -2
  93. package/src/hooks/useIfcFederation.ts +326 -71
  94. package/src/hooks/useIfcLoader.ts +23 -10
  95. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  96. package/src/hooks/useSandbox.ts +1 -1
  97. package/src/hooks/useSearchIndex.ts +125 -0
  98. package/src/hooks/useViewControls.ts +13 -5
  99. package/src/index.css +550 -10
  100. package/src/lib/desktop-entitlement.ts +2 -4
  101. package/src/lib/geo/cesium-bridge.ts +15 -7
  102. package/src/lib/geo/effective-georef.test.ts +73 -0
  103. package/src/lib/geo/effective-georef.ts +111 -0
  104. package/src/lib/geo/reproject.ts +105 -19
  105. package/src/lib/llm/byok-guard.test.ts +77 -0
  106. package/src/lib/llm/byok-guard.ts +39 -0
  107. package/src/lib/llm/free-models.test.ts +0 -6
  108. package/src/lib/llm/models.ts +104 -42
  109. package/src/lib/llm/stream-client.ts +74 -110
  110. package/src/lib/llm/stream-direct.test.ts +130 -0
  111. package/src/lib/llm/stream-direct.ts +316 -0
  112. package/src/lib/llm/system-prompt.test.ts +14 -0
  113. package/src/lib/llm/system-prompt.ts +102 -1
  114. package/src/lib/llm/types.ts +20 -2
  115. package/src/lib/recent-files.ts +38 -4
  116. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  117. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  118. package/src/lib/scripts/templates.ts +7 -0
  119. package/src/lib/search/common-ifc-types.ts +36 -0
  120. package/src/lib/search/filter-evaluate.test.ts +537 -0
  121. package/src/lib/search/filter-evaluate.ts +610 -0
  122. package/src/lib/search/filter-rules.test.ts +119 -0
  123. package/src/lib/search/filter-rules.ts +198 -0
  124. package/src/lib/search/filter-schema.test.ts +233 -0
  125. package/src/lib/search/filter-schema.ts +146 -0
  126. package/src/lib/search/recent-searches.test.ts +116 -0
  127. package/src/lib/search/recent-searches.ts +93 -0
  128. package/src/lib/search/result-export.test.ts +101 -0
  129. package/src/lib/search/result-export.ts +104 -0
  130. package/src/lib/search/saved-filters.test.ts +118 -0
  131. package/src/lib/search/saved-filters.ts +154 -0
  132. package/src/lib/search/tier0-scan.test.ts +196 -0
  133. package/src/lib/search/tier0-scan.ts +237 -0
  134. package/src/lib/search/tier1-index.test.ts +242 -0
  135. package/src/lib/search/tier1-index.ts +448 -0
  136. package/src/main.tsx +1 -10
  137. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  138. package/src/sdk/adapters/export-adapter.ts +404 -1
  139. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  140. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  141. package/src/sdk/adapters/model-compat.ts +8 -2
  142. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  143. package/src/sdk/adapters/store-adapter.ts +201 -0
  144. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  145. package/src/sdk/local-backend.ts +16 -8
  146. package/src/services/api-keys.ts +73 -0
  147. package/src/services/desktop-export.ts +3 -1
  148. package/src/services/desktop-native-metadata.ts +41 -18
  149. package/src/services/file-dialog.ts +4 -1
  150. package/src/services/tauri-modules.d.ts +25 -0
  151. package/src/store/basketVisibleSet.ts +3 -0
  152. package/src/store/constants.ts +20 -2
  153. package/src/store/globalId.ts +4 -1
  154. package/src/store/index.ts +82 -6
  155. package/src/store/slices/addElementMeshes.ts +365 -0
  156. package/src/store/slices/addElementSlice.ts +275 -0
  157. package/src/store/slices/annotationsSlice.test.ts +133 -0
  158. package/src/store/slices/annotationsSlice.ts +251 -0
  159. package/src/store/slices/cesiumSlice.ts +5 -0
  160. package/src/store/slices/chatSlice.test.ts +6 -76
  161. package/src/store/slices/chatSlice.ts +17 -58
  162. package/src/store/slices/dataSlice.test.ts +23 -4
  163. package/src/store/slices/dataSlice.ts +1 -1
  164. package/src/store/slices/modelSlice.test.ts +67 -9
  165. package/src/store/slices/modelSlice.ts +39 -7
  166. package/src/store/slices/mutationSlice.ts +964 -3
  167. package/src/store/slices/overlayCompositor.test.ts +164 -0
  168. package/src/store/slices/overlaySlice.test.ts +93 -0
  169. package/src/store/slices/overlaySlice.ts +151 -0
  170. package/src/store/slices/pinboardSlice.test.ts +6 -1
  171. package/src/store/slices/playbackSlice.ts +128 -0
  172. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  173. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  174. package/src/store/slices/scheduleSlice.test.ts +694 -0
  175. package/src/store/slices/scheduleSlice.ts +1330 -0
  176. package/src/store/slices/searchSlice.test.ts +342 -0
  177. package/src/store/slices/searchSlice.ts +341 -0
  178. package/src/store/slices/sectionSlice.test.ts +87 -7
  179. package/src/store/slices/sectionSlice.ts +151 -5
  180. package/src/store/slices/selectionSlice.test.ts +46 -0
  181. package/src/store/slices/selectionSlice.ts +20 -0
  182. package/src/store/slices/uiSlice.ts +28 -5
  183. package/src/store/types.ts +26 -0
  184. package/src/store.ts +14 -0
  185. package/src/utils/nativeSpatialDataStore.ts +4 -1
  186. package/src/utils/viewportUtils.ts +7 -2
  187. package/src/vite-env.d.ts +0 -4
  188. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  189. package/dist/assets/ids-B4jTqB1O.js +0 -1
  190. package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
  191. package/dist/assets/index-DckuDqlv.css +0 -1
  192. package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
  193. package/src/components/viewer/UpgradePage.tsx +0 -71
  194. package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
  195. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  196. package/src/lib/llm/clerk-auth.ts +0 -62
@@ -0,0 +1,97 @@
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
+ import { Calendar, CalendarClock, CalendarPlus, X } from 'lucide-react';
6
+ import { Button } from '@/components/ui/button';
7
+
8
+ interface GanttEmptyStateProps {
9
+ loading: boolean;
10
+ hasModel: boolean;
11
+ /** When true, the active model has a spatial hierarchy — enables the CTA. */
12
+ canGenerate?: boolean;
13
+ /** Human-readable extraction error (last parser failure), if any. */
14
+ extractionError?: string | null;
15
+ onClose?: () => void;
16
+ onGenerate?: () => void;
17
+ }
18
+
19
+ export function GanttEmptyState({
20
+ loading,
21
+ hasModel,
22
+ canGenerate,
23
+ extractionError,
24
+ onClose,
25
+ onGenerate,
26
+ }: GanttEmptyStateProps) {
27
+ return (
28
+ <div className="relative h-full w-full flex flex-col items-center justify-center text-center p-8 gap-3 text-muted-foreground">
29
+ {onClose && (
30
+ <Button
31
+ size="icon-sm"
32
+ variant="ghost"
33
+ className="absolute top-2 right-2"
34
+ onClick={onClose}
35
+ aria-label="Close"
36
+ >
37
+ <X className="h-4 w-4" />
38
+ </Button>
39
+ )}
40
+ <div className="relative">
41
+ <Calendar className="h-12 w-12" strokeWidth={1} />
42
+ <CalendarClock className="h-6 w-6 absolute -bottom-1 -right-1 text-primary" strokeWidth={1.5} />
43
+ </div>
44
+ {!hasModel ? (
45
+ <>
46
+ <h3 className="text-sm font-semibold text-foreground">Load a model with IfcTasks</h3>
47
+ <p className="text-xs max-w-sm">
48
+ Open an IFC file containing <span className="font-mono">IfcTask</span> or
49
+ <span className="font-mono"> IfcWorkSchedule</span> entities to see the construction
50
+ schedule here.
51
+ </p>
52
+ </>
53
+ ) : loading ? (
54
+ <p className="text-xs">Extracting schedule…</p>
55
+ ) : extractionError ? (
56
+ <>
57
+ <h3 className="text-sm font-semibold text-destructive">Schedule extraction failed</h3>
58
+ <p className="text-xs max-w-md text-muted-foreground">
59
+ <span className="font-mono text-destructive">{extractionError}</span>
60
+ <br />
61
+ Re-open the model or inspect the browser console for details.
62
+ </p>
63
+ {canGenerate && onGenerate && (
64
+ <div className="flex flex-col items-center gap-2 pt-2">
65
+ <Button size="sm" variant="outline" onClick={onGenerate} className="gap-2">
66
+ <CalendarPlus className="h-4 w-4" />
67
+ Generate a schedule instead
68
+ </Button>
69
+ </div>
70
+ )}
71
+ </>
72
+ ) : (
73
+ <>
74
+ <h3 className="text-sm font-semibold text-foreground">No schedule found</h3>
75
+ <p className="text-xs max-w-md">
76
+ This model doesn&apos;t define any <span className="font-mono">IfcTask</span>,
77
+ <span className="font-mono"> IfcWorkSchedule</span>, or
78
+ <span className="font-mono"> IfcRelSequence</span> entities. The Gantt panel powers
79
+ itself from those entities and the products they control via
80
+ <span className="font-mono"> IfcRelAssignsToProcess</span>.
81
+ </p>
82
+ {canGenerate && onGenerate && (
83
+ <div className="flex flex-col items-center gap-2 pt-2">
84
+ <Button size="sm" onClick={onGenerate} className="gap-2">
85
+ <CalendarPlus className="h-4 w-4" />
86
+ Generate schedule
87
+ </Button>
88
+ <p className="text-xs text-muted-foreground max-w-xs">
89
+ Build a schedule by storey, building, or element-Z height slice.
90
+ </p>
91
+ </div>
92
+ )}
93
+ </>
94
+ )}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,295 @@
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
+ * GanttPanel — 4D / IfcTask Gantt chart rendered in the viewer's bottom panel.
7
+ *
8
+ * Gantt ↔ 3D: selecting task rows highlights their products in the 3D
9
+ * viewport via the renderer's selection-highlight channel (no isolation,
10
+ * no hiding — highlight only). Clearing the selection removes the
11
+ * highlight. The 4D animator runs completely uninterrupted either way.
12
+ */
13
+
14
+ import { useEffect, useMemo, useRef, useState } from 'react';
15
+ import { useShallow } from 'zustand/react/shallow';
16
+ import { extractScheduleOnDemand } from '@ifc-lite/parser';
17
+ import { useViewerStore } from '@/store';
18
+ import { resolveScheduleSourceModelId } from '@/store/slices/schedule-edit-helpers';
19
+ import { useIfc } from '@/hooks/useIfc';
20
+ import { GanttToolbar } from './GanttToolbar';
21
+ import { GanttTaskTree } from './GanttTaskTree';
22
+ import { GanttTimeline } from './GanttTimeline';
23
+ import { GanttEmptyState } from './GanttEmptyState';
24
+ import { GenerateScheduleDialog } from './GenerateScheduleDialog';
25
+ import { flattenTaskTree } from './schedule-utils';
26
+ import { canGenerateScheduleFrom, resolveActiveDataStore } from './generate-schedule';
27
+ import { useConstructionSequence } from './useConstructionSequence';
28
+ import { useGanttSelection3DHighlight } from './useGanttSelection3DHighlight';
29
+ import { useOverlayCompositor } from './useOverlayCompositor';
30
+
31
+ interface GanttPanelProps {
32
+ onClose?: () => void;
33
+ }
34
+
35
+ const LEFT_PANE_WIDTH = 320;
36
+
37
+ export function GanttPanel({ onClose }: GanttPanelProps) {
38
+ const { ifcDataStore, models, loading, activeModelId } = useIfc();
39
+
40
+ // Resolve the active model once; shared by extraction + canGenerate.
41
+ const activeStore = useMemo(
42
+ () => resolveActiveDataStore(ifcDataStore, activeModelId, models),
43
+ [ifcDataStore, activeModelId, models],
44
+ );
45
+
46
+ const {
47
+ scheduleData,
48
+ scheduleRange,
49
+ activeWorkScheduleId,
50
+ expandedTaskGlobalIds,
51
+ hoveredTaskGlobalId,
52
+ selectedTaskGlobalIds,
53
+ ganttTimeScale,
54
+ playbackTime,
55
+ setScheduleData,
56
+ toggleTaskExpanded,
57
+ setHoveredTaskGlobalId,
58
+ setSelectedTaskGlobalIds,
59
+ seekSchedule,
60
+ } = useViewerStore(useShallow(s => ({
61
+ scheduleData: s.scheduleData,
62
+ scheduleRange: s.scheduleRange,
63
+ activeWorkScheduleId: s.activeWorkScheduleId,
64
+ expandedTaskGlobalIds: s.expandedTaskGlobalIds,
65
+ hoveredTaskGlobalId: s.hoveredTaskGlobalId,
66
+ selectedTaskGlobalIds: s.selectedTaskGlobalIds,
67
+ ganttTimeScale: s.ganttTimeScale,
68
+ playbackTime: s.playbackTime,
69
+ setScheduleData: s.setScheduleData,
70
+ toggleTaskExpanded: s.toggleTaskExpanded,
71
+ setHoveredTaskGlobalId: s.setHoveredTaskGlobalId,
72
+ setSelectedTaskGlobalIds: s.setSelectedTaskGlobalIds,
73
+ seekSchedule: s.seekSchedule,
74
+ })));
75
+
76
+ /** Last schedule-extraction error message (surfaced in the empty state). */
77
+ const [extractionError, setExtractionError] = useState<string | null>(null);
78
+
79
+ // Extract schedule data whenever the resolved data store changes.
80
+ useEffect(() => {
81
+ if (!activeStore) {
82
+ if (scheduleData) setScheduleData(null);
83
+ setExtractionError(null);
84
+ return;
85
+ }
86
+ try {
87
+ const extraction = extractScheduleOnDemand(activeStore);
88
+
89
+ // CRITICAL guard: do NOT overwrite an in-memory user-edited /
90
+ // generated schedule with null just because the underlying
91
+ // IfcDataStore reference shifted (this effect re-runs when
92
+ // geometry finishes streaming, spatial hierarchy rebuilds, or
93
+ // any other store mutation changes the activeStore identity).
94
+ // Earlier revisions did `setScheduleData(hasSchedule ? extraction
95
+ // : null)` unconditionally, which silently wiped the generated
96
+ // schedule moments before the user clicked Export — leading to
97
+ // an exported IFC with no task entities and an empty Gantt on
98
+ // re-import. Only replace when the extraction actually has data,
99
+ // or when we've previously had no schedule in memory.
100
+ const s = useViewerStore.getState();
101
+ const hasPendingSchedule = !!s.scheduleData && s.scheduleData.tasks.length > 0
102
+ && (s.scheduleIsEdited || s.scheduleData.tasks.some(t => !t.expressId || t.expressId <= 0));
103
+ if (extraction.hasSchedule) {
104
+ // New extraction wins — this is the "fresh file with a real
105
+ // schedule" case. Any generated tail in memory is replaced;
106
+ // that's intentional because we can't reconcile it with a
107
+ // different source.
108
+ setScheduleData(extraction);
109
+ } else if (!hasPendingSchedule) {
110
+ // No extraction + no pending edits → fine to clear.
111
+ setScheduleData(null);
112
+ } // else: keep whatever's in memory (generated / edited).
113
+ setExtractionError(null);
114
+ } catch (err) {
115
+ const message = err instanceof Error ? err.message : String(err);
116
+ console.warn('[GanttPanel] Failed to extract schedule', err);
117
+ setScheduleData(null);
118
+ setExtractionError(message);
119
+ }
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, [activeStore]);
122
+
123
+ // Single compositor — reads the overlay-layer registry and writes the
124
+ // composite into the renderer's legacy hiddenEntities / pendingColorUpdates
125
+ // channels. Must be mounted BEFORE any layer owner in the render tree so
126
+ // its first reconcile can observe their initial contributions.
127
+ useOverlayCompositor();
128
+
129
+ // Drive the 3D viewport's hidden-entity set from the playback clock.
130
+ // Registers the 'animation' overlay layer; the compositor above does
131
+ // the actual write.
132
+ useConstructionSequence();
133
+
134
+ // Highlight the current Gantt selection's products in 3D. Selection-only
135
+ // — no visibility changes — so it never interferes with the animator.
136
+ useGanttSelection3DHighlight();
137
+
138
+ // Flatten task tree honoring expand/collapse state.
139
+ const rows = useMemo(
140
+ () => flattenTaskTree(scheduleData, expandedTaskGlobalIds, activeWorkScheduleId || undefined),
141
+ [scheduleData, expandedTaskGlobalIds, activeWorkScheduleId],
142
+ );
143
+
144
+ // Shared scroll position between task list and timeline (so rows line up).
145
+ const [scrollTop, setScrollTop] = useState(0);
146
+ const leftRef = useRef<HTMLDivElement>(null);
147
+
148
+ // Generate-from-storeys dialog state lives in the slice so the command
149
+ // palette / hotkeys can open it without going through this component.
150
+ const generateOpen = useViewerStore(s => s.generateScheduleDialogOpen);
151
+ const setGenerateOpen = useViewerStore(s => s.setGenerateScheduleDialogOpen);
152
+ const canGenerate = useMemo(() => {
153
+ // Geometry-only models (no spatial hierarchy) can still generate via
154
+ // the Height strategy, so surface the button whenever EITHER a
155
+ // spatial tree OR meshes exist on the active source model.
156
+ const sourceModelId = resolveScheduleSourceModelId(models, activeModelId);
157
+ const meshes = sourceModelId ? models.get(sourceModelId)?.geometryResult?.meshes : undefined;
158
+ const ctx = meshes && meshes.length > 0
159
+ ? { meshes, idOffset: models.get(sourceModelId!)?.idOffset ?? 0 }
160
+ : undefined;
161
+ return canGenerateScheduleFrom(activeStore, ctx);
162
+ }, [activeStore, activeModelId, models]);
163
+
164
+ const handleSelect = (globalId: string, multi: boolean) => {
165
+ const current = new Set(selectedTaskGlobalIds);
166
+ if (multi) {
167
+ // Ctrl/Shift-click — toggle membership of the clicked row.
168
+ if (current.has(globalId)) current.delete(globalId);
169
+ else current.add(globalId);
170
+ } else {
171
+ // Plain click — toggle if it's the ONLY selected row (click again to
172
+ // deselect), otherwise replace the selection. This is what users
173
+ // expect from file-manager-style rows: one click selects, same click
174
+ // again clears.
175
+ const isSoleSelection = current.size === 1 && current.has(globalId);
176
+ if (isSoleSelection) {
177
+ current.clear();
178
+ } else {
179
+ current.clear();
180
+ current.add(globalId);
181
+ }
182
+ }
183
+ setSelectedTaskGlobalIds(Array.from(current));
184
+ };
185
+
186
+ /**
187
+ * Empty-space click (task-tree background, timeline background) clears
188
+ * the current Gantt selection. Matches the deselect ergonomics of every
189
+ * other list widget and gives the user a predictable "out" that doesn't
190
+ * require hunting the same row again.
191
+ */
192
+ const handleBackgroundClick = () => {
193
+ if (selectedTaskGlobalIds.size > 0) setSelectedTaskGlobalIds([]);
194
+ };
195
+
196
+ const showEmpty = !scheduleData || !scheduleRange || rows.length === 0;
197
+
198
+ // Keyboard shortcuts for schedule undo/redo — active only while the
199
+ // Gantt panel (or a descendant) has focus, so the shortcut doesn't
200
+ // steal Ctrl+Z from the script editor / text inputs elsewhere.
201
+ const onPanelKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
202
+ const mod = e.ctrlKey || e.metaKey;
203
+ if (!mod) return;
204
+ // Ignore when the user is typing into an input/textarea — the
205
+ // browser's own undo history is usually what they want there.
206
+ const target = e.target as HTMLElement | null;
207
+ const tag = target?.tagName;
208
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || target?.isContentEditable) return;
209
+
210
+ if (e.key === 'z' || e.key === 'Z') {
211
+ e.preventDefault();
212
+ if (e.shiftKey) useViewerStore.getState().redoScheduleEdit();
213
+ else useViewerStore.getState().undoScheduleEdit();
214
+ } else if (e.key === 'y' || e.key === 'Y') {
215
+ e.preventDefault();
216
+ useViewerStore.getState().redoScheduleEdit();
217
+ }
218
+ };
219
+
220
+ return (
221
+ <div
222
+ className="h-full w-full flex flex-col overflow-hidden bg-background outline-none"
223
+ tabIndex={-1}
224
+ onKeyDown={onPanelKeyDown}
225
+ >
226
+ <GanttToolbar
227
+ onClose={onClose}
228
+ onOpenGenerate={() => setGenerateOpen(true)}
229
+ canGenerate={canGenerate}
230
+ />
231
+
232
+ <GenerateScheduleDialog open={generateOpen} onOpenChange={setGenerateOpen} />
233
+
234
+ {showEmpty ? (
235
+ <GanttEmptyState
236
+ loading={loading}
237
+ hasModel={!!ifcDataStore || models.size > 0}
238
+ canGenerate={canGenerate}
239
+ extractionError={extractionError}
240
+ onGenerate={() => setGenerateOpen(true)}
241
+ onClose={onClose}
242
+ />
243
+ ) : (
244
+ <div className="flex-1 min-h-0 flex">
245
+ <div
246
+ ref={leftRef}
247
+ style={{ width: LEFT_PANE_WIDTH, flex: `0 0 ${LEFT_PANE_WIDTH}px` }}
248
+ className="relative"
249
+ >
250
+ <GanttTaskTree
251
+ rows={rows}
252
+ selectedGlobalIds={selectedTaskGlobalIds}
253
+ hoveredGlobalId={hoveredTaskGlobalId}
254
+ onToggleExpand={toggleTaskExpanded}
255
+ onSelect={handleSelect}
256
+ onBackgroundClick={handleBackgroundClick}
257
+ onReorder={(sourceGid, targetIdx) => {
258
+ // The target index is the flattened-rows position of the
259
+ // drop target. Map to the underlying tasks-array position
260
+ // via the row's globalId. With a single-level tree this
261
+ // is 1:1; nested children align because `rows` is a flat
262
+ // pre-order traversal.
263
+ const targetGid = rows[targetIdx]?.task.globalId;
264
+ if (!targetGid) return;
265
+ const store = useViewerStore.getState();
266
+ const allTasks = store.scheduleData?.tasks ?? [];
267
+ const newIdx = allTasks.findIndex(t => t.globalId === targetGid);
268
+ if (newIdx >= 0) store.moveTask(sourceGid, newIdx);
269
+ }}
270
+ onHover={setHoveredTaskGlobalId}
271
+ scrollTop={scrollTop}
272
+ onScroll={setScrollTop}
273
+ />
274
+ </div>
275
+ <div className="flex-1 min-w-0">
276
+ <GanttTimeline
277
+ rows={rows}
278
+ data={scheduleData}
279
+ range={scheduleRange}
280
+ scale={ganttTimeScale}
281
+ playbackTime={playbackTime}
282
+ selectedGlobalIds={selectedTaskGlobalIds}
283
+ hoveredGlobalId={hoveredTaskGlobalId}
284
+ onSelect={handleSelect}
285
+ onHover={setHoveredTaskGlobalId}
286
+ onScrubSeek={seekSchedule}
287
+ scrollTop={scrollTop}
288
+ onScroll={setScrollTop}
289
+ />
290
+ </div>
291
+ </div>
292
+ )}
293
+ </div>
294
+ );
295
+ }
@@ -0,0 +1,199 @@
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
+ * GanttTaskBar — renders a single Gantt row's bar (or diamond for milestones)
7
+ * plus its drag hit-zones and completion overlay. Extracted from
8
+ * GanttTimeline so the orchestrator there stays focused on layout / ticks /
9
+ * cursor / arrows.
10
+ *
11
+ * Memoized on its own props so panning the playback cursor across a
12
+ * schedule with hundreds of rows doesn't re-diff every bar every frame.
13
+ */
14
+
15
+ import { memo } from 'react';
16
+ import type { ScheduleTaskInfo } from '@ifc-lite/parser';
17
+ import { timeToX, formatDateTime } from './schedule-utils';
18
+ import { GANTT_ROW_HEIGHT } from './GanttTaskTree';
19
+
20
+ export interface GanttTaskBarProps {
21
+ task: ScheduleTaskInfo;
22
+ rowIndex: number;
23
+ /** Task start as epoch ms (already parsed by the parent's taskEpochs map). */
24
+ start: number;
25
+ /** Task finish as epoch ms. */
26
+ finish: number;
27
+ rangeStart: number;
28
+ rangeEnd: number;
29
+ pixelWidth: number;
30
+ playbackTime: number;
31
+ isSelected: boolean;
32
+ isDragging: boolean;
33
+ onHover: (globalId: string | null) => void;
34
+ onSelect: (globalId: string, multi: boolean) => void;
35
+ onPointerDown: (
36
+ e: React.PointerEvent<SVGElement>,
37
+ taskGlobalId: string,
38
+ mode: 'shift' | 'resize-start' | 'resize-finish',
39
+ ) => void;
40
+ }
41
+
42
+ export const GanttTaskBar = memo(function GanttTaskBar({
43
+ task,
44
+ rowIndex,
45
+ start,
46
+ finish,
47
+ rangeStart,
48
+ rangeEnd,
49
+ pixelWidth,
50
+ playbackTime,
51
+ isSelected,
52
+ isDragging,
53
+ onHover,
54
+ onSelect,
55
+ onPointerDown,
56
+ }: GanttTaskBarProps) {
57
+ const y = rowIndex * GANTT_ROW_HEIGHT;
58
+ const barX = timeToX(start, rangeStart, rangeEnd, pixelWidth);
59
+ const barX2 = timeToX(finish, rangeStart, rangeEnd, pixelWidth);
60
+ const barWidth = Math.max(task.isMilestone ? 0 : 2, barX2 - barX);
61
+
62
+ const isActive = playbackTime >= start && playbackTime <= finish;
63
+ const isDone = playbackTime > finish;
64
+ const isPending = !isActive && !isDone;
65
+ const isCritical = task.taskTime?.isCritical ?? false;
66
+
67
+ if (task.isMilestone) {
68
+ const cx = barX;
69
+ const cy = y + GANTT_ROW_HEIGHT / 2;
70
+ const s = 6;
71
+ return (
72
+ <g
73
+ onMouseEnter={() => onHover(task.globalId)}
74
+ onMouseLeave={() => onHover(null)}
75
+ onClick={(e) => {
76
+ e.stopPropagation();
77
+ onSelect(task.globalId, e.shiftKey || e.ctrlKey || e.metaKey);
78
+ }}
79
+ className="cursor-pointer"
80
+ >
81
+ <polygon
82
+ points={`${cx},${cy - s} ${cx + s},${cy} ${cx},${cy + s} ${cx - s},${cy}`}
83
+ fill={isDone ? '#f59e0b' : isActive ? '#fbbf24' : '#94a3b8'}
84
+ stroke={isSelected ? '#111827' : '#713f12'}
85
+ strokeWidth={isSelected ? 1.5 : 1}
86
+ />
87
+ <title>
88
+ {task.name || task.globalId}
89
+ {'\n'}
90
+ {formatDateTime(start)}
91
+ </title>
92
+ </g>
93
+ );
94
+ }
95
+
96
+ // Edge hit zones for resize. Minimum 4 px wide so we stay
97
+ // clickable even on very short bars; capped at 25 % of the
98
+ // bar width so on bars < 20 px the whole bar becomes a shift
99
+ // zone (you can still resize via the Inspector).
100
+ const edgeZone = Math.min(8, Math.max(4, Math.floor(barWidth * 0.25)));
101
+ const showEdgeHandles = barWidth >= edgeZone * 2 + 4;
102
+ const barTop = y + 6;
103
+ const barH = GANTT_ROW_HEIGHT - 12;
104
+
105
+ return (
106
+ <g
107
+ onMouseEnter={() => onHover(task.globalId)}
108
+ onMouseLeave={() => onHover(null)}
109
+ onClick={(e) => {
110
+ e.stopPropagation();
111
+ onSelect(task.globalId, e.shiftKey || e.ctrlKey || e.metaKey);
112
+ }}
113
+ >
114
+ <rect
115
+ x={barX}
116
+ y={barTop}
117
+ width={Math.max(2, barWidth)}
118
+ height={barH}
119
+ rx={3}
120
+ ry={3}
121
+ fill={
122
+ isCritical
123
+ ? isDone
124
+ ? '#dc2626'
125
+ : isActive
126
+ ? '#ef4444'
127
+ : '#7f1d1d'
128
+ : isDone
129
+ ? '#6366f1'
130
+ : isActive
131
+ ? '#818cf8'
132
+ : '#c7d2fe'
133
+ }
134
+ fillOpacity={isPending ? 0.55 : 0.95}
135
+ stroke={isDragging ? '#0ea5e9' : isSelected ? '#111827' : 'transparent'}
136
+ strokeWidth={isDragging ? 2 : isSelected ? 1.5 : 0}
137
+ />
138
+ {task.taskTime?.completion !== undefined && (
139
+ <rect
140
+ x={barX}
141
+ y={barTop}
142
+ width={Math.max(0, barWidth) * Math.min(1, Math.max(0, task.taskTime.completion / 100))}
143
+ height={barH}
144
+ rx={3}
145
+ ry={3}
146
+ fill="#111827"
147
+ fillOpacity={0.28}
148
+ pointerEvents="none"
149
+ />
150
+ )}
151
+ {/* Shift hit-zone: the interior of the bar. Draws no fill
152
+ (the visible fill rect above handles that) but owns the
153
+ pointer events that map to drag-body. */}
154
+ <rect
155
+ x={showEdgeHandles ? barX + edgeZone : barX}
156
+ y={barTop}
157
+ width={
158
+ showEdgeHandles
159
+ ? Math.max(1, barWidth - edgeZone * 2)
160
+ : Math.max(2, barWidth)
161
+ }
162
+ height={barH}
163
+ fill="transparent"
164
+ className="cursor-move"
165
+ onPointerDown={(e) => onPointerDown(e, task.globalId, 'shift')}
166
+ />
167
+ {/* Edge resize hit-zones. Only render when the bar is wide
168
+ enough for separate zones — otherwise the whole bar is
169
+ a shift zone and resize goes through the Inspector. */}
170
+ {showEdgeHandles && (
171
+ <>
172
+ <rect
173
+ x={barX}
174
+ y={barTop}
175
+ width={edgeZone}
176
+ height={barH}
177
+ fill="transparent"
178
+ className="cursor-ew-resize"
179
+ onPointerDown={(e) => onPointerDown(e, task.globalId, 'resize-start')}
180
+ />
181
+ <rect
182
+ x={barX + barWidth - edgeZone}
183
+ y={barTop}
184
+ width={edgeZone}
185
+ height={barH}
186
+ fill="transparent"
187
+ className="cursor-ew-resize"
188
+ onPointerDown={(e) => onPointerDown(e, task.globalId, 'resize-finish')}
189
+ />
190
+ </>
191
+ )}
192
+ <title>
193
+ {task.name || task.globalId}
194
+ {'\n'}
195
+ {formatDateTime(start)} → {formatDateTime(finish)}
196
+ </title>
197
+ </g>
198
+ );
199
+ });