@ifc-lite/viewer 1.17.6 → 1.19.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 -15
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +949 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-RZy5c3Td.js} +1 -1
- package/dist/assets/decode-worker-Collf_X_.js +1320 -0
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-BraHBeoi.js} +4194 -3025
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-DQEZB2rB.js} +1 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +1 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BOi3BuUI.js} +46423 -31181
- package/dist/assets/index-XwKzDuw6.js +22 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-CpBeOPQa.js} +1 -1
- package/dist/assets/sandbox-Baez7n-t.js +9682 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-BB6cMAXE.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-CAYCUHbE.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +11 -10
- 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/ChatPanel.tsx +64 -2
- 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 -12
- package/src/components/viewer/PointCloudPanel.tsx +174 -0
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- 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/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +29 -2
- package/src/components/viewer/ViewportContainer.tsx +45 -5
- package/src/components/viewer/ViewportOverlays.tsx +13 -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 +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- 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 +581 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/usePointCloudLifecycle.ts +64 -0
- package/src/components/viewer/usePointCloudSync.ts +98 -0
- package/src/hooks/ingest/pointCloudIngest.ts +391 -0
- package/src/hooks/ingest/viewerModelIngest.ts +32 -3
- package/src/hooks/useIfcFederation.ts +72 -3
- package/src/hooks/useIfcLoader.ts +89 -13
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -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 +6 -0
- 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/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/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +8 -3
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +79 -1
- 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/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/pointCloudSlice.ts +102 -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/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/types.ts +7 -0
- package/src/store.ts +14 -0
- package/vite.config.ts +1 -0
- package/dist/assets/ifc-lite_bg-BINvzoCP.wasm +0 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
|
@@ -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
|
+
"Skip empty groups".
|
|
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
|
+
}
|