@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,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 { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import {
|
|
8
|
+
parseIsoDate,
|
|
9
|
+
msToIsoDuration,
|
|
10
|
+
addIsoDurationToEpoch,
|
|
11
|
+
toIsoUtc,
|
|
12
|
+
reconcileTaskTime,
|
|
13
|
+
cloneExtraction,
|
|
14
|
+
} from './schedule-edit-helpers.js';
|
|
15
|
+
|
|
16
|
+
describe('schedule-edit-helpers — ISO 8601 date+duration round-trip', () => {
|
|
17
|
+
it('parseIsoDate normalises tz-less inputs to UTC', () => {
|
|
18
|
+
// Regression: before this normalization, opening the same IFC on
|
|
19
|
+
// machines in different timezones produced different epoch values,
|
|
20
|
+
// shifting the Gantt and breaking STEP round-trip equality.
|
|
21
|
+
assert.strictEqual(
|
|
22
|
+
parseIsoDate('2024-05-01T08:00:00'),
|
|
23
|
+
parseIsoDate('2024-05-01T08:00:00Z'),
|
|
24
|
+
);
|
|
25
|
+
assert.strictEqual(parseIsoDate('not-a-date'), undefined);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('msToIsoDuration ↔ addIsoDurationToEpoch invert each other', () => {
|
|
29
|
+
// Property test — the two halves of the duration pipeline must be
|
|
30
|
+
// strict inverses, otherwise a task's finish = start + duration
|
|
31
|
+
// computation drifts on every round-trip.
|
|
32
|
+
const start = parseIsoDate('2024-05-01T08:00:00Z')!;
|
|
33
|
+
const deltaMs = 5 * 86_400_000 + 4 * 3_600_000;
|
|
34
|
+
const iso = msToIsoDuration(deltaMs);
|
|
35
|
+
assert.strictEqual(iso, 'P5DT4H');
|
|
36
|
+
assert.strictEqual(addIsoDurationToEpoch(start, iso)! - start, deltaMs);
|
|
37
|
+
assert.strictEqual(addIsoDurationToEpoch(0, 'NOT-A-DURATION'), undefined);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('toIsoUtc round-trips with parseIsoDate', () => {
|
|
41
|
+
const ms = parseIsoDate('2024-05-01T08:00:00Z')!;
|
|
42
|
+
assert.strictEqual(toIsoUtc(ms), '2024-05-01T08:00:00');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('schedule-edit-helpers — reconcileTaskTime', () => {
|
|
47
|
+
it('derives the missing attribute from the two supplied', () => {
|
|
48
|
+
// start + finish → duration
|
|
49
|
+
assert.strictEqual(
|
|
50
|
+
reconcileTaskTime({ scheduleStart: '2024-05-01T08:00:00Z', scheduleFinish: '2024-05-03T08:00:00Z' })
|
|
51
|
+
?.scheduleDuration,
|
|
52
|
+
'P2D',
|
|
53
|
+
);
|
|
54
|
+
// start + duration → finish
|
|
55
|
+
assert.strictEqual(
|
|
56
|
+
reconcileTaskTime({ scheduleStart: '2024-05-01T08:00:00Z', scheduleDuration: 'P3D' })
|
|
57
|
+
?.scheduleFinish,
|
|
58
|
+
'2024-05-04T08:00:00',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects finish < start with null (caller must not commit)', () => {
|
|
63
|
+
// This is what gates the Inspector's time edit from committing a
|
|
64
|
+
// negative duration. Returning null explicitly rather than a reconciled
|
|
65
|
+
// object is the signal the caller watches for.
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
reconcileTaskTime({
|
|
68
|
+
scheduleStart: '2024-05-03T08:00:00Z',
|
|
69
|
+
scheduleFinish: '2024-05-01T08:00:00Z',
|
|
70
|
+
}),
|
|
71
|
+
null,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('schedule-edit-helpers — cloneExtraction', () => {
|
|
77
|
+
it('breaks mutable-ref aliasing so undo snapshots stay independent', () => {
|
|
78
|
+
// The undo stack depends on this: if snapshots aliased the live
|
|
79
|
+
// extraction's arrays, editing a task after a snapshot would corrupt
|
|
80
|
+
// the snapshot and undo would fail silently.
|
|
81
|
+
const src = {
|
|
82
|
+
hasSchedule: true,
|
|
83
|
+
workSchedules: [],
|
|
84
|
+
sequences: [],
|
|
85
|
+
tasks: [{
|
|
86
|
+
expressId: 1, globalId: 'a', name: 'A', isMilestone: false,
|
|
87
|
+
childGlobalIds: ['child1'], productExpressIds: [10, 20],
|
|
88
|
+
productGlobalIds: ['g1'], controllingScheduleGlobalIds: [],
|
|
89
|
+
}],
|
|
90
|
+
};
|
|
91
|
+
const clone = cloneExtraction(src as never);
|
|
92
|
+
clone.tasks[0].name = 'B';
|
|
93
|
+
clone.tasks[0].productExpressIds.push(99);
|
|
94
|
+
assert.strictEqual(src.tasks[0].name, 'A');
|
|
95
|
+
assert.deepStrictEqual(src.tasks[0].productExpressIds, [10, 20]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
* Pure helpers for the schedule edit pipeline: ISO-8601 date / duration
|
|
7
|
+
* math, deep-clone of the extraction, and federation-id helpers. Extracted
|
|
8
|
+
* from scheduleSlice.ts so the slice file focuses on state + mutators and
|
|
9
|
+
* these functions can be unit-tested in isolation.
|
|
10
|
+
*
|
|
11
|
+
* No Zustand imports here — every function is pure (or state-less enough
|
|
12
|
+
* to accept raw inputs).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ScheduleExtraction } from '@ifc-lite/parser';
|
|
16
|
+
|
|
17
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
18
|
+
// ISO-8601 date/time helpers
|
|
19
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert an ISO 8601 datetime string to epoch ms. Returns undefined when
|
|
23
|
+
* the input is missing or unparseable.
|
|
24
|
+
*
|
|
25
|
+
* `IfcDateTime` values produced by authoring tools are typically written
|
|
26
|
+
* without a timezone designator (e.g. `2024-05-01T08:00:00`). `Date.parse`
|
|
27
|
+
* treats those as *local* time, so the same IFC opened on machines in
|
|
28
|
+
* different timezones would yield different epoch values — shifting the
|
|
29
|
+
* Gantt and breaking equality with exported STEP strings. We normalize
|
|
30
|
+
* TZ-less inputs to UTC (append `Z`) so playback stays stable across
|
|
31
|
+
* machines and STEP round-trips.
|
|
32
|
+
*/
|
|
33
|
+
export function parseIsoDate(value: string | undefined): number | undefined {
|
|
34
|
+
if (!value) return undefined;
|
|
35
|
+
const hasTz = /Z$|[+-]\d{2}:?\d{2}$/.test(value);
|
|
36
|
+
const normalized = hasTz ? value : `${value}Z`;
|
|
37
|
+
const t = Date.parse(normalized);
|
|
38
|
+
return Number.isNaN(t) ? undefined : t;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Emit an ISO 8601 P…T… duration from a millisecond quantity. */
|
|
42
|
+
export function msToIsoDuration(ms: number): string {
|
|
43
|
+
const clamped = Math.max(0, Math.round(ms));
|
|
44
|
+
if (clamped === 0) return 'PT0S';
|
|
45
|
+
const days = Math.floor(clamped / 86_400_000);
|
|
46
|
+
const remAfterDays = clamped - days * 86_400_000;
|
|
47
|
+
const hours = Math.floor(remAfterDays / 3_600_000);
|
|
48
|
+
const remAfterHours = remAfterDays - hours * 3_600_000;
|
|
49
|
+
const mins = Math.floor(remAfterHours / 60_000);
|
|
50
|
+
const secs = Math.floor((remAfterHours - mins * 60_000) / 1000);
|
|
51
|
+
let out = 'P';
|
|
52
|
+
if (days > 0) out += `${days}D`;
|
|
53
|
+
if (hours > 0 || mins > 0 || secs > 0) {
|
|
54
|
+
out += 'T';
|
|
55
|
+
if (hours > 0) out += `${hours}H`;
|
|
56
|
+
if (mins > 0) out += `${mins}M`;
|
|
57
|
+
if (secs > 0) out += `${secs}S`;
|
|
58
|
+
}
|
|
59
|
+
return out === 'P' ? 'P0D' : out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function addIsoDurationToEpoch(start: number, iso: string): number | undefined {
|
|
63
|
+
const match = iso.match(
|
|
64
|
+
/^P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/,
|
|
65
|
+
);
|
|
66
|
+
if (!match) return undefined;
|
|
67
|
+
const [, y, mo, w, d, h, mi, s] = match;
|
|
68
|
+
const yearMs = 365.2425 * 86_400_000;
|
|
69
|
+
const monthMs = yearMs / 12;
|
|
70
|
+
const total =
|
|
71
|
+
(y ? parseFloat(y) * yearMs : 0) +
|
|
72
|
+
(mo ? parseFloat(mo) * monthMs : 0) +
|
|
73
|
+
(w ? parseFloat(w) * 7 * 86_400_000 : 0) +
|
|
74
|
+
(d ? parseFloat(d) * 86_400_000 : 0) +
|
|
75
|
+
(h ? parseFloat(h) * 3_600_000 : 0) +
|
|
76
|
+
(mi ? parseFloat(mi) * 60_000 : 0) +
|
|
77
|
+
(s ? parseFloat(s) * 1000 : 0);
|
|
78
|
+
return start + total;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Epoch ms → ISO-8601 UTC (no milliseconds), matching the extractor. */
|
|
82
|
+
export function toIsoUtc(ms: number): string {
|
|
83
|
+
const d = new Date(ms);
|
|
84
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
85
|
+
return (
|
|
86
|
+
`${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T` +
|
|
87
|
+
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Today at 08:00 UTC, ISO-8601 no milliseconds — a friendly default. */
|
|
92
|
+
export function isoNowAt8(): string {
|
|
93
|
+
const d = new Date();
|
|
94
|
+
d.setUTCHours(8, 0, 0, 0);
|
|
95
|
+
return toIsoUtc(d.getTime());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reconcile scheduleStart / scheduleFinish / scheduleDuration so any
|
|
100
|
+
* two-of-three the caller supplies produce a consistent third.
|
|
101
|
+
* • If start + finish supplied → derive duration from their delta.
|
|
102
|
+
* • If start + duration → derive finish.
|
|
103
|
+
* • If finish + duration (no start) → leave as-is; no start to anchor.
|
|
104
|
+
* • Otherwise return patched merge as-is.
|
|
105
|
+
* Returns null when finish < start (invalid, caller should reject).
|
|
106
|
+
*/
|
|
107
|
+
export function reconcileTaskTime(
|
|
108
|
+
merged: { scheduleStart?: string; scheduleFinish?: string; scheduleDuration?: string }
|
|
109
|
+
& Record<string, unknown>,
|
|
110
|
+
): typeof merged | null {
|
|
111
|
+
const start = parseIsoDate(merged.scheduleStart as string | undefined);
|
|
112
|
+
const finish = parseIsoDate(merged.scheduleFinish as string | undefined);
|
|
113
|
+
if (start !== undefined && finish !== undefined && finish < start) return null;
|
|
114
|
+
|
|
115
|
+
if (start !== undefined && finish !== undefined) {
|
|
116
|
+
merged.scheduleDuration = msToIsoDuration(finish - start);
|
|
117
|
+
} else if (start !== undefined && merged.scheduleDuration) {
|
|
118
|
+
const finishMs = addIsoDurationToEpoch(start, merged.scheduleDuration);
|
|
119
|
+
if (finishMs !== undefined) merged.scheduleFinish = toIsoUtc(finishMs);
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
125
|
+
// Extraction clone
|
|
126
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
127
|
+
|
|
128
|
+
/** Deep-clone an extraction so snapshots don't share mutable refs. */
|
|
129
|
+
export function cloneExtraction(src: ScheduleExtraction): ScheduleExtraction {
|
|
130
|
+
// `structuredClone` is available in every runtime we target. Falls
|
|
131
|
+
// back to JSON only if the environment is ancient — the tasks /
|
|
132
|
+
// sequences are plain data so both paths round-trip cleanly.
|
|
133
|
+
if (typeof structuredClone === 'function') return structuredClone(src);
|
|
134
|
+
return JSON.parse(JSON.stringify(src)) as ScheduleExtraction;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
138
|
+
// Federation helpers — translate renderer globals ↔ local expressIds
|
|
139
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
/** Pick the only model when running single-model; null otherwise. */
|
|
142
|
+
export function resolveSingleModelId(
|
|
143
|
+
state: { models?: Map<string, unknown> },
|
|
144
|
+
): string | null {
|
|
145
|
+
const models = state.models;
|
|
146
|
+
if (!models || models.size !== 1) return null;
|
|
147
|
+
const firstKey = models.keys().next().value;
|
|
148
|
+
return typeof firstKey === 'string' ? firstKey : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function resolveIdOffset(
|
|
152
|
+
state: { models?: Map<string, { idOffset?: number }> },
|
|
153
|
+
sourceModelId: string | null,
|
|
154
|
+
): number {
|
|
155
|
+
if (!sourceModelId) return 0;
|
|
156
|
+
return state.models?.get(sourceModelId)?.idOffset ?? 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Standard "which model does this schedule attach to?" resolution: prefer
|
|
161
|
+
* the currently-active model; fall back to the only model in single-model
|
|
162
|
+
* sessions; otherwise return `emptyFallback` (defaults to `''`).
|
|
163
|
+
*
|
|
164
|
+
* Extracted so every schedule-pipeline site uses the same rule — previously
|
|
165
|
+
* the `activeModelId ?? (models.size === 1 ? ... : '')` snippet was
|
|
166
|
+
* duplicated across 6 files, inviting drift.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveScheduleSourceModelId<M>(
|
|
169
|
+
models: ReadonlyMap<string, M>,
|
|
170
|
+
activeModelId: string | null | undefined,
|
|
171
|
+
emptyFallback: string = '',
|
|
172
|
+
): string {
|
|
173
|
+
if (activeModelId) return activeModelId;
|
|
174
|
+
if (models.size === 1) {
|
|
175
|
+
const first = models.keys().next().value;
|
|
176
|
+
return typeof first === 'string' ? first : emptyFallback;
|
|
177
|
+
}
|
|
178
|
+
return emptyFallback;
|
|
179
|
+
}
|