1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import { z } from 'zod';
10} from './scenarioSchema';
11import { generateSimId } from './scenarioStore/idUtils';
15 createUniqueScenarioCopyName,
16} from './scenarioStore/nameUtils';
24} from './scenarioStore/types';
26export const ASSET_LIBRARY_SCHEMA_VERSION = 1 as const;
28const TemplateBaseSchema = z.object({
29 schemaVersion: z.literal(ASSET_LIBRARY_SCHEMA_VERSION),
30 id: z.string().min(1),
31 name: z.string().min(1),
32 createdAt: z.string().min(1),
33 updatedAt: z.string().min(1),
36 scenarioName: z.string().optional(),
37 itemId: z.string().optional(),
42const PlatformDependenciesSchema = z.object({
43 waveforms: z.array(WaveformSchema),
44 timings: z.array(TimingSchema),
45 antennas: z.array(AntennaSchema),
48const WaveformTemplateSchema = TemplateBaseSchema.extend({
49 kind: z.literal('waveform'),
50 payload: WaveformSchema,
53const TimingTemplateSchema = TemplateBaseSchema.extend({
54 kind: z.literal('timing'),
55 payload: TimingSchema,
58const AntennaTemplateSchema = TemplateBaseSchema.extend({
59 kind: z.literal('antenna'),
60 payload: AntennaSchema,
63const PlatformTemplateSchema = TemplateBaseSchema.extend({
64 kind: z.literal('platform'),
65 payload: PlatformSchema,
66 dependencies: PlatformDependenciesSchema,
69export const AssetLibraryTemplateSchema = z.discriminatedUnion('kind', [
70 WaveformTemplateSchema,
72 AntennaTemplateSchema,
73 PlatformTemplateSchema,
76export const AssetLibraryFileSchema = z.object({
77 schemaVersion: z.literal(ASSET_LIBRARY_SCHEMA_VERSION),
78 templates: z.array(AssetLibraryTemplateSchema),
81export type AssetTemplateKind = z.infer<
82 typeof AssetLibraryTemplateSchema
84export type AssetLibraryTemplate = z.infer<typeof AssetLibraryTemplateSchema>;
85export type AssetLibraryFile = z.infer<typeof AssetLibraryFileSchema>;
87export interface AssetTemplateInsertionResult {
88 insertedItemId: string | null;
89 insertedName: string | null;
93type CreateTemplateOptions = {
98let fallbackTemplateCounter = 1;
100export function createAssetTemplateId(): string {
101 if (globalThis.crypto?.randomUUID) {
102 return globalThis.crypto.randomUUID();
105 const id = `template-${Date.now()}-${fallbackTemplateCounter}`;
106 fallbackTemplateCounter += 1;
110function deepClone<T>(value: T): T {
111 return JSON.parse(JSON.stringify(value)) as T;
114function createBaseTemplate(
115 item: Waveform | Timing | Antenna | Platform,
116 scenarioData: ScenarioData,
117 options: CreateTemplateOptions
119 const timestamp = options.timestamp ?? new Date().toISOString();
121 schemaVersion: ASSET_LIBRARY_SCHEMA_VERSION,
122 id: options.id ?? createAssetTemplateId(),
124 createdAt: timestamp,
125 updatedAt: timestamp,
127 scenarioName: scenarioData.globalParameters.simulation_name,
133function sanitizePlatform(platform: Platform): Platform {
134 const cloned = deepClone(platform);
135 delete cloned.pathPoints;
136 delete cloned.rotationPathPoints;
140function uniqueById<T extends { id: string }>(items: T[]): T[] {
141 const seen = new Set<string>();
142 return items.filter((item) => {
143 if (seen.has(item.id)) {
151function collectPlatformDependencies(
153 scenarioData: ScenarioData
155 const antennaIds = new Set<string>();
156 const waveformIds = new Set<string>();
157 const timingIds = new Set<string>();
159 for (const component of platform.components) {
160 if ('antennaId' in component && component.antennaId) {
161 antennaIds.add(component.antennaId);
163 if ('waveformId' in component && component.waveformId) {
164 waveformIds.add(component.waveformId);
166 if ('timingId' in component && component.timingId) {
167 timingIds.add(component.timingId);
172 waveforms: uniqueById(
173 scenarioData.waveforms.filter((waveform) =>
174 waveformIds.has(waveform.id)
178 scenarioData.timings.filter((timing) => timingIds.has(timing.id))
180 antennas: uniqueById(
181 scenarioData.antennas.filter((antenna) =>
182 antennaIds.has(antenna.id)
188export function createTemplateFromScenarioItem(
189 scenarioData: ScenarioData,
191 options: CreateTemplateOptions = {}
192): AssetLibraryTemplate | null {
193 const waveform = scenarioData.waveforms.find((item) => item.id === itemId);
196 ...createBaseTemplate(waveform, scenarioData, options),
198 payload: deepClone(waveform),
202 const timing = scenarioData.timings.find((item) => item.id === itemId);
205 ...createBaseTemplate(timing, scenarioData, options),
207 payload: deepClone(timing),
211 const antenna = scenarioData.antennas.find((item) => item.id === itemId);
214 ...createBaseTemplate(antenna, scenarioData, options),
216 payload: deepClone(antenna),
220 const platform = scenarioData.platforms.find((item) => item.id === itemId);
223 ...createBaseTemplate(platform, scenarioData, options),
225 payload: sanitizePlatform(platform),
226 dependencies: collectPlatformDependencies(platform, scenarioData),
233function cloneWaveform(
235 scenarioData: ScenarioData,
236 baseName = waveform.name
239 ...deepClone(waveform),
240 id: generateSimId('Waveform'),
241 name: createUniqueScenarioCopyName(scenarioData, baseName),
247 scenarioData: ScenarioData,
248 baseName = timing.name
251 ...deepClone(timing),
252 id: generateSimId('Timing'),
253 name: createUniqueScenarioCopyName(scenarioData, baseName),
254 noiseEntries: timing.noiseEntries.map((entry) => ({
256 id: generateSimId('Timing'),
261function cloneAntenna(
263 scenarioData: ScenarioData,
264 baseName = antenna.name
267 ...deepClone(antenna),
268 id: generateSimId('Antenna'),
269 name: createUniqueScenarioCopyName(scenarioData, baseName),
273function clonePlatformWaypointIds(platform: Platform): Platform {
274 const cloned = sanitizePlatform(platform);
275 cloned.id = generateSimId('Platform');
276 cloned.motionPath.waypoints = cloned.motionPath.waypoints.map(
279 id: generateSimId('Platform'),
283 if (cloned.rotation.type === 'path') {
284 cloned.rotation.waypoints = cloned.rotation.waypoints.map(
287 id: generateSimId('Platform'),
295function remapReference(
296 originalId: string | null,
297 idMap: Map<string, string>,
305 const remapped = idMap.get(originalId);
311 `Missing ${label} dependency ${originalId}; reference cleared.`
316function cloneComponent(
317 component: PlatformComponent,
318 antennaIdMap: Map<string, string>,
319 timingIdMap: Map<string, string>,
320 waveformIdMap: Map<string, string>,
322 existingNames: Set<string>
323): PlatformComponent {
324 const name = createUniqueName(component.name, existingNames, {
327 existingNames.add(name);
329 switch (component.type) {
331 const txId = generateSimId('Transmitter');
333 ...deepClone(component),
336 rxId: generateSimId('Receiver'),
338 antennaId: remapReference(
344 timingId: remapReference(
350 waveformId: remapReference(
351 component.waveformId,
360 ...deepClone(component),
361 id: generateSimId('Transmitter'),
363 antennaId: remapReference(
369 timingId: remapReference(
375 waveformId: remapReference(
376 component.waveformId,
384 ...deepClone(component),
385 id: generateSimId('Receiver'),
387 antennaId: remapReference(
393 timingId: remapReference(
402 ...deepClone(component),
403 id: generateSimId('Target'),
409export function cloneTemplateIntoScenarioData(
410 scenarioData: ScenarioData,
411 template: AssetLibraryTemplate
412): { scenarioData: ScenarioData; result: AssetTemplateInsertionResult } {
413 const nextScenarioData: ScenarioData = {
414 globalParameters: deepClone(scenarioData.globalParameters),
415 waveforms: scenarioData.waveforms.map(deepClone),
416 timings: scenarioData.timings.map(deepClone),
417 antennas: scenarioData.antennas.map(deepClone),
418 platforms: scenarioData.platforms.map(sanitizePlatform),
420 const warnings: string[] = [];
422 switch (template.kind) {
424 const waveform = cloneWaveform(
429 nextScenarioData.waveforms.push(waveform);
431 scenarioData: nextScenarioData,
433 insertedItemId: waveform.id,
434 insertedName: waveform.name,
440 const timing = cloneTiming(
445 nextScenarioData.timings.push(timing);
447 scenarioData: nextScenarioData,
449 insertedItemId: timing.id,
450 insertedName: timing.name,
456 const antenna = cloneAntenna(
461 nextScenarioData.antennas.push(antenna);
463 scenarioData: nextScenarioData,
465 insertedItemId: antenna.id,
466 insertedName: antenna.name,
472 const dependencies = template.dependencies ?? {
477 const waveformIdMap = new Map<string, string>();
478 const timingIdMap = new Map<string, string>();
479 const antennaIdMap = new Map<string, string>();
481 for (const waveformTemplate of dependencies.waveforms) {
482 const waveform = cloneWaveform(
486 waveformIdMap.set(waveformTemplate.id, waveform.id);
487 nextScenarioData.waveforms.push(waveform);
490 for (const timingTemplate of dependencies.timings) {
491 const timing = cloneTiming(timingTemplate, nextScenarioData);
492 timingIdMap.set(timingTemplate.id, timing.id);
493 nextScenarioData.timings.push(timing);
496 for (const antennaTemplate of dependencies.antennas) {
497 const antenna = cloneAntenna(antennaTemplate, nextScenarioData);
498 antennaIdMap.set(antennaTemplate.id, antenna.id);
499 nextScenarioData.antennas.push(antenna);
502 const platform = clonePlatformWaypointIds(template.payload);
503 platform.name = createUniqueScenarioCopyName(
507 const existingNames = collectScenarioNames(nextScenarioData);
508 existingNames.add(platform.name);
509 platform.components = template.payload.components.map((component) =>
519 nextScenarioData.platforms.push(platform);
522 scenarioData: nextScenarioData,
524 insertedItemId: platform.id,
525 insertedName: platform.name,
533export function createAssetLibraryFile(
534 templates: AssetLibraryTemplate[]
537 schemaVersion: ASSET_LIBRARY_SCHEMA_VERSION,
542export function parseAssetTemplates(data: unknown): AssetLibraryTemplate[] {
543 const fileResult = AssetLibraryFileSchema.safeParse(data);
544 if (fileResult.success) {
545 return fileResult.data.templates;
548 const templateResult = AssetLibraryTemplateSchema.safeParse(data);
549 if (templateResult.success) {
550 return [templateResult.data];
554 'Asset library JSON does not match the v1 template schema.'
558export function prepareTemplatesForCatalog(
559 templates: AssetLibraryTemplate[],
560 timestamp = new Date().toISOString()
561): AssetLibraryTemplate[] {
562 return templates.map((template) => ({
563 ...deepClone(template),
564 id: createAssetTemplateId(),
565 updatedAt: timestamp,