@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,392 @@
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
+ * GenerateScheduleDialog — spawn an IFC 4D schedule from the model's spatial
7
+ * hierarchy in a few clicks.
8
+ *
9
+ * Progressive disclosure: the primary flow (strategy / start / duration /
10
+ * order) is always visible; lag, schedule name, PredefinedType, and the
11
+ * link-sequence / skip-empty toggles hide behind "Advanced".
12
+ *
13
+ * Writes the generated schedule into the viewer store via `setScheduleData`,
14
+ * which is the same path the 4D Gantt and playback loop already read from.
15
+ */
16
+
17
+ import { useEffect, useMemo, useState, useCallback } from 'react';
18
+ import { CalendarPlus, Layers, Building2, Ruler, AlertTriangle, Loader2 } from 'lucide-react';
19
+ import {
20
+ Dialog,
21
+ DialogContent,
22
+ DialogDescription,
23
+ DialogFooter,
24
+ DialogHeader,
25
+ DialogTitle,
26
+ } from '@/components/ui/dialog';
27
+ import { Button } from '@/components/ui/button';
28
+ import { Input } from '@/components/ui/input';
29
+ import { Label } from '@/components/ui/label';
30
+ import { useViewerStore } from '@/store';
31
+ import { resolveScheduleSourceModelId } from '@/store/slices/schedule-edit-helpers';
32
+ import { useIfc } from '@/hooks/useIfc';
33
+ import { serializeScheduleToStep } from '@ifc-lite/parser';
34
+ import {
35
+ generateScheduleFromSpatialHierarchy,
36
+ canGenerateScheduleFrom,
37
+ defaultStartDate,
38
+ resolveActiveDataStore,
39
+ DEFAULT_OPTIONS,
40
+ type GenerateScheduleOptions,
41
+ type GenerateOrder,
42
+ } from './generate-schedule';
43
+ import { formatDateTime } from './schedule-utils';
44
+ import { HeightStrategyPanel } from './HeightStrategyPanel';
45
+ import { GenerateAdvancedPanel } from './GenerateAdvancedPanel';
46
+
47
+ interface GenerateScheduleDialogProps {
48
+ open: boolean;
49
+ onOpenChange: (next: boolean) => void;
50
+ }
51
+
52
+ export function GenerateScheduleDialog({ open, onOpenChange }: GenerateScheduleDialogProps) {
53
+ const { ifcDataStore, models, activeModelId } = useIfc();
54
+ const commitGeneratedSchedule = useViewerStore(s => s.commitGeneratedSchedule);
55
+ const setGanttPanelVisible = useViewerStore(s => s.setGanttPanelVisible);
56
+ const setAnimationEnabled = useViewerStore(s => s.setAnimationEnabled);
57
+
58
+ // Resolve the store to read from in federation-aware order. See
59
+ // `resolveActiveDataStore` in GanttPanel for the shared rationale.
60
+ const activeStore = resolveActiveDataStore(ifcDataStore, activeModelId, models);
61
+
62
+ // Resolve the source-model's geometry context. The `IfcElement` strategy
63
+ // needs `meshes` + `idOffset` to compute each element's true Z elevation;
64
+ // the spatial strategies don't touch geometry.
65
+ const modelContext = useMemo(() => {
66
+ const sourceModelId = resolveScheduleSourceModelId(models, activeModelId);
67
+ if (!sourceModelId) return null;
68
+ const model = models.get(sourceModelId);
69
+ const meshes = model?.geometryResult?.meshes;
70
+ if (!meshes || meshes.length === 0) return null;
71
+ return { meshes, idOffset: model?.idOffset ?? 0 };
72
+ }, [models, activeModelId]);
73
+
74
+ const hasSpatial = canGenerateScheduleFrom(activeStore);
75
+ const hasGeometry = !!modelContext;
76
+ const canGenerate = hasSpatial || hasGeometry;
77
+
78
+ const [options, setOptions] = useState<GenerateScheduleOptions>(DEFAULT_OPTIONS);
79
+ const [advancedOpen, setAdvancedOpen] = useState(false);
80
+ const [submitting, setSubmitting] = useState(false);
81
+
82
+ // Reset form state on every (re)open so users can reuse the dialog.
83
+ useEffect(() => {
84
+ if (open) {
85
+ // Compute a fresh start date on each open so re-opening the dialog
86
+ // reflects "today" — `DEFAULT_OPTIONS.startDate` is evaluated at module
87
+ // load and goes stale in long-running sessions.
88
+ setOptions({ ...DEFAULT_OPTIONS, startDate: defaultStartDate() });
89
+ setAdvancedOpen(false);
90
+ setSubmitting(false);
91
+ }
92
+ }, [open]);
93
+
94
+ // If the only available source is geometry (no spatial hierarchy),
95
+ // auto-switch the strategy to `IfcElement` so the preview isn't empty.
96
+ useEffect(() => {
97
+ if (!open) return;
98
+ if (!hasSpatial && hasGeometry && options.strategy !== 'IfcElement') {
99
+ setOptions(prev => ({ ...prev, strategy: 'IfcElement' }));
100
+ }
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, [open, hasSpatial, hasGeometry]);
103
+
104
+ // Live preview — runs on every option change. The helper is pure and cheap
105
+ // enough (O(vertex count) for the Z strategy; O(storeys × products) for
106
+ // the others) that we don't debounce.
107
+ const preview = useMemo(() => {
108
+ if (!canGenerate) return null;
109
+ return generateScheduleFromSpatialHierarchy(activeStore, options, modelContext);
110
+ }, [activeStore, canGenerate, modelContext, options]);
111
+
112
+ const canSubmit = !!preview && !preview.empty && preview.groupCount > 0 && !submitting;
113
+
114
+ const handleChange = useCallback(<K extends keyof GenerateScheduleOptions>(
115
+ key: K,
116
+ value: GenerateScheduleOptions[K],
117
+ ) => {
118
+ setOptions(prev => ({ ...prev, [key]: value }));
119
+ }, []);
120
+
121
+ const handleGenerate = useCallback(() => {
122
+ if (!preview || preview.empty) return;
123
+ setSubmitting(true);
124
+
125
+ // DEBUG: full inspection of what's being added to the model. Dumps the
126
+ // extraction (tasks + work schedules + sequences) *and* the STEP lines
127
+ // the serializer will emit when the file is exported. Safe to keep —
128
+ // runs only on user-initiated generation and only logs to console.
129
+ try {
130
+ const extraction = preview.extraction;
131
+ const stepPreview = serializeScheduleToStep(extraction, {
132
+ // These IDs don't matter for inspection — the export adapter
133
+ // remaps them to the host file's ID space at injection time.
134
+ nextId: 1_000_000,
135
+ });
136
+ /* eslint-disable no-console */
137
+ console.groupCollapsed(
138
+ `%c[IfcTask] Generated schedule — ${extraction.tasks.length} task(s), ${stepPreview.lines.length} STEP line(s)`,
139
+ 'color:#6ea2ff;font-weight:bold',
140
+ );
141
+ console.log('options', options);
142
+ console.log('workSchedules', extraction.workSchedules);
143
+ console.log('tasks', extraction.tasks);
144
+ console.log('sequences', extraction.sequences);
145
+ console.log('stats', stepPreview.stats);
146
+ console.log('STEP preview (first 50 lines):');
147
+ for (const line of stepPreview.lines.slice(0, 50)) console.log(line);
148
+ if (stepPreview.lines.length > 50) {
149
+ console.log(`… ${stepPreview.lines.length - 50} more line(s). Full STEP:`);
150
+ console.log(stepPreview.lines.join('\n'));
151
+ }
152
+ console.log('raw extraction (JSON)', JSON.stringify(extraction, null, 2));
153
+ console.groupEnd();
154
+ /* eslint-enable no-console */
155
+ } catch (err) {
156
+ // eslint-disable-next-line no-console
157
+ console.warn('[IfcTask] Debug log failed (non-fatal):', err);
158
+ }
159
+
160
+ // rAF gives the button time to paint its pressed state before we swap
161
+ // the Gantt rows; cheap-but-visible feedback.
162
+ requestAnimationFrame(() => {
163
+ // Attribute the generated schedule to the currently-active model.
164
+ // Legacy single-model sessions fall back to '__legacy__' so the
165
+ // dirty flag still pairs with the viewer's model identity.
166
+ const sourceModelId = resolveScheduleSourceModelId(models, activeModelId, '__legacy__');
167
+ commitGeneratedSchedule(preview.extraction, sourceModelId);
168
+ setGanttPanelVisible(true);
169
+ setAnimationEnabled(true);
170
+ setSubmitting(false);
171
+ onOpenChange(false);
172
+ });
173
+ }, [preview, options, commitGeneratedSchedule, setGanttPanelVisible, setAnimationEnabled, onOpenChange, activeModelId, models]);
174
+
175
+ return (
176
+ <Dialog open={open} onOpenChange={onOpenChange}>
177
+ <DialogContent className="max-w-xl">
178
+ <DialogHeader>
179
+ <DialogTitle className="flex items-center gap-2">
180
+ <CalendarPlus className="h-5 w-5 text-primary" />
181
+ Generate schedule
182
+ </DialogTitle>
183
+ <DialogDescription>
184
+ Creates a work schedule with one task per group and assigns every
185
+ product in that group to the task, so the 4D Gantt animation can
186
+ reveal them as time advances.
187
+ </DialogDescription>
188
+ </DialogHeader>
189
+
190
+ {!canGenerate ? (
191
+ <div className="flex items-start gap-3 rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
192
+ <AlertTriangle className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
193
+ <div>
194
+ <p className="font-medium text-foreground">Nothing to group by</p>
195
+ <p className="text-muted-foreground">
196
+ The loaded model has neither a spatial hierarchy nor visible
197
+ geometry. Load an IFC with IfcBuildingStorey/IfcBuilding
198
+ containers or meshed elements and try again.
199
+ </p>
200
+ </div>
201
+ </div>
202
+ ) : (
203
+ <div className="grid gap-4">
204
+ {/* Strategy — three tiles. Height is the rescue for models with
205
+ broken spatial hierarchies; sub-options reveal only when it's
206
+ active, keeping the dialog uncluttered for the common case. */}
207
+ <div className="grid gap-2">
208
+ <Label>Group by</Label>
209
+ <div className="grid grid-cols-3 gap-2">
210
+ <StrategyChoice
211
+ icon={<Layers className="h-4 w-4" />}
212
+ label="Storey"
213
+ description="Per IfcBuildingStorey"
214
+ active={options.strategy === 'IfcBuildingStorey'}
215
+ disabled={!hasSpatial}
216
+ onSelect={() => handleChange('strategy', 'IfcBuildingStorey')}
217
+ />
218
+ <StrategyChoice
219
+ icon={<Building2 className="h-4 w-4" />}
220
+ label="Building"
221
+ description="Per IfcBuilding"
222
+ active={options.strategy === 'IfcBuilding'}
223
+ disabled={!hasSpatial}
224
+ onSelect={() => handleChange('strategy', 'IfcBuilding')}
225
+ />
226
+ <StrategyChoice
227
+ icon={<Ruler className="h-4 w-4" />}
228
+ label="Height"
229
+ description="Slice by element Z"
230
+ active={options.strategy === 'IfcElement'}
231
+ disabled={!hasGeometry}
232
+ onSelect={() => handleChange('strategy', 'IfcElement')}
233
+ />
234
+ </div>
235
+ {options.strategy !== 'IfcElement' && !hasSpatial && (
236
+ <p className="text-[11px] text-muted-foreground">
237
+ Spatial hierarchy missing — only Height is available for this model.
238
+ </p>
239
+ )}
240
+ </div>
241
+
242
+ {/* Height sub-panel — only when the IfcElement strategy is active.
243
+ Reads as "settings for the selected group-by". */}
244
+ {options.strategy === 'IfcElement' && (
245
+ <HeightStrategyPanel
246
+ heightTolerance={options.heightTolerance}
247
+ elementZSubgroup={options.elementZSubgroup}
248
+ onHeightToleranceChange={(n) => handleChange('heightTolerance', n)}
249
+ onSubgroupChange={(s) => handleChange('elementZSubgroup', s)}
250
+ />
251
+ )}
252
+
253
+ {/* Primary fields */}
254
+ <div className="grid grid-cols-2 gap-3">
255
+ <div className="grid gap-1.5">
256
+ <Label htmlFor="gen-start">Start date</Label>
257
+ <Input
258
+ id="gen-start"
259
+ type="datetime-local"
260
+ value={options.startDate.slice(0, 16)}
261
+ onChange={(e) => {
262
+ const v = e.target.value;
263
+ handleChange('startDate', v ? `${v}:00` : DEFAULT_OPTIONS.startDate);
264
+ }}
265
+ />
266
+ </div>
267
+ <div className="grid gap-1.5">
268
+ <Label htmlFor="gen-duration">Days per group</Label>
269
+ <Input
270
+ id="gen-duration"
271
+ type="number"
272
+ min={0.5}
273
+ step={0.5}
274
+ value={options.daysPerGroup}
275
+ onChange={(e) => {
276
+ const v = parseFloat(e.target.value);
277
+ handleChange('daysPerGroup', Number.isFinite(v) && v > 0 ? v : 1);
278
+ }}
279
+ />
280
+ </div>
281
+ </div>
282
+
283
+ {/* Order */}
284
+ <div className="grid gap-2">
285
+ <Label>Order</Label>
286
+ <div className="grid grid-cols-2 gap-2">
287
+ <StrategyChoice
288
+ icon={<span className="text-xs font-semibold">↑</span>}
289
+ label="Bottom-up"
290
+ description="Site → ground → upper floors"
291
+ active={options.order === 'bottom-up'}
292
+ onSelect={() => handleChange('order', 'bottom-up' satisfies GenerateOrder)}
293
+ />
294
+ <StrategyChoice
295
+ icon={<span className="text-xs font-semibold">↓</span>}
296
+ label="Top-down"
297
+ description="Roof → upper floors → ground"
298
+ active={options.order === 'top-down'}
299
+ onSelect={() => handleChange('order', 'top-down' satisfies GenerateOrder)}
300
+ />
301
+ </div>
302
+ </div>
303
+
304
+ <GenerateAdvancedPanel
305
+ open={advancedOpen}
306
+ onOpenChange={setAdvancedOpen}
307
+ strategy={options.strategy}
308
+ lagDays={options.lagDays}
309
+ predefinedType={options.predefinedType}
310
+ scheduleName={options.scheduleName}
311
+ linkSequences={options.linkSequences}
312
+ skipEmptyGroups={options.skipEmptyGroups}
313
+ onChange={handleChange}
314
+ />
315
+
316
+ {/* Live summary */}
317
+ <div className="rounded-md bg-muted/30 p-3 text-sm">
318
+ {preview && !preview.empty ? (
319
+ <div className="grid gap-1">
320
+ <div className="flex items-baseline justify-between">
321
+ <span className="font-medium">Summary</span>
322
+ <span className="text-xs text-muted-foreground">Generated locally — not written to IFC</span>
323
+ </div>
324
+ <p>
325
+ <span className="font-semibold">{preview.groupCount}</span> tasks ·{' '}
326
+ <span className="font-semibold">{preview.productCount}</span> products ·{' '}
327
+ finishes <span className="font-mono">{formatDateTime(new Date(preview.finishDate).getTime())}</span>
328
+ </p>
329
+ {preview.groupCount > 0 && (
330
+ <p className="text-xs text-muted-foreground">
331
+ First task: <span className="font-medium">{preview.extraction.tasks[0]?.name}</span>
332
+ {preview.groupCount > 1 && <> · last: <span className="font-medium">{preview.extraction.tasks.at(-1)?.name}</span></>}
333
+ </p>
334
+ )}
335
+ </div>
336
+ ) : (
337
+ <p className="text-muted-foreground">
338
+ No groups match the current options — tweak the strategy or disable
339
+ &quot;Skip empty groups&quot;.
340
+ </p>
341
+ )}
342
+ </div>
343
+ </div>
344
+ )}
345
+
346
+ <DialogFooter>
347
+ <Button variant="ghost" onClick={() => onOpenChange(false)}>Cancel</Button>
348
+ <Button onClick={handleGenerate} disabled={!canSubmit}>
349
+ {submitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <CalendarPlus className="h-4 w-4 mr-2" />}
350
+ Generate schedule
351
+ </Button>
352
+ </DialogFooter>
353
+ </DialogContent>
354
+ </Dialog>
355
+ );
356
+ }
357
+
358
+ interface StrategyChoiceProps {
359
+ icon: React.ReactNode;
360
+ label: string;
361
+ description: string;
362
+ active: boolean;
363
+ /** When true the tile is unavailable (greyed out, not clickable). */
364
+ disabled?: boolean;
365
+ onSelect: () => void;
366
+ }
367
+
368
+ function StrategyChoice({ icon, label, description, active, disabled, onSelect }: StrategyChoiceProps) {
369
+ const base = 'flex items-start gap-2 rounded-md border p-2.5 text-left transition-colors';
370
+ const state = active
371
+ ? 'border-primary bg-primary/5 text-foreground'
372
+ : disabled
373
+ ? 'border-dashed border-input/60 bg-muted/20 text-muted-foreground cursor-not-allowed opacity-60'
374
+ : 'border-input hover:bg-muted/40 text-foreground';
375
+ return (
376
+ <button
377
+ type="button"
378
+ onClick={disabled ? undefined : onSelect}
379
+ className={`${base} ${state}`}
380
+ aria-pressed={active}
381
+ disabled={disabled}
382
+ title={disabled ? 'Not available for this model' : undefined}
383
+ >
384
+ <span className={'mt-0.5 ' + (active ? 'text-primary' : 'text-muted-foreground')}>{icon}</span>
385
+ <span className="grid gap-0.5">
386
+ <span className="text-sm font-medium">{label}</span>
387
+ <span className="text-xs text-muted-foreground">{description}</span>
388
+ </span>
389
+ </button>
390
+ );
391
+ }
392
+
@@ -0,0 +1,120 @@
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
+ * HeightStrategyPanel — sub-panel shown when the Generate dialog's Height
7
+ * strategy is selected. Exposes slice-height + subgroup mode. Extracted so
8
+ * the parent dialog file focuses on strategy selection, primary fields,
9
+ * and the preview.
10
+ */
11
+
12
+ import { Ruler } from 'lucide-react';
13
+ import { Label } from '@/components/ui/label';
14
+ import type { GenerateScheduleOptions } from './generate-schedule';
15
+
16
+ export interface HeightStrategyPanelProps {
17
+ heightTolerance: number;
18
+ elementZSubgroup: GenerateScheduleOptions['elementZSubgroup'];
19
+ onHeightToleranceChange: (next: number) => void;
20
+ onSubgroupChange: (next: GenerateScheduleOptions['elementZSubgroup']) => void;
21
+ }
22
+
23
+ export function HeightStrategyPanel({
24
+ heightTolerance,
25
+ elementZSubgroup,
26
+ onHeightToleranceChange,
27
+ onSubgroupChange,
28
+ }: HeightStrategyPanelProps) {
29
+ return (
30
+ <div className="rounded-md border border-primary/30 bg-primary/5 p-3 grid gap-3">
31
+ <div className="flex items-center gap-2">
32
+ <Ruler className="h-3.5 w-3.5 text-primary" />
33
+ <span className="text-xs font-medium">Height-slice options</span>
34
+ <span className="ml-auto text-[10px] text-muted-foreground">
35
+ Uses geometry, ignores spatial tree
36
+ </span>
37
+ </div>
38
+
39
+ <div className="grid gap-1.5">
40
+ <div className="flex items-center justify-between">
41
+ <Label htmlFor="gen-tol" className="text-xs">Slice height</Label>
42
+ <span className="text-xs font-mono text-muted-foreground">
43
+ {heightTolerance.toFixed(1)} m
44
+ </span>
45
+ </div>
46
+ <input
47
+ id="gen-tol"
48
+ type="range"
49
+ min={0.5}
50
+ max={10}
51
+ step={0.25}
52
+ value={heightTolerance}
53
+ onChange={(e) => onHeightToleranceChange(parseFloat(e.target.value))}
54
+ className="w-full accent-primary"
55
+ />
56
+ <p className="text-[10px] text-muted-foreground">
57
+ Elements whose geometry centroid Z falls inside the same
58
+ band share a task. Typical storey heights are 3–4 m.
59
+ </p>
60
+ </div>
61
+
62
+ <div className="grid gap-1.5">
63
+ <Label className="text-xs">Subdivide each slice</Label>
64
+ <div className="grid grid-cols-4 gap-1.5">
65
+ {([
66
+ { k: 'none', label: 'None' },
67
+ { k: 'class', label: 'Class' },
68
+ { k: 'type', label: 'Type' },
69
+ { k: 'name', label: 'Name' },
70
+ ] as const).map(opt => (
71
+ <SubgroupPill
72
+ key={opt.k}
73
+ label={opt.label}
74
+ active={elementZSubgroup === opt.k}
75
+ onSelect={() => onSubgroupChange(opt.k)}
76
+ />
77
+ ))}
78
+ </div>
79
+ <p className="text-[10px] text-muted-foreground">
80
+ {elementZSubgroup === 'none'
81
+ ? 'One task per slice — every element in the band goes to that task.'
82
+ : elementZSubgroup === 'class'
83
+ ? 'Split each slice by IFC class (IfcWall, IfcSlab, …).'
84
+ : elementZSubgroup === 'type'
85
+ ? 'Split each slice by the element’s type name (IfcRelDefinesByType target).'
86
+ : 'Split each slice by each element’s Name attribute.'}
87
+ </p>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ interface SubgroupPillProps {
94
+ label: string;
95
+ active: boolean;
96
+ onSelect: () => void;
97
+ }
98
+
99
+ /**
100
+ * Compact 4-across segmented pill used for the Z-subgroup mode
101
+ * (None / Class / Type / Name). Kept small and modest so it reads as a
102
+ * setting, not a navigation target.
103
+ */
104
+ function SubgroupPill({ label, active, onSelect }: SubgroupPillProps) {
105
+ return (
106
+ <button
107
+ type="button"
108
+ onClick={onSelect}
109
+ aria-pressed={active}
110
+ className={
111
+ 'rounded border px-2 py-1 text-xs transition-colors ' +
112
+ (active
113
+ ? 'border-primary bg-primary/10 text-primary font-medium'
114
+ : 'border-input text-muted-foreground hover:bg-muted/40 hover:text-foreground')
115
+ }
116
+ >
117
+ {label}
118
+ </button>
119
+ );
120
+ }