@ifc-lite/viewer 1.17.4 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -0,0 +1,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
|
+
}
|