import http from './http' import { ElMessageBox } from 'element-plus' import { mockReflectionAssemblies, mockReflectionBundle, mockReflectionDeleteField, mockReflectionExecute, mockReflectionFields, mockReflectionKinds, mockReflectionMethods, mockReflectionMethodsByType, mockReflectionObjects, mockReflectionSetField, mockReflectionStatus } from '@/mock/data/reflection' // 与 auth.ts / config.ts / ops.ts 统一走 VITE_USE_MOCK 开关,避免 PROJECTION/USE 双命名造成 // .env 改一个忘改另一个的诡异半 mock 状态。env.d.ts 已声明 VITE_USE_MOCK 类型。 const MOCK = import.meta.env.VITE_USE_MOCK === 'true' /** * 与 SimpleLite `ReflectionApiController` 一一对应的前端胶水。 * 路径前缀:`/sl/projection/reflection`(迷榖平台 YARP 反代下游 → SimpleLite EmbedIO)。 * * 该客户端覆盖了 SimpleLite 主体以及通过 plugins/*.dll 动态加载的 Standard 等 * 插件中所有标注了 `MethodMember` 的方法与所有继承自 Car/Mission/CarProgram/Site/Track * 的子类型,平台前端可据此动态渲染列表、属性、动作按钮。 */ const BASE = '/sl/projection/reflection' export type ReflectionKind = | 'car' | 'vehicle' | 'mission' | 'process' | 'site' | 'track' | 'map' | 'special' | 'script' // 顶级合成 kind:等于 site + track + special 的并集,由 SimpleLite 后端 /objects/scene 直接返回, // 每行带 subKind 字段,前端 bundle / execute / setField 走对应底层 kind。 | 'scene' export interface ReflectionEnvelope { success: boolean code: number data: T | null message: string } export interface ReflectionParam { name: string typeName: string hasDefault: boolean defaultValue?: string | null } export interface ReflectionMethod { methodName: string label?: string | null description?: string | null hint?: string | null returnType: string hasParams: boolean params: ReflectionParam[] requiresPlatformConfirm?: boolean confirmMessage?: string | null } export interface ReflectionObject { id: number name: string typeName: string layer?: string | null status?: string | null summary?: string | null /** 当顶级 kind 是合成 kind(如 "scene")时,此字段标记底层真实 kind。 */ subKind?: string | null } export interface ReflectionKv { key: string value: string locked?: boolean /** "typed" = 强类型 [FieldMember],不可删;"dynamic" = Prop.fields 动态字段,可删。 */ source?: 'typed' | 'dynamic' /** 后端字段的 .NET 类型名(String / Single / Int32 / Boolean 等),用于前端渲染对应控件。 */ typeName?: string } export interface ReflectionTypeMethods { typeName: string fullTypeName?: string typeLabel?: string assemblyName: string methods: ReflectionMethod[] } export interface ReflectionAssembly { name: string version?: string location?: string } /** 通过 GET /reflection/types/{kind} 拿到的可创建子类型行(进程 / 脚本 / 车辆管理面板的「新建」下拉用)。 */ export interface ReflectionCreatableType { typeName: string shortName: string label: string assemblyName: string } /** GET /reflection/plugins 返回的单个插件元信息。 */ export interface PluginEntry { name: string dllPath: string assemblyName: string assemblyVersion: string collectible: boolean loadedTypes: number loadedAt: string missionTypes: number carTypes: number } export interface ReflectionKindMeta { kind: ReflectionKind count: number label: string /** "primary" 表示顶级 5 分类;undefined / 其他视为底层 kind。 */ group?: string } export interface ReflectionSubKindMeta extends ReflectionKindMeta { parent: ReflectionKind } export interface ReflectionSelection { kind: ReflectionKind | null id: number name: string typeName?: string } export interface ScriptSourcePayload { id: number name: string typeName: string state?: string | null script: string } export interface ScriptExceptionStatusPayload { id: number name: string typeName: string car: string state?: string | null exception: string notifies: string report: string } // ────────────────────────────────────────────────────────────────────────── // 地图监控可见性配置(与后端 MonitorVisibilityConfig / MonitorVisibilityForKind 对应) // // `config` = 管理员勾选的白名单。空数组 = 显示全部;非空 = 仅显示列表中的 key。 // `available` = 当前 SimpleLite 进程里能扫描到的全部可勾选项(基类 + 已加载子类 + // 运行时实例的 Prop.fields / status 反射键)。是 GET 返回的副产物, // POST 写入时不需要、也不该回传。 // ────────────────────────────────────────────────────────────────────────── export type MonitorVisibilityKind = 'car' | 'site' | 'track' export interface MonitorVisibilityForKind { fields: string[] status: string[] methods: string[] } export interface MonitorVisibilityMap { car: MonitorVisibilityForKind site: MonitorVisibilityForKind track: MonitorVisibilityForKind } /** POST /monitor-config 请求体:三组白名单 + 可选按车型动作。 */ export interface MonitorConfigSaveBody extends MonitorVisibilityMap { carActionByType?: Record } export interface MonitorConfigPayload { config: MonitorVisibilityMap & { carActionByType?: Record } available: MonitorVisibilityMap } const MOCK_MONITOR_STORAGE_KEY = 'simple-platform-mock-monitor-config' function loadMockMonitorConfig(): MonitorConfigSaveBody { try { const raw = localStorage.getItem(MOCK_MONITOR_STORAGE_KEY) if (raw) return JSON.parse(raw) as MonitorConfigSaveBody } catch { /* ignore */ } return emptyMonitorVisibilityMap() } function saveMockMonitorConfig(cfg: MonitorConfigSaveBody) { try { localStorage.setItem(MOCK_MONITOR_STORAGE_KEY, JSON.stringify(cfg)) } catch { /* ignore */ } } export function emptyMonitorVisibilityForKind(): MonitorVisibilityForKind { return { fields: [], status: [], methods: [] } } export function emptyMonitorVisibilityMap(): MonitorVisibilityMap { return { car: emptyMonitorVisibilityForKind(), site: emptyMonitorVisibilityForKind(), track: emptyMonitorVisibilityForKind() } } // 语义上 available 与 config 同构(都是按 kind 分组的三组 key 列表),所以共用一个工厂。 // 单独保留命名是为了表达"业务含义不同"——可勾选全集 vs 已勾选白名单。 export const emptyMonitorAvailableMap = emptyMonitorVisibilityMap export class ReflectionApiError extends Error { code: number data: unknown constructor(message: string, code: number, data?: unknown) { super(message) this.name = 'ReflectionApiError' this.code = code this.data = data ?? null } } export interface ReflectionExecuteResult { returnValue?: string | null accepted?: boolean completed?: boolean } /** 根据 execute 返回区分「已完成」与「已受理(后台继续)」文案。 */ export function formatReflectionExecuteMessage( label: string, result: ReflectionExecuteResult ): string { if (result.returnValue) return `已执行:${result.returnValue}` if (result.accepted && result.completed === false) { return `已受理:${label}(后台继续执行,请稍后在 SimpleLite 查看结果)` } if (result.accepted) return `已执行 ${label}` return `已执行 ${label}` } export interface ReflectionExecuteOptions { /** 已在平台侧完成二次确认时带上,对应后端 X-Platform-Confirmed: 1 */ platformConfirmed?: boolean } async function get(path: string): Promise { const { data } = await http.get>(`${BASE}${path}`) if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data) return data.data as T } async function post( path: string, params?: Record, headers?: Record ): Promise { const { data } = await http.post>(`${BASE}${path}`, null, { params, headers }) if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data) return data.data as T } async function del(path: string): Promise { const { data } = await http.delete>(`${BASE}${path}`) if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data) return data.data as T } async function patchJson(path: string, body: unknown): Promise { const { data } = await http.patch>(`${BASE}${path}`, body) if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection PATCH ${path} failed`, data?.code ?? 500, data?.data) return data.data as T } async function executeWithPlatformConfirm( path: string, params?: Record, opts?: ReflectionExecuteOptions ): Promise { const headers = opts?.platformConfirmed ? { 'X-Platform-Confirmed': '1' } : undefined try { return await post(path, params, headers) } catch (e) { if (e instanceof ReflectionApiError && e.code === 428 && !opts?.platformConfirmed) { const confirmMessage = (e.data as { confirmMessage?: string | null } | null)?.confirmMessage?.trim() || '此操作需要确认' await ElMessageBox.confirm(confirmMessage, '确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }) return await post(path, params, { 'X-Platform-Confirmed': '1' }) } throw e } } export const reflectionApi = { listKinds: () => MOCK ? Promise.resolve(mockReflectionKinds()) : get<{ kinds: ReflectionKindMeta[]; subKinds?: ReflectionSubKindMeta[] }>('/kinds'), listAssemblies: () => MOCK ? Promise.resolve(mockReflectionAssemblies()) : get('/assemblies'), listObjects: (kind: ReflectionKind) => MOCK ? Promise.resolve(mockReflectionObjects(kind)) : get(`/objects/${kind}`), /** 列出当前可实例化的子类型(用于「新建」下拉);mock 模式直接给空。 */ listCreatableTypes: (kind: ReflectionKind): Promise => MOCK ? Promise.resolve([]) : get(`/types/${kind}`), /** * 创建一条对象。底层 POST `/objects/{kind}?...`,所有 extras 字段都作为 query 传给后端。 * * 三类典型用法: * - process/script + UiDiscoveryCache 子类:`createObject('process', 'TaskFlow')` * - car(DummyCar 兜底,typeName 可空):`createObject('car', '', { name: 'AGV-1', x: 0, y: 0 })` * - site/track/image/text/model:`createObject('site', '', { x: 100, y: 200, name: 'A' })` * * 后端 BuildAndPersist 会做完整的字段类型转换;前端只负责传字符串。 */ createObject: ( kind: ReflectionKind, typeName?: string, extras?: Record ) => { if (MOCK) return Promise.resolve({ kind, id: -1, typeName: typeName ?? '' }) const params: Record = {} if (typeName) params.typeName = typeName if (extras) { for (const [k, v] of Object.entries(extras)) { if (v === undefined || v === null || v === '') continue params[k] = v } } return post<{ kind: string; id: number; typeName: string }>(`/objects/${kind}`, params) }, /** 删除一条对象(process / script / car / site / track / special)。 */ deleteObject: (kind: ReflectionKind, id: number) => MOCK ? Promise.resolve({ kind, id, deleted: true }) : del<{ kind: string; id: number; deleted: boolean }>(`/objects/${kind}/${id}`), /** * 触发 SimpleLite 重新扫描 ./plugins 目录、加载新增 dll 并重建 UiDiscoveryCache。 * 已加载的 dll 不会重复加载。 * 返回:本次发现的 dll 总数、新加载的 assembly 数、当前可创建的 mission/car 类型计数。 */ reloadPlugins: () => MOCK ? Promise.resolve({ totalDlls: 0, newlyLoaded: 0, failed: 0, missionTypes: 0, carTypes: 0 }) : post<{ totalDlls: number; newlyLoaded: number; failed: number; missionTypes: number; carTypes: number }>( '/plugins/reload' ), /** 列出当前已加载的所有 collectible 插件(PluginManager 跟踪范围内)。 */ listPlugins: () => MOCK ? Promise.resolve([]) : get('/plugins'), /** * 卸载一个 collectible 插件。 * 失败原因常见:仍有 Mission / Car 实例占用插件类型 → 409。 */ unloadPlugin: (name: string) => MOCK ? Promise.resolve({ name, message: 'mock', missionTypes: 0, carTypes: 0 }) : post<{ name: string; message: string; missionTypes: number; carTypes: number }>( `/plugins/${encodeURIComponent(name)}/unload` ), listMethods: (kind: ReflectionKind, id: number) => MOCK ? Promise.resolve(mockReflectionMethods(kind)) : get(`/methods/${kind}/${id}`), listMethodsByType: (kind: ReflectionKind) => MOCK ? Promise.resolve(mockReflectionMethodsByType(kind)) : get(`/methods-by-type/${kind}`), getStatus: (kind: ReflectionKind, id: number) => MOCK ? Promise.resolve(mockReflectionStatus(kind, id)) : get(`/status/${kind}/${id}`), getFields: (kind: ReflectionKind, id: number) => MOCK ? Promise.resolve(mockReflectionFields(kind, id)) : get(`/fields/${kind}/${id}`), setField: (kind: ReflectionKind, id: number, field: string, value: string) => MOCK ? Promise.resolve(mockReflectionSetField(kind, id, field, value)) : post<{ kind: ReflectionKind; id: number; field: string; value: string }>( `/fields/${kind}/${id}/${encodeURIComponent(field)}`, { value } ), deleteField: (kind: ReflectionKind, id: number, field: string) => MOCK ? Promise.resolve(mockReflectionDeleteField(kind, id, field)) : del<{ kind: ReflectionKind; id: number; field: string }>( `/fields/${kind}/${id}/${encodeURIComponent(field)}` ), getBundle: (kind: ReflectionKind, id: number) => MOCK ? Promise.resolve(mockReflectionBundle(kind, id)) : get<{ kind: string id: number typeName: string fullTypeName?: string assembly: string summary: ReflectionObject methods: ReflectionMethod[] status: ReflectionKv[] /** 扁平 key→value 兼容旧组件。 */ fields: Record /** 带 source/locked/typeName 的字段表,新版「对象管理」面板用。 */ fieldList?: ReflectionKv[] }>(`/bundle/${kind}/${id}`), execute: ( kind: ReflectionKind, id: number, method: string, params?: Record, opts?: ReflectionExecuteOptions ) => MOCK ? Promise.resolve(mockReflectionExecute( kind, id, method, Object.fromEntries(Object.entries(params ?? {}).map(([k, v]) => [k, String(v)])) )) : executeWithPlatformConfirm( `/execute/${kind}/${id}/${encodeURIComponent(method)}`, params, opts ), /** 车辆前往指定站点(Web「去某地」;不依赖 SimpleUI.GetPoint)。 */ gotoCarSite: (carId: number, siteId: number) => MOCK ? Promise.resolve({ carId, siteId, message: `mock goto ${carId} -> ${siteId}` }) : post<{ carId: number; siteId: number; message: string }>( `/car/${carId}/goto-site`, { siteId } ), getScriptSource: (id: number): Promise => MOCK ? Promise.resolve({ id, name: `MockScript#${id}`, typeName: 'CarProgram', state: 'Running', script: '// mock script' }) : get(`/scripts/${id}/source`), getScriptExceptionStatus: (id: number): Promise => MOCK ? Promise.resolve({ id, name: `MockScript#${id}`, typeName: 'CarProgram', car: 'AGV-1(#1)', state: 'Running', exception: '(无)', notifies: '(无)', report: '=== CarProgram 异常状态报告 ===\nstate : Running\n\n--- exception ---\n(无)' }) : get(`/scripts/${id}/exception-status`), // ────────────────────────────────────────────────────────────────────────── // 地图监控配置(Configuration.conf.monitorVisibility) // /monitor-config GET 现有配置 + 各 kind 可勾选 fields/status/methods 全集 // /monitor-config POST body JSON 全量覆盖 // 空白名单 = 显示全部;非空 = 仅显示列表中的 key。 // ────────────────────────────────────────────────────────────────────────── getMonitorConfig: (): Promise => MOCK ? Promise.resolve({ config: loadMockMonitorConfig(), available: emptyMonitorAvailableMap() }) : get('/monitor-config'), saveMonitorConfig: async (cfg: MonitorConfigSaveBody): Promise => { if (MOCK) { saveMockMonitorConfig(cfg) return cfg } const { data } = await http.post>( `${BASE}/monitor-config`, cfg ) if (!data?.success) throw new Error(data?.message ?? 'saveMonitorConfig failed') return data.data as MonitorConfigSaveBody }, // 选中同步:让 SimpleLite 3D 场景同步高亮被点击对象 getSelection: () => MOCK ? Promise.resolve({ kind: null, id: 0, name: '' }) : get('/selection'), setSelection: (kind: ReflectionKind, id: number) => MOCK ? Promise.resolve({ kind, id, name: `mock(${kind}#${id})` }) : post<{ kind: ReflectionKind; id: number; name: string }>('/selection', { kind, id }), clearSelection: () => MOCK ? Promise.resolve({ cleared: true }) : post<{ cleared: boolean }>('/selection/clear'), // ────────────────────────────────────────────────────────────────────────── // 项目属性(Scene.conf 单例) // /project/fields GET → 列出 Scene.conf 全部 [FieldMember] 字段 // /project/fields/{key} POST → ?value=xxx 写入单字段 // /project/save POST → 把内存项目(含修改后的 conf)写回 JSON // ────────────────────────────────────────────────────────────────────────── getProjectFields: (): Promise => MOCK ? Promise.resolve({ target: 'Scene.conf', lastLoadedPath: null, autoloadPath: null, fields: [] }) : get('/project/fields'), setProjectField: (field: string, value: string) => MOCK ? Promise.resolve({ field, value }) : post<{ field: string; value: string }>( `/project/fields/${encodeURIComponent(field)}`, { value } ), /** 保存当前项目到磁盘。path 为空则后端用 LastLoadedPath / Configuration.conf.autoload。 */ saveProject: (path?: string) => MOCK ? Promise.resolve({ path: path ?? '(mock)' }) : post<{ path: string }>('/project/save', path ? { path } : undefined), // ────────────────────────────────────────────────────────────────────────── // 核心配置(simple.json / Configuration.conf) // /app-config/fields GET → 列出 Configuration.conf 全部 [FieldMember] // /app-config/fields/{key} POST → ?value=xxx 写入单字段 // /app-config/save POST → 调 Configuration.ToFile("simple.json") // ────────────────────────────────────────────────────────────────────────── getAppConfigFields: (): Promise => MOCK ? Promise.resolve({ target: 'Configuration.conf', savePath: 'simple.json', fields: [] }) : get('/app-config/fields'), setAppConfigField: (field: string, value: string) => MOCK ? Promise.resolve({ field, value }) : post<{ field: string; value: string }>( `/app-config/fields/${encodeURIComponent(field)}`, { value } ), saveAppConfig: () => MOCK ? Promise.resolve({ path: 'simple.json' }) : post<{ path: string }>('/app-config/save'), // ────────────────────────────────────────────────────────────────────────── // 车型样式(WorkspaceCarStylesByType / WorkspaceAlarmColorScheme) // /viewport-style GET 站点/路径视口 Painter 样式 // /viewport-style PATCH 部分更新并立即重绘画布 getViewportStyle: () => MOCK ? Promise.resolve({ site: { drawScale: 2.5, dotRadiusNormal: 4, dotRadiusSelected: 6, selectionRingM: 0.14, colorNormalArgb: 0xffffffff, colorSelectedArgb: 0xffff0000, labelColorNormalArgb: 0xffffffff, labelColorSelectedArgb: 0xffff0000 }, track: { drawScale: 1.5, widthNormal: 1.2, widthSelected: 2.5, showDirectionArrows: true, colorNormalArgb: 0xff00ffff, colorSelectedArgb: 0xffff0000 } } satisfies ViewportStylePayload) : get('/viewport-style'), patchViewportStyle: (patch: ViewportStylePatch) => MOCK ? Promise.resolve({ site: { drawScale: 2.5, dotRadiusNormal: 4, dotRadiusSelected: 6, selectionRingM: 0.14, colorNormalArgb: 0xffffffff, colorSelectedArgb: 0xffff0000, labelColorNormalArgb: 0xffffffff, labelColorSelectedArgb: 0xffff0000 }, track: { drawScale: 1.5, widthNormal: 1.2, widthSelected: 2.5, showDirectionArrows: true, colorNormalArgb: 0xff00ffff, colorSelectedArgb: 0xffff0000 } } satisfies ViewportStylePayload) : patchJson('/viewport-style', sanitizeViewportPatch(patch)), // /car-style/types GET 列出所有 Car 子类型样式 // /car-style/{typeFullName} GET 单个车型当前样式 // /car-style/{typeFullName} POST body JSON 全量覆盖 // /car-style/{typeFullName} DELETE 恢复为默认 // /car-style/alarm-colors GET 7 种报警键 → 颜色 // /car-style/alarm-colors POST body JSON 全量覆盖映射 // /car-style/save POST 写回 simple.json // ────────────────────────────────────────────────────────────────────────── getCarStyleTypes: () => MOCK ? Promise.resolve({ globalDefault: defaultCarStyleDto(), types: [] }) : get('/car-style/types'), /** 车型编码字段元数据(site/track/plan/car 四类字段及默认值)。 */ getCarTypeCoderFields: () => MOCK ? Promise.resolve([]) : get('/car-types/coder-fields'), getCarStyle: (typeFullName: string) => MOCK ? Promise.resolve(defaultCarStyleDto()) : get(`/car-style/${encodeURIComponent(typeFullName)}`), /** 用 axios 直接以 JSON body 提交,避免拼 query。 */ putCarStyle: async (typeFullName: string, body: CarStyleDto) => { if (MOCK) return body const { data } = await http.post>( `${BASE}/car-style/${encodeURIComponent(typeFullName)}`, body ) if (!data?.success) throw new Error(data?.message ?? 'putCarStyle failed') return data.data as CarStyleDto }, deleteCarStyle: (typeFullName: string) => MOCK ? Promise.resolve({ typeFullName, removed: true }) : del<{ typeFullName: string; removed: boolean }>(`/car-style/${encodeURIComponent(typeFullName)}`), getAlarmColors: () => MOCK ? Promise.resolve({ defaultKeys: [], entries: [] }) : get('/car-style/alarm-colors'), putAlarmColors: async (palette: Record) => { if (MOCK) return { count: Object.keys(palette).length } const { data } = await http.post>( `${BASE}/car-style/alarm-colors`, palette ) if (!data?.success) throw new Error(data?.message ?? 'putAlarmColors failed') return data.data as { count: number } }, saveCarStyle: () => MOCK ? Promise.resolve({ path: 'simple.json' }) : post<{ path: string }>('/car-style/save') } /** 与 SimpleLite 视口样式对话框 / Configuration.conf.viewport 同步。 */ export interface ViewportStyleSite { drawScale: number dotRadiusNormal: number dotRadiusSelected: number selectionRingM: number colorNormalArgb: number colorSelectedArgb: number labelColorNormalArgb: number labelColorSelectedArgb: number } export interface ViewportStyleTrack { drawScale: number widthNormal: number widthSelected: number showDirectionArrows: boolean colorNormalArgb: number colorSelectedArgb: number } export interface ViewportStylePayload { site: ViewportStyleSite track: ViewportStyleTrack } export type ViewportStylePatch = { site?: Partial track?: Partial } /** 将 ARGB 规范为无符号 32 位,避免 JSON 序列化成负数导致后端 UInt32 反序列化失败。 */ export function normalizeArgb(argb: number): number { if (!Number.isFinite(argb)) return 0 return argb >>> 0 } export function argbToHex(argb: number): string { const u = normalizeArgb(argb) const a = (u >>> 24) & 0xff const r = (u >>> 16) & 0xff const g = (u >>> 8) & 0xff const b = u & 0xff if (a >= 255) return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` return `#${a.toString(16).padStart(2, '0')}${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` } export function hexToArgb(hex: string): number { const h = hex.replace('#', '').trim() if (h.length === 6) { const r = parseInt(h.slice(0, 2), 16) const g = parseInt(h.slice(2, 4), 16) const b = parseInt(h.slice(4, 6), 16) return normalizeArgb((0xff << 24) | (r << 16) | (g << 8) | b) } if (h.length === 8) { const a = parseInt(h.slice(0, 2), 16) const r = parseInt(h.slice(2, 4), 16) const g = parseInt(h.slice(4, 6), 16) const b = parseInt(h.slice(6, 8), 16) return normalizeArgb((a << 24) | (r << 16) | (g << 8) | b) } return 0xffffffff } const VIEWPORT_SITE_ARGB_KEYS = [ 'colorNormalArgb', 'colorSelectedArgb', 'labelColorNormalArgb', 'labelColorSelectedArgb' ] as const const VIEWPORT_TRACK_ARGB_KEYS = ['colorNormalArgb', 'colorSelectedArgb'] as const /** PATCH 前把视口样式中的 ARGB 字段转为 UInt32 可接受的非负整数。 */ export function sanitizeViewportPatch(patch: ViewportStylePatch): ViewportStylePatch { const out: ViewportStylePatch = {} if (patch.site) { const site = { ...patch.site } for (const k of VIEWPORT_SITE_ARGB_KEYS) { if (site[k] != null) site[k] = normalizeArgb(site[k]!) } out.site = site } if (patch.track) { const track = { ...patch.track } for (const k of VIEWPORT_TRACK_ARGB_KEYS) { if (track[k] != null) track[k] = normalizeArgb(track[k]!) } out.track = track } return out } export function normalizeViewportPayload(raw: ViewportStylePayload): ViewportStylePayload { return { site: { ...raw.site, colorNormalArgb: normalizeArgb(raw.site.colorNormalArgb), colorSelectedArgb: normalizeArgb(raw.site.colorSelectedArgb), labelColorNormalArgb: normalizeArgb(raw.site.labelColorNormalArgb), labelColorSelectedArgb: normalizeArgb(raw.site.labelColorSelectedArgb) }, track: { ...raw.track, colorNormalArgb: normalizeArgb(raw.track.colorNormalArgb), colorSelectedArgb: normalizeArgb(raw.track.colorSelectedArgb) } } } export interface CarStyleDto { bodyLengthM: number bodyWidthM: number bodyColorArgb: number outlineColorArgb: number labelColorArgb: number showLabel: boolean modelPath: string } export interface CarStyleTypeRow { typeName: string shortName: string label: string assemblyName: string hasOverride: boolean style: CarStyleDto } export interface CarStyleTypesPayload { globalDefault: CarStyleDto types: CarStyleTypeRow[] } export interface CoderFieldDef { name: string typeName: string defaultValue: unknown } export interface CoderFieldGroup { typeName: string shortName: string assemblyName: string baseTypeName?: string fields: CoderFieldDef[] } export interface CarTypeCoderFieldsRow { typeName: string shortName: string label: string assemblyName: string siteFields: CoderFieldGroup trackFields: CoderFieldGroup planFields: CoderFieldGroup carFields: CoderFieldGroup } export interface AlarmColorEntry { key: string colorArgb: number isDefault: boolean } export interface AlarmColorsPayload { defaultKeys: string[] entries: AlarmColorEntry[] } function defaultCarStyleDto(): CarStyleDto { return { bodyLengthM: 0.64, bodyWidthM: 0.42, bodyColorArgb: 0xffffffff, outlineColorArgb: 0xff2a2a2a, labelColorArgb: 0xffffffff, showLabel: true, modelPath: '' } } export interface ProjectPropertyRow { key: string label: string value: string typeName: string locked: boolean } export interface ProjectPropertiesPayload { target: string lastLoadedPath: string | null autoloadPath: string | null fields: ProjectPropertyRow[] } export interface AppConfigPayload { target: string savePath: string fields: ProjectPropertyRow[] }