poe-svelte-ui-lib 1.36.0 → 1.37.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/README.md +12 -0
- package/dist/WidgetHeader.svelte +5 -2
- package/dist/WidgetPFD/PlaneIcon.svelte +12 -0
- package/dist/WidgetPFD/PlaneIcon.svelte.d.ts +18 -0
- package/dist/WidgetPFD/README.md +76 -0
- package/dist/WidgetPFD/WidgetPFD.svelte +396 -0
- package/dist/WidgetPFD/WidgetPFD.svelte.d.ts +4 -0
- package/dist/WidgetPFD/WidgetPFDProps.svelte +53 -0
- package/dist/WidgetPFD/WidgetPFDProps.svelte.d.ts +14 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/locales/translations.js +5 -0
- package/dist/types.d.ts +51 -4
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -71,6 +71,18 @@ npm run build # сборка пакета (svelte-kit sync + svelte-packag
|
|
|
71
71
|
npm run Formatting # prettier --write .
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Публикация в npm
|
|
75
|
+
|
|
76
|
+
Коммит в `main` с `[publish]` в сообщении (`"X.Y.Z [deploy] [publish]"`, версия поднята в `package.json`) запускает
|
|
77
|
+
`.github/workflows/npm_publish.yaml`. Публикация идёт через **trusted publisher** npm (OIDC): workflow получает
|
|
78
|
+
одноразовый токен от GitHub (`permissions: id-token: write`), постоянный `NPM_TOKEN` не нужен, provenance добавляется
|
|
79
|
+
автоматически. Требования npm: npm CLI 11.5.1+ (workflow обновляет его сам), Node 22.14+, `repository.url` в
|
|
80
|
+
`package.json` дословно совпадает с репозиторием.
|
|
81
|
+
|
|
82
|
+
Связь настраивается на npmjs.com → пакет → Settings → Trusted Publisher: GitHub Actions, `Oleg-Perevyshin/ElementsUI`,
|
|
83
|
+
workflow `npm_publish.yaml`, разрешение «npm publish». Новая связь подтверждается первой успешной публикацией и
|
|
84
|
+
должна пройти её в течение 2 дней, иначе истекает. Существующую связь изменить нельзя — только удалить и создать заново.
|
|
85
|
+
|
|
74
86
|
---
|
|
75
87
|
|
|
76
88
|
Лицензия: MIT — см. `LICENSE`.
|
package/dist/WidgetHeader.svelte
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
const Icon = $derived(icon)
|
|
25
25
|
</script>
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
<!-- Фиксированная высота 32 px без переноса — свёрнутые виджеты одной высоты при любом содержимом справа -->
|
|
28
|
+
<div class="flex h-[32px] items-center justify-between gap-3">
|
|
28
29
|
<button
|
|
29
30
|
type="button"
|
|
30
31
|
class="flex min-w-0 flex-1 cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left"
|
|
@@ -43,6 +44,8 @@
|
|
|
43
44
|
</span>
|
|
44
45
|
</button>
|
|
45
46
|
{#if right}
|
|
46
|
-
|
|
47
|
+
<div class="flex min-w-0 shrink-0 items-center justify-end">
|
|
48
|
+
{@render right()}
|
|
49
|
+
</div>
|
|
47
50
|
{/if}
|
|
48
51
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
|
|
4
|
+
><path
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
stroke-linecap="round"
|
|
8
|
+
stroke-linejoin="round"
|
|
9
|
+
stroke-width="2"
|
|
10
|
+
d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"
|
|
11
|
+
/></svg
|
|
12
|
+
>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const PlaneIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type PlaneIcon = InstanceType<typeof PlaneIcon>;
|
|
18
|
+
export default PlaneIcon;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# WidgetPFD Component
|
|
2
|
+
|
|
3
|
+
## Описание
|
|
4
|
+
|
|
5
|
+
> **Размеры.** Ширина — 100% контейнера (в конструкторе — ширина ячейки сетки), без собственного ограничения; высота — по содержимому, в сетке конструктора виджет занимает одну строку. Мастер-цвет (`componentClass`) перекрашивает шапку и рамки значений.
|
|
6
|
+
|
|
7
|
+
Смарт-виджет «Пилотажный дисплей» (Primary Flight Display) — главный прибор контроля полёта, как в
|
|
8
|
+
стеклянной кабине самолёта или наземной станции управления. Работает с полями группы (по умолчанию
|
|
9
|
+
`MAV`) ровно в том виде, в каком их отдаёт телеметрия автопилота MAVLink устройства 20.00.000-01.
|
|
10
|
+
Только отображение: ничего не отправляет и не знает про DeviceStore/WebSocket.
|
|
11
|
+
|
|
12
|
+
- **Строка состояния** — ARMED/DISARMED, режим полёта (название по типу аппарата: самолёт, коптер,
|
|
13
|
+
наземный), GPS (тип фиксации и спутники), батарея (В, А, %), расстояние и пеленг на точку взлёта.
|
|
14
|
+
- **Авиагоризонт** — небо и земля по крену и тангажу, лестница тангажа через 5°, дуга крена
|
|
15
|
+
(10/20/30/45/60°), символ самолёта. Ориентация сглаживается между пакетами (Tween 120 мс).
|
|
16
|
+
- **Лента скорости** (слева, путевая) с воздушной числом, **лента высоты** (справа, над точкой взлёта)
|
|
17
|
+
с вариометром, **лента курса** (снизу) с меткой трека (розовая) и направления на дом (зелёная).
|
|
18
|
+
- **Под прибором** — координаты, высота над уровнем моря, давление, температура.
|
|
19
|
+
- **Нет данных** — если пакеты не приходили дольше `limits.staleMs` (по умолчанию 2 с), прибор
|
|
20
|
+
перечёркивается и помечается «НЕТ ДАННЫХ»; в шапке всегда виден возраст данных.
|
|
21
|
+
- **Узкая ячейка** (уже 440 px) — ленты скорости и высоты заменяются числами поверх горизонта.
|
|
22
|
+
|
|
23
|
+
## Протокол устройства
|
|
24
|
+
|
|
25
|
+
Поток телеметрии (`Telemetry_SendObject`, ключ `Data`) — облако раскладывает поля в значения группы:
|
|
26
|
+
|
|
27
|
+
| Поле | Что | Источник MAVLink | Частота |
|
|
28
|
+
| ---------------------------------------- | ----------------------------------------------- | ---------------------------- | ------- |
|
|
29
|
+
| `MAV.Roll`, `MAV.Pitch`, `MAV.Heading` | крен, тангаж, курс (°) | ATTITUDE | 20 Гц |
|
|
30
|
+
| `MAV.GSpeed`, `MAV.ASpeed`, `MAV.Climb` | путевая, воздушная, вертикальная скорость (м/с) | VFR_HUD | 2 Гц |
|
|
31
|
+
| `MAV.Alt`, `MAV.AltMSL` | высота над взлётом и над морем (м) | GLOBAL_POSITION_INT, VFR_HUD | 2 Гц |
|
|
32
|
+
| `MAV.Lat`, `MAV.Lon`, `MAV.Vx`, `MAV.Vy` | координаты, скорость на север/восток | GLOBAL_POSITION_INT | 2 Гц |
|
|
33
|
+
| `MAV.Fix`, `MAV.Sat` | тип фиксации, спутники | GPS_RAW_INT | 2 Гц |
|
|
34
|
+
| `MAV.BV`, `MAV.BC`, `MAV.BRem` | батарея: В, А (-1 нет), % (-1 нет) | SYS_STATUS | 2 Гц |
|
|
35
|
+
| `MAV.Mode`, `MAV.VType`, `MAV.Armed` | custom_mode, MAV_TYPE, взведён | HEARTBEAT | 2 Гц |
|
|
36
|
+
| `MAV.HomeLat`, `MAV.HomeLon` | точка взлёта (0 — нет) | HOME_POSITION | 2 Гц |
|
|
37
|
+
| `MAV.Press`, `MAV.Temp` | давление (гПа), температура (°C) | SCALED_PRESSURE | 2 Гц |
|
|
38
|
+
|
|
39
|
+
Расстояние и пеленг на дом, трек и возраст данных виджет считает сам.
|
|
40
|
+
|
|
41
|
+
## Пропсы
|
|
42
|
+
|
|
43
|
+
| Название | Тип | По умолчанию | Описание |
|
|
44
|
+
| ---------------- | ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------- |
|
|
45
|
+
| `id` | `string` | `undefined` | Префикс группы полей (`"MAV"`), используется вызывающим кодом |
|
|
46
|
+
| `wrapperClass` | `string` | `""` | CSS-классы обёртки |
|
|
47
|
+
| `componentClass` | `string` | `""` | «Мастер-цвет» виджета — роль вида `"bg-red"` |
|
|
48
|
+
| `label` | `{ name?: string }` | `{ name: "Пилотажный дисплей" }` | Заголовок карточки |
|
|
49
|
+
| `value` | `Partial<IWidgetPFDData>` | `undefined` | Поля группы; без них — «НЕТ ДАННЫХ» |
|
|
50
|
+
| `keys` | `Partial<Record<keyof IWidgetPFDData, string>>` | `undefined` | Переопределение имён полей устройства |
|
|
51
|
+
| `limits` | `IWidgetPFDLimits` | `undefined` | Границы цветовых зон, см. ниже |
|
|
52
|
+
| `collapsed` | `boolean` | `false` | Свёрнуто ли тело виджета (`$bindable`) |
|
|
53
|
+
| `persistKey` | `string` | `undefined` | Ключ для сохранения `collapsed` в `localStorage` |
|
|
54
|
+
|
|
55
|
+
### IWidgetPFDLimits
|
|
56
|
+
|
|
57
|
+
| Поле | Описание |
|
|
58
|
+
| ---------- | ------------------------------------------------------------ |
|
|
59
|
+
| `speedMin` | м/с — ниже красная зона на ленте скорости |
|
|
60
|
+
| `speedMax` | м/с — выше красная зона |
|
|
61
|
+
| `bankMax` | ° — за пределом дуга крена красная, указатель крена краснеет |
|
|
62
|
+
| `altMax` | м — выше красная зона на ленте высоты |
|
|
63
|
+
| `battLow` | % — ниже батарея в строке состояния красная |
|
|
64
|
+
| `staleMs` | мс без новых данных до флага «НЕТ ДАННЫХ», по умолчанию 2000 |
|
|
65
|
+
|
|
66
|
+
Незаданная граница не рисуется.
|
|
67
|
+
|
|
68
|
+
## Примеры
|
|
69
|
+
|
|
70
|
+
```svelte
|
|
71
|
+
<UI.WidgetPFD value={mav} limits={{ speedMin: 8, speedMax: 25, bankMax: 30, altMax: 150, battLow: 20 }} />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Конструктор свойств (WidgetPFDProps.svelte)
|
|
75
|
+
|
|
76
|
+
Группы панели: общие (заголовок, группа полей, мастер-цвет), границы.
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<!-- $lib/WidgetPFD/WidgetPFD.svelte — смарт-виджет «Пилотажный дисплей» (Primary Flight Display).
|
|
2
|
+
Поля группы (по умолчанию MAV) — телеметрия автопилота MAVLink (см. IWidgetPFDData): авиагоризонт,
|
|
3
|
+
ленты скорости/высоты/курса, строка состояния, цветовые зоны по границам (limits). Только отображение:
|
|
4
|
+
ничего не отправляет и не знает про DeviceStore/WebSocket. Между пакетами ориентация сглаживается (Tween),
|
|
5
|
+
устаревшие данные закрываются флагом «нет данных». В узкой ячейке ленты заменяются числами. -->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { slide } from "svelte/transition"
|
|
8
|
+
import { Tween } from "svelte/motion"
|
|
9
|
+
import { cubicOut } from "svelte/easing"
|
|
10
|
+
import { twMerge } from "tailwind-merge"
|
|
11
|
+
import type { IWidgetPFDProps } from "../types"
|
|
12
|
+
import PlaneIcon from "./PlaneIcon.svelte"
|
|
13
|
+
import WidgetHeader from "../WidgetHeader.svelte"
|
|
14
|
+
import { widgetAccentStyle } from "../widgetAccent"
|
|
15
|
+
import { readPersistedCollapsed, writePersistedCollapsed } from "../widgetCollapse"
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
wrapperClass = "",
|
|
19
|
+
componentClass = "",
|
|
20
|
+
label = { name: "Пилотажный дисплей" },
|
|
21
|
+
value,
|
|
22
|
+
limits,
|
|
23
|
+
collapsed = $bindable(false),
|
|
24
|
+
persistKey,
|
|
25
|
+
}: IWidgetPFDProps = $props()
|
|
26
|
+
|
|
27
|
+
let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
|
|
28
|
+
|
|
29
|
+
/* Сворачивание тела по клику на шапку — как у остальных смарт-виджетов (см. WidgetWiFi.svelte) */
|
|
30
|
+
let hasMergedPersisted = false
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (hasMergedPersisted) return
|
|
33
|
+
hasMergedPersisted = true
|
|
34
|
+
const persisted = readPersistedCollapsed(persistKey, collapsed)
|
|
35
|
+
if (persisted !== collapsed) collapsed = persisted
|
|
36
|
+
})
|
|
37
|
+
$effect(() => writePersistedCollapsed(persistKey, collapsed))
|
|
38
|
+
|
|
39
|
+
const num = (v: unknown, fallback = 0) => (typeof v === "number" && Number.isFinite(v) ? v : fallback)
|
|
40
|
+
const wrap360 = (deg: number) => ((deg % 360) + 360) % 360
|
|
41
|
+
const toRad = (deg: number) => (deg * Math.PI) / 180
|
|
42
|
+
|
|
43
|
+
/* Плавность: ориентация и курс приходят ~20 Гц, между пакетами — короткая интерполяция */
|
|
44
|
+
const roll = new Tween(0, { duration: 120, easing: cubicOut })
|
|
45
|
+
const pitch = new Tween(0, { duration: 120, easing: cubicOut })
|
|
46
|
+
const heading = new Tween(0, { duration: 120, easing: cubicOut })
|
|
47
|
+
let lastUpdate = $state(0)
|
|
48
|
+
let now = $state(Date.now())
|
|
49
|
+
$effect(() => {
|
|
50
|
+
const r = value?.Roll
|
|
51
|
+
const p = value?.Pitch
|
|
52
|
+
const h = value?.Heading
|
|
53
|
+
if (r === undefined && p === undefined && h === undefined) return
|
|
54
|
+
roll.target = num(r)
|
|
55
|
+
pitch.target = num(p)
|
|
56
|
+
/* Курс без скачка через 0/360: цель — ближайшая к текущему значению */
|
|
57
|
+
const current = heading.target
|
|
58
|
+
const delta = ((wrap360(num(h)) - wrap360(current) + 540) % 360) - 180
|
|
59
|
+
heading.target = current + delta
|
|
60
|
+
lastUpdate = Date.now()
|
|
61
|
+
})
|
|
62
|
+
$effect(() => {
|
|
63
|
+
const timer = setInterval(() => (now = Date.now()), 250)
|
|
64
|
+
return () => clearInterval(timer)
|
|
65
|
+
})
|
|
66
|
+
let staleMs = $derived(limits?.staleMs ?? 2000)
|
|
67
|
+
let age = $derived(lastUpdate ? Math.max(0, now - lastUpdate) / 1000 : Infinity)
|
|
68
|
+
let stale = $derived(age * 1000 > staleMs)
|
|
69
|
+
|
|
70
|
+
/* Значения с устройства */
|
|
71
|
+
let gs = $derived(num(value?.GSpeed))
|
|
72
|
+
let as = $derived(num(value?.ASpeed))
|
|
73
|
+
let climb = $derived(num(value?.Climb))
|
|
74
|
+
let alt = $derived(num(value?.Alt))
|
|
75
|
+
let altMsl = $derived(num(value?.AltMSL))
|
|
76
|
+
let lat = $derived(num(value?.Lat))
|
|
77
|
+
let lon = $derived(num(value?.Lon))
|
|
78
|
+
let track = $derived(Math.hypot(num(value?.Vx), num(value?.Vy)) > 0.5 ? wrap360((Math.atan2(num(value?.Vy), num(value?.Vx)) * 180) / Math.PI) : null)
|
|
79
|
+
|
|
80
|
+
/* Точка взлёта: расстояние (гаверсинус) и пеленг */
|
|
81
|
+
let home = $derived.by(() => {
|
|
82
|
+
const hl = num(value?.HomeLat)
|
|
83
|
+
const ho = num(value?.HomeLon)
|
|
84
|
+
if (!hl || !ho || !lat || !lon) return null
|
|
85
|
+
const dLat = toRad(hl - lat)
|
|
86
|
+
const dLon = toRad(ho - lon)
|
|
87
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat)) * Math.cos(toRad(hl)) * Math.sin(dLon / 2) ** 2
|
|
88
|
+
const distance = 2 * 6371000 * Math.asin(Math.sqrt(a))
|
|
89
|
+
const y = Math.sin(dLon) * Math.cos(toRad(hl))
|
|
90
|
+
const x = Math.cos(toRad(lat)) * Math.sin(toRad(hl)) - Math.sin(toRad(lat)) * Math.cos(toRad(hl)) * Math.cos(dLon)
|
|
91
|
+
return { distance, bearing: wrap360((Math.atan2(y, x) * 180) / Math.PI) }
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/* Строка состояния */
|
|
95
|
+
const FIX = ["нет", "нет", "2D", "3D", "DGPS", "RTK float", "RTK fix"]
|
|
96
|
+
const COPTER_MODES: Record<number, string> = {
|
|
97
|
+
0: "STABILIZE",
|
|
98
|
+
1: "ACRO",
|
|
99
|
+
2: "ALT HOLD",
|
|
100
|
+
3: "AUTO",
|
|
101
|
+
4: "GUIDED",
|
|
102
|
+
5: "LOITER",
|
|
103
|
+
6: "RTL",
|
|
104
|
+
7: "CIRCLE",
|
|
105
|
+
9: "LAND",
|
|
106
|
+
11: "DRIFT",
|
|
107
|
+
13: "SPORT",
|
|
108
|
+
15: "AUTOTUNE",
|
|
109
|
+
16: "POSHOLD",
|
|
110
|
+
17: "BRAKE",
|
|
111
|
+
18: "THROW",
|
|
112
|
+
20: "GUIDED NOGPS",
|
|
113
|
+
21: "SMART RTL",
|
|
114
|
+
23: "FOLLOW",
|
|
115
|
+
24: "ZIGZAG",
|
|
116
|
+
27: "AUTO RTL",
|
|
117
|
+
}
|
|
118
|
+
const PLANE_MODES: Record<number, string> = {
|
|
119
|
+
0: "MANUAL",
|
|
120
|
+
1: "CIRCLE",
|
|
121
|
+
2: "STABILIZE",
|
|
122
|
+
3: "TRAINING",
|
|
123
|
+
4: "ACRO",
|
|
124
|
+
5: "FBWA",
|
|
125
|
+
6: "FBWB",
|
|
126
|
+
7: "CRUISE",
|
|
127
|
+
8: "AUTOTUNE",
|
|
128
|
+
10: "AUTO",
|
|
129
|
+
11: "RTL",
|
|
130
|
+
12: "LOITER",
|
|
131
|
+
13: "TAKEOFF",
|
|
132
|
+
15: "GUIDED",
|
|
133
|
+
17: "QSTABILIZE",
|
|
134
|
+
18: "QHOVER",
|
|
135
|
+
19: "QLOITER",
|
|
136
|
+
20: "QLAND",
|
|
137
|
+
21: "QRTL",
|
|
138
|
+
22: "QAUTOTUNE",
|
|
139
|
+
23: "QACRO",
|
|
140
|
+
24: "THERMAL",
|
|
141
|
+
}
|
|
142
|
+
const ROVER_MODES: Record<number, string> = {
|
|
143
|
+
0: "MANUAL",
|
|
144
|
+
1: "ACRO",
|
|
145
|
+
3: "STEERING",
|
|
146
|
+
4: "HOLD",
|
|
147
|
+
5: "LOITER",
|
|
148
|
+
6: "FOLLOW",
|
|
149
|
+
7: "SIMPLE",
|
|
150
|
+
10: "AUTO",
|
|
151
|
+
11: "RTL",
|
|
152
|
+
12: "SMART RTL",
|
|
153
|
+
15: "GUIDED",
|
|
154
|
+
}
|
|
155
|
+
let modeName = $derived.by(() => {
|
|
156
|
+
if (value?.Mode === undefined) return "—"
|
|
157
|
+
const vtype = num(value?.VType)
|
|
158
|
+
const mode = num(value?.Mode)
|
|
159
|
+
const table = vtype === 1 || (vtype >= 19 && vtype <= 25) ? PLANE_MODES : vtype === 10 || vtype === 11 ? ROVER_MODES : COPTER_MODES
|
|
160
|
+
return table[mode] ?? `MODE ${mode}`
|
|
161
|
+
})
|
|
162
|
+
let armed = $derived(num(value?.Armed) === 1)
|
|
163
|
+
let fix = $derived(num(value?.Fix))
|
|
164
|
+
let battRem = $derived(num(value?.BRem, -1))
|
|
165
|
+
let battLow = $derived(limits?.battLow !== undefined && battRem >= 0 && battRem <= limits.battLow)
|
|
166
|
+
let battCurrent = $derived(num(value?.BC, -1))
|
|
167
|
+
|
|
168
|
+
/* Компактный режим — ленты заменяются числами поверх горизонта */
|
|
169
|
+
let width = $state(600)
|
|
170
|
+
let compact = $derived(width < 440)
|
|
171
|
+
|
|
172
|
+
/* Геометрия прибора (viewBox 600×320) */
|
|
173
|
+
const VW = 600
|
|
174
|
+
const HH = 270
|
|
175
|
+
let HX = $derived(compact ? 0 : 80)
|
|
176
|
+
let HW = $derived(compact ? VW : 440)
|
|
177
|
+
let CX = $derived(HX + HW / 2)
|
|
178
|
+
const CY = HH / 2
|
|
179
|
+
const PPD = 4 /* пикселей на градус тангажа */
|
|
180
|
+
const SPEED_PPU = 6 /* пикселей на 1 м/с */
|
|
181
|
+
const ALT_PPU = 3 /* пикселей на 1 м */
|
|
182
|
+
const HDG_PPD = 4 /* пикселей на градус курса */
|
|
183
|
+
|
|
184
|
+
const pitchLadder = Array.from({ length: 37 }, (_, i) => (i - 18) * 5).filter((d) => d !== 0)
|
|
185
|
+
const ROLL_TICKS = [-60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60]
|
|
186
|
+
const ROLL_R = 110
|
|
187
|
+
|
|
188
|
+
const arcPoint = (deg: number, r: number) => ({ x: CX + r * Math.sin(toRad(deg)), y: CY - r * Math.cos(toRad(deg)) })
|
|
189
|
+
const arcPath = (from: number, to: number, r: number) => {
|
|
190
|
+
const a = arcPoint(from, r)
|
|
191
|
+
const b = arcPoint(to, r)
|
|
192
|
+
return `M ${a.x} ${a.y} A ${r} ${r} 0 0 1 ${b.x} ${b.y}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const speedTicks = $derived(Array.from({ length: 41 }, (_, i) => Math.floor(gs) - 20 + i).filter((v) => v >= 0))
|
|
196
|
+
const altTicks = $derived(Array.from({ length: 19 }, (_, i) => Math.floor(alt / 5) * 5 - 45 + i * 5))
|
|
197
|
+
const hdgTicks = $derived(Array.from({ length: 29 }, (_, i) => Math.floor(heading.current / 5) * 5 - 70 + i * 5))
|
|
198
|
+
const hdgLabel = (deg: number) => ({ 0: "N", 90: "E", 180: "S", 270: "W" })[wrap360(deg)] ?? String(wrap360(deg)).padStart(3, "0")
|
|
199
|
+
const hdgOffset = (deg: number) => (((deg - heading.current + 540) % 360) - 180) * HDG_PPD
|
|
200
|
+
|
|
201
|
+
/* Вариометр: ±5 м/с на всю полувысоту полосы */
|
|
202
|
+
let vsi = $derived(Math.max(-5, Math.min(5, climb)) * 20)
|
|
203
|
+
/* Уникальные id для clipPath — несколько виджетов на одной странице не должны делить отсечения */
|
|
204
|
+
const uid = crypto.randomUUID().slice(0, 8)
|
|
205
|
+
|
|
206
|
+
const fmt = (v: number, digits = 1) => (Number.isFinite(v) ? v.toFixed(digits) : "—")
|
|
207
|
+
const fmtDistance = (m: number) => (m >= 1000 ? `${(m / 1000).toFixed(2)} км` : `${Math.round(m)} м`)
|
|
208
|
+
const fmtAge = (s: number) => (Number.isFinite(s) ? `${s.toFixed(1)} с` : "нет связи")
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<div
|
|
212
|
+
class={twMerge("relative flex w-full flex-col gap-4 rounded-2xl border border-(--hairline-color) bg-(--container-color) p-4", wrapperClass)}
|
|
213
|
+
style={accentStyle}
|
|
214
|
+
>
|
|
215
|
+
<WidgetHeader icon={PlaneIcon} label={label?.name ?? "Пилотажный дисплей"} bind:collapsed>
|
|
216
|
+
{#snippet right()}
|
|
217
|
+
<span class="text-[12px] font-semibold tabular-nums {stale ? 'text-(--red-color)' : 'text-(--muted-color)'}">{fmtAge(age)}</span>
|
|
218
|
+
{/snippet}
|
|
219
|
+
</WidgetHeader>
|
|
220
|
+
|
|
221
|
+
{#if !collapsed}
|
|
222
|
+
<div class="flex flex-col gap-2" transition:slide={{ duration: 150 }} bind:clientWidth={width}>
|
|
223
|
+
<!-- Строка состояния -->
|
|
224
|
+
<div
|
|
225
|
+
class="flex flex-wrap items-center gap-x-4 gap-y-1 rounded-[10px] border border-(--hairline-color) bg-(--back-color) px-3 py-2 text-[12px] font-semibold tabular-nums"
|
|
226
|
+
>
|
|
227
|
+
<span class="rounded-md px-1.5 py-0.5 {armed ? 'bg-(--red-color) text-white' : 'bg-(--container-color) text-(--muted-color)'}"
|
|
228
|
+
>{armed ? "ARMED" : "DISARMED"}</span
|
|
229
|
+
>
|
|
230
|
+
<span>{modeName}</span>
|
|
231
|
+
<span class={fix < 3 ? "text-(--orange-color)" : ""}>GPS {FIX[fix] ?? fix} · {num(value?.Sat)}</span>
|
|
232
|
+
<span class={battLow ? "text-(--red-color)" : ""}>
|
|
233
|
+
{fmt(num(value?.BV))} В{battCurrent >= 0 ? ` · ${fmt(battCurrent)} А` : ""}{battRem >= 0 ? ` · ${battRem}%` : ""}
|
|
234
|
+
</span>
|
|
235
|
+
{#if home}
|
|
236
|
+
<span>Дом {fmtDistance(home.distance)} · {Math.round(home.bearing)}°</span>
|
|
237
|
+
{/if}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Прибор -->
|
|
241
|
+
<svg viewBox="0 0 {VW} 320" class="w-full rounded-[10px] bg-(--back-color)" role="img" aria-label="Пилотажный дисплей">
|
|
242
|
+
<defs>
|
|
243
|
+
<clipPath id="pfd-horizon-{uid}"><rect x={HX} y="0" width={HW} height={HH} rx="8" /></clipPath>
|
|
244
|
+
<clipPath id="pfd-speed-{uid}"><rect x="0" y="0" width="70" height={HH} rx="8" /></clipPath>
|
|
245
|
+
<clipPath id="pfd-alt-{uid}"><rect x={VW - 70} y="0" width="70" height={HH} rx="8" /></clipPath>
|
|
246
|
+
<clipPath id="pfd-hdg-{uid}"><rect x={HX} y={HH + 6} width={HW} height="44" rx="8" /></clipPath>
|
|
247
|
+
</defs>
|
|
248
|
+
|
|
249
|
+
<!-- Авиагоризонт -->
|
|
250
|
+
<g clip-path="url(#pfd-horizon-{uid})">
|
|
251
|
+
<g transform="rotate({-roll.current} {CX} {CY}) translate(0 {pitch.current * PPD})">
|
|
252
|
+
<rect x={CX - 600} y={CY - 1000} width="1200" height="1000" fill="#3a86c8" />
|
|
253
|
+
<rect x={CX - 600} y={CY} width="1200" height="1000" fill="#8a5a2b" />
|
|
254
|
+
<line x1={CX - 600} y1={CY} x2={CX + 600} y2={CY} stroke="#fff" stroke-width="2" />
|
|
255
|
+
{#each pitchLadder as d (d)}
|
|
256
|
+
{@const y = CY - d * PPD}
|
|
257
|
+
{@const half = d % 10 === 0 ? 40 : 18}
|
|
258
|
+
<line x1={CX - half} y1={y} x2={CX + half} y2={y} stroke="#fff" stroke-width="1.5" />
|
|
259
|
+
{#if d % 10 === 0}
|
|
260
|
+
<text x={CX - half - 6} y={y + 4} fill="#fff" font-size="11" text-anchor="end">{Math.abs(d)}</text>
|
|
261
|
+
<text x={CX + half + 6} y={y + 4} fill="#fff" font-size="11">{Math.abs(d)}</text>
|
|
262
|
+
{/if}
|
|
263
|
+
{/each}
|
|
264
|
+
</g>
|
|
265
|
+
|
|
266
|
+
<!-- Шкала крена: зона за пределом bankMax — красная -->
|
|
267
|
+
<path d={arcPath(-60, 60, ROLL_R)} fill="none" stroke="#fff" stroke-width="1.5" />
|
|
268
|
+
{#if limits?.bankMax !== undefined && limits.bankMax < 60}
|
|
269
|
+
<path d={arcPath(-60, -limits.bankMax, ROLL_R)} fill="none" stroke="#ef4444" stroke-width="4" />
|
|
270
|
+
<path d={arcPath(limits.bankMax, 60, ROLL_R)} fill="none" stroke="#ef4444" stroke-width="4" />
|
|
271
|
+
{/if}
|
|
272
|
+
{#each ROLL_TICKS as t (t)}
|
|
273
|
+
{@const a = arcPoint(t, ROLL_R)}
|
|
274
|
+
{@const b = arcPoint(t, ROLL_R + (t % 30 === 0 ? 12 : 7))}
|
|
275
|
+
<line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#fff" stroke-width="1.5" />
|
|
276
|
+
{/each}
|
|
277
|
+
<g transform="rotate({-roll.current} {CX} {CY})">
|
|
278
|
+
<polygon
|
|
279
|
+
points="{CX},{CY - ROLL_R + 2} {CX - 7},{CY - ROLL_R + 14} {CX + 7},{CY - ROLL_R + 14}"
|
|
280
|
+
fill={limits?.bankMax !== undefined && Math.abs(roll.current) > limits.bankMax ? "#ef4444" : "#facc15"}
|
|
281
|
+
/>
|
|
282
|
+
</g>
|
|
283
|
+
|
|
284
|
+
<!-- Символ самолёта -->
|
|
285
|
+
<g stroke="#facc15" stroke-width="4" fill="none" stroke-linecap="round">
|
|
286
|
+
<polyline points="{CX - 70},{CY} {CX - 25},{CY} {CX - 15},{CY + 10}" />
|
|
287
|
+
<polyline points="{CX + 70},{CY} {CX + 25},{CY} {CX + 15},{CY + 10}" />
|
|
288
|
+
</g>
|
|
289
|
+
<circle cx={CX} cy={CY} r="3" fill="#facc15" />
|
|
290
|
+
|
|
291
|
+
<!-- Компактный режим: числа вместо лент -->
|
|
292
|
+
{#if compact}
|
|
293
|
+
<g font-size="15" font-weight="600" fill="#fff">
|
|
294
|
+
<rect x="8" y={CY - 16} width="76" height="32" rx="6" fill="#000" fill-opacity="0.55" />
|
|
295
|
+
<text x="46" y={CY + 6} text-anchor="middle">{fmt(gs)}</text>
|
|
296
|
+
<rect x={VW - 84} y={CY - 16} width="76" height="32" rx="6" fill="#000" fill-opacity="0.55" />
|
|
297
|
+
<text x={VW - 46} y={CY + 6} text-anchor="middle">{Math.round(alt)}</text>
|
|
298
|
+
<text x="46" y={CY + 32} text-anchor="middle" font-size="11">м/с</text>
|
|
299
|
+
<text x={VW - 46} y={CY + 32} text-anchor="middle" font-size="11">м · {climb >= 0 ? "+" : ""}{fmt(climb)}</text>
|
|
300
|
+
</g>
|
|
301
|
+
{/if}
|
|
302
|
+
</g>
|
|
303
|
+
|
|
304
|
+
{#if !compact}
|
|
305
|
+
<!-- Лента скорости (путевая), красные зоны вне speedMin/speedMax -->
|
|
306
|
+
<g clip-path="url(#pfd-speed-{uid})">
|
|
307
|
+
<rect x="0" y="0" width="70" height={HH} fill="#000" fill-opacity="0.72" />
|
|
308
|
+
{#if limits?.speedMin !== undefined}
|
|
309
|
+
{@const y = CY - (limits.speedMin - gs) * SPEED_PPU}
|
|
310
|
+
<rect x="62" {y} width="8" height={Math.max(0, HH - y)} fill="#ef4444" />
|
|
311
|
+
{/if}
|
|
312
|
+
{#if limits?.speedMax !== undefined}
|
|
313
|
+
{@const y = CY - (limits.speedMax - gs) * SPEED_PPU}
|
|
314
|
+
<rect x="62" y="0" width="8" height={Math.max(0, y)} fill="#ef4444" />
|
|
315
|
+
{/if}
|
|
316
|
+
{#each speedTicks as v (v)}
|
|
317
|
+
{@const y = CY - (v - gs) * SPEED_PPU}
|
|
318
|
+
<line x1={v % 5 === 0 ? 48 : 56} y1={y} x2="62" y2={y} stroke="#fff" stroke-width="1" />
|
|
319
|
+
{#if v % 5 === 0}<text x="42" y={y + 4} fill="#fff" font-size="11" text-anchor="end">{v}</text>{/if}
|
|
320
|
+
{/each}
|
|
321
|
+
</g>
|
|
322
|
+
<rect x="4" y={CY - 14} width="58" height="28" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
|
|
323
|
+
<text x="33" y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">{fmt(gs)}</text>
|
|
324
|
+
<rect x="0" y="0" width="70" height="20" rx="8" fill="#000" />
|
|
325
|
+
<rect x="0" y={HH - 20} width="70" height="20" rx="8" fill="#000" />
|
|
326
|
+
<text x="35" y="14" fill="#fff" font-size="10" text-anchor="middle" fill-opacity="0.7">GS м/с</text>
|
|
327
|
+
<text x="35" y={HH - 6} fill="#fff" font-size="11" text-anchor="middle">AS {fmt(as)}</text>
|
|
328
|
+
|
|
329
|
+
<!-- Лента высоты над точкой взлёта, красная зона выше altMax; вариометр справа -->
|
|
330
|
+
<g clip-path="url(#pfd-alt-{uid})">
|
|
331
|
+
<rect x={VW - 70} y="0" width="70" height={HH} fill="#000" fill-opacity="0.72" />
|
|
332
|
+
{#if limits?.altMax !== undefined}
|
|
333
|
+
{@const y = CY - (limits.altMax - alt) * ALT_PPU}
|
|
334
|
+
<rect x={VW - 70} y="0" width="8" height={Math.max(0, y)} fill="#ef4444" />
|
|
335
|
+
{/if}
|
|
336
|
+
{#each altTicks as v (v)}
|
|
337
|
+
{@const y = CY - (v - alt) * ALT_PPU}
|
|
338
|
+
<line x1={VW - 62} y1={y} x2={VW - (v % 10 === 0 ? 48 : 54)} y2={y} stroke="#fff" stroke-width="1" />
|
|
339
|
+
{#if v % 10 === 0}<text x={VW - 44} y={y + 4} fill="#fff" font-size="11">{v}</text>{/if}
|
|
340
|
+
{/each}
|
|
341
|
+
<rect x={VW - 6} y={Math.min(CY, CY - vsi)} width="4" height={Math.abs(vsi)} fill={climb >= 0 ? "#22c55e" : "#f97316"} />
|
|
342
|
+
</g>
|
|
343
|
+
<rect x={VW - 66} y={CY - 14} width="58" height="28" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
|
|
344
|
+
<text x={VW - 37} y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">{Math.round(alt)}</text>
|
|
345
|
+
<rect x={VW - 70} y="0" width="70" height="20" rx="8" fill="#000" />
|
|
346
|
+
<rect x={VW - 70} y={HH - 20} width="70" height="20" rx="8" fill="#000" />
|
|
347
|
+
<text x={VW - 35} y="14" fill="#fff" font-size="10" text-anchor="middle" fill-opacity="0.7">ALT м</text>
|
|
348
|
+
<text x={VW - 35} y={HH - 6} fill="#fff" font-size="11" text-anchor="middle">VS {climb >= 0 ? "+" : ""}{fmt(climb)}</text>
|
|
349
|
+
{/if}
|
|
350
|
+
|
|
351
|
+
<!-- Лента курса: трек (розовый), направление на дом (зелёный) -->
|
|
352
|
+
<g clip-path="url(#pfd-hdg-{uid})">
|
|
353
|
+
<rect x={HX} y={HH + 6} width={HW} height="44" fill="#000" fill-opacity="0.72" />
|
|
354
|
+
{#each hdgTicks as d (d)}
|
|
355
|
+
{@const x = CX + hdgOffset(d)}
|
|
356
|
+
<line x1={x} y1={HH + 6} x2={x} y2={HH + (d % 10 === 0 ? 18 : 12)} stroke="#fff" stroke-width="1" />
|
|
357
|
+
{#if d % 10 === 0}<text {x} y={HH + 34} fill="#fff" font-size="11" text-anchor="middle">{hdgLabel(d)}</text>{/if}
|
|
358
|
+
{/each}
|
|
359
|
+
{#if track !== null}
|
|
360
|
+
{@const x = CX + hdgOffset(track)}
|
|
361
|
+
<polygon points="{x},{HH + 8} {x - 6},{HH + 18} {x + 6},{HH + 18}" fill="#ec4899" />
|
|
362
|
+
{/if}
|
|
363
|
+
{#if home}
|
|
364
|
+
{@const x = CX + hdgOffset(home.bearing)}
|
|
365
|
+
<circle cx={x} cy={HH + 44} r="4" fill="#22c55e" />
|
|
366
|
+
{/if}
|
|
367
|
+
</g>
|
|
368
|
+
<rect x={CX - 26} y={HH + 20} width="52" height="24" rx="4" fill="#000" stroke="var(--accent-color)" stroke-width="1.5" />
|
|
369
|
+
<text x={CX} y={HH + 37} fill="#fff" font-size="14" font-weight="600" text-anchor="middle"
|
|
370
|
+
>{String(Math.round(wrap360(heading.current)) % 360).padStart(3, "0")}</text
|
|
371
|
+
>
|
|
372
|
+
|
|
373
|
+
<!-- Нет данных: флаг поверх прибора -->
|
|
374
|
+
{#if stale}
|
|
375
|
+
<g>
|
|
376
|
+
<rect x="0" y="0" width={VW} height="320" rx="10" fill="#000" fill-opacity="0.45" />
|
|
377
|
+
<line x1={HX + 20} y1="20" x2={HX + HW - 20} y2={HH - 20} stroke="#ef4444" stroke-width="3" />
|
|
378
|
+
<line x1={HX + HW - 20} y1="20" x2={HX + 20} y2={HH - 20} stroke="#ef4444" stroke-width="3" />
|
|
379
|
+
<rect x={CX - 70} y={CY - 16} width="140" height="32" rx="6" fill="#ef4444" />
|
|
380
|
+
<text x={CX} y={CY + 6} fill="#fff" font-size="15" font-weight="600" text-anchor="middle">НЕТ ДАННЫХ</text>
|
|
381
|
+
</g>
|
|
382
|
+
{/if}
|
|
383
|
+
</svg>
|
|
384
|
+
|
|
385
|
+
<!-- Значения под прибором -->
|
|
386
|
+
<div
|
|
387
|
+
class="flex flex-wrap gap-x-5 gap-y-1 rounded-[10px] border border-(--hairline-color) bg-(--back-color) px-3 py-2 text-[12px] whitespace-nowrap tabular-nums"
|
|
388
|
+
>
|
|
389
|
+
<span><span class="text-(--muted-color)">Координаты</span> {lat ? `${lat.toFixed(6)}, ${lon.toFixed(6)}` : "—"}</span>
|
|
390
|
+
<span><span class="text-(--muted-color)">Высота MSL</span> {Math.round(altMsl)} м</span>
|
|
391
|
+
<span><span class="text-(--muted-color)">Давление</span> {fmt(num(value?.Press))} гПа</span>
|
|
392
|
+
<span><span class="text-(--muted-color)">Температура</span> {fmt(num(value?.Temp))} °C</span>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!-- $lib/WidgetPFD/WidgetPFDProps.svelte — панель пропсов конструктора для WidgetPFD:
|
|
2
|
+
заголовок, префикс группы полей (по умолчанию MAV), мастер-цвет и границы цветовых зон. -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { T } from "../locales/i18n"
|
|
5
|
+
import * as UI from ".."
|
|
6
|
+
import { updateProperty, type UIComponent, type IWidgetPFDProps, type IWidgetPFDLimits } from "../types"
|
|
7
|
+
import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
|
|
8
|
+
import PropsGroup from "../PropsGroup.svelte"
|
|
9
|
+
|
|
10
|
+
const { component, onPropertyChange } = $props<{
|
|
11
|
+
component: UIComponent & { properties: Partial<IWidgetPFDProps> }
|
|
12
|
+
onPropertyChange: (updates: Partial<{ properties?: string | object; name?: string; access?: string }>) => void
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
/* Пустое поле — граница не задана (зона не рисуется) */
|
|
16
|
+
const setLimit = (key: keyof IWidgetPFDLimits, raw: unknown) => {
|
|
17
|
+
const text = String(raw ?? "").trim()
|
|
18
|
+
const next: IWidgetPFDLimits = { ...(component.properties.limits ?? {}) }
|
|
19
|
+
if (text === "" || !Number.isFinite(Number(text))) delete next[key]
|
|
20
|
+
else next[key] = Number(text)
|
|
21
|
+
updateProperty("limits", next, component, onPropertyChange)
|
|
22
|
+
}
|
|
23
|
+
const limits = $derived((component.properties.limits ?? {}) as IWidgetPFDLimits)
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="flex flex-col gap-2">
|
|
27
|
+
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
28
|
+
<UI.Input
|
|
29
|
+
label={{ name: "Заголовок" }}
|
|
30
|
+
value={component.properties.label?.name}
|
|
31
|
+
onUpdate={(value) => updateProperty("label.name", value as string, component, onPropertyChange)}
|
|
32
|
+
/>
|
|
33
|
+
<UI.Input
|
|
34
|
+
label={{ name: "Группа полей (ID)" }}
|
|
35
|
+
value={component.properties.id ?? "MAV"}
|
|
36
|
+
onUpdate={(value) => updateProperty("id", value as string, component, onPropertyChange)}
|
|
37
|
+
/>
|
|
38
|
+
<WidgetAccentPicker
|
|
39
|
+
value={component.properties.componentClass ?? ""}
|
|
40
|
+
onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
|
|
41
|
+
/>
|
|
42
|
+
</PropsGroup>
|
|
43
|
+
<PropsGroup label="Границы">
|
|
44
|
+
<div class="grid grid-cols-2 gap-2">
|
|
45
|
+
<UI.Input label={{ name: "Скорость мин., м/с" }} type="number" value={limits.speedMin} onUpdate={(v) => setLimit("speedMin", v)} />
|
|
46
|
+
<UI.Input label={{ name: "Скорость макс., м/с" }} type="number" value={limits.speedMax} onUpdate={(v) => setLimit("speedMax", v)} />
|
|
47
|
+
<UI.Input label={{ name: "Предельный крен, °" }} type="number" value={limits.bankMax} onUpdate={(v) => setLimit("bankMax", v)} />
|
|
48
|
+
<UI.Input label={{ name: "Высота макс., м" }} type="number" value={limits.altMax} onUpdate={(v) => setLimit("altMax", v)} />
|
|
49
|
+
<UI.Input label={{ name: "Низкий заряд, %" }} type="number" value={limits.battLow} onUpdate={(v) => setLimit("battLow", v)} />
|
|
50
|
+
<UI.Input label={{ name: "Нет данных через, мс" }} type="number" value={limits.staleMs ?? 2000} onUpdate={(v) => setLimit("staleMs", v)} />
|
|
51
|
+
</div>
|
|
52
|
+
</PropsGroup>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UIComponent, type IWidgetPFDProps } from "../types";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
component: UIComponent & {
|
|
4
|
+
properties: Partial<IWidgetPFDProps>;
|
|
5
|
+
};
|
|
6
|
+
onPropertyChange: (updates: Partial<{
|
|
7
|
+
properties?: string | object;
|
|
8
|
+
name?: string;
|
|
9
|
+
access?: string;
|
|
10
|
+
}>) => void;
|
|
11
|
+
};
|
|
12
|
+
declare const WidgetPFDProps: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type WidgetPFDProps = ReturnType<typeof WidgetPFDProps>;
|
|
14
|
+
export default WidgetPFDProps;
|
package/dist/index.d.ts
CHANGED
|
@@ -49,7 +49,9 @@ export { default as WidgetStackInfo } from "./WidgetStackInfo/WidgetStackInfo.sv
|
|
|
49
49
|
export { default as WidgetStackInfoProps } from "./WidgetStackInfo/WidgetStackInfoProps.svelte";
|
|
50
50
|
export { default as WidgetRGBStrip } from "./WidgetRGBStrip/WidgetRGBStrip.svelte";
|
|
51
51
|
export { default as WidgetRGBStripProps } from "./WidgetRGBStrip/WidgetRGBStripProps.svelte";
|
|
52
|
+
export { default as WidgetPFD } from "./WidgetPFD/WidgetPFD.svelte";
|
|
53
|
+
export { default as WidgetPFDProps } from "./WidgetPFD/WidgetPFDProps.svelte";
|
|
52
54
|
export * from "./locales/i18n";
|
|
53
55
|
export * from "./locales/translations";
|
|
54
56
|
export * from "./ModalStackStore";
|
|
55
|
-
export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, type IWidgetStackInfoProps, type IWidgetStackInfo, type IWidgetStackInfoHeap, type IWidgetStackInfoTask, type IWidgetRGBStripProps, type IWidgetRGBStripConfig, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
|
|
57
|
+
export { type UIComponent, type Position, type IUIComponentHandler, type IButtonProps, type IAccordionProps, type IInputProps, type ISelectProps, type IOption, type ISwitchProps, type IColorPickerProps, type IDividerProps, type ISliderProps, type ITextFieldProps, type IMapProps, type IProgressBarProps, type IWidgetProps, type IWidgetWiFiProps, type IWidgetWiFiConfig, type IWidgetDeviceInfoProps, type IWidgetDeviceInfoConfig, type IWidgetFileSystemProps, type IWidgetFileSystemInfo, type IWidgetFileSystemFile, type IWidgetFileSystemUpload, type IWidgetStackInfoProps, type IWidgetStackInfo, type IWidgetStackInfoHeap, type IWidgetStackInfoTask, type IWidgetRGBStripProps, type IWidgetRGBStripConfig, type IWidgetPFDProps, type IWidgetPFDData, type IWidgetPFDLimits, type IGraphProps, type IGraphDataObject, type IGraphHistoryPoint, type IGraphHistorySeries, type ITableHeader, type ITableText, type ITableSelect, type ITableButton, type ITableProgressBar, type ITableImage, type ITableContent, type ITableProps, type ITabsProps, type IJoystickProps, type IFileAttachProps, type IDeviceGNSS, type ICarouselProps, type IReceivingDataObject, type IVideoViewerProps, type IPeriodChartProps, type IPeriodChartLevel, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,8 @@ export { default as WidgetStackInfo } from "./WidgetStackInfo/WidgetStackInfo.sv
|
|
|
50
50
|
export { default as WidgetStackInfoProps } from "./WidgetStackInfo/WidgetStackInfoProps.svelte";
|
|
51
51
|
export { default as WidgetRGBStrip } from "./WidgetRGBStrip/WidgetRGBStrip.svelte";
|
|
52
52
|
export { default as WidgetRGBStripProps } from "./WidgetRGBStrip/WidgetRGBStripProps.svelte";
|
|
53
|
+
export { default as WidgetPFD } from "./WidgetPFD/WidgetPFD.svelte";
|
|
54
|
+
export { default as WidgetPFDProps } from "./WidgetPFD/WidgetPFDProps.svelte";
|
|
53
55
|
export * from "./locales/i18n";
|
|
54
56
|
export * from "./locales/translations";
|
|
55
57
|
export * from "./ModalStackStore";
|
|
@@ -128,6 +128,11 @@ const translations = {
|
|
|
128
128
|
'🎯 **LED strip widget** — controls an addressable strip: color, effect, speed and the strip itself.\n\n**🔗 Device link** — fields of the `LST` group (`RGB_Strip` driver), no variable binding needed\n- 🎨 `Color` — `[R, G, B, W]`; the white channel is shown only for the SK6812 model\n- ✨ `Effect` — static, rainbow, breath, palette, chase, twinkle, fire, theater chase; `Speed` — 0…10 on a non-linear scale, 0 is pause; rainbow, palette and fire ignore the color\n- 🔧 `Count`, `Model`, `Order` — number of LEDs, chip model, color order; `MaxCount` — firmware capacity (read only)\n\n**📤 Sending**\n- Color, effect and speed apply immediately\n- The "Hardware" block is edited locally and sent in one packet on "Save" — the strip restarts with the new settings\n\n**💡 Tips**\n- Wrong colors (red instead of green) — change the color order: WS2812 is usually GRB, some WS2811 are RGB or BRG\n- The data pin and transport (RMT/SPI) are set in firmware and can\'t be changed here\n\n⚙️ A group other than `LST` — change the prefix (ID) in widget settings.',
|
|
129
129
|
"🎯 **LED 灯带组件** —— 控制可寻址灯带:颜色、效果、速度以及灯带本身的参数。\n\n**🔗 与设备的关联** —— `LST` 分组的字段(`RGB_Strip` 驱动),无需绑定变量\n- 🎨 `Color` —— `[R, G, B, W]`;白色通道仅在 SK6812 型号时显示\n- ✨ `Effect` —— 静态、彩虹、呼吸、调色板、彗星、闪烁、火焰、跑马灯;`Speed` —— 0…10(非线性),0 为暂停;彩虹、调色板和火焰不使用颜色\n- 🔧 `Count`、`Model`、`Order` —— LED 数量、芯片型号、颜色顺序;`MaxCount` —— 固件中的容量(只读)\n\n**📤 发送**\n- 颜色、效果和速度立即生效\n- 「硬件」区块在本地编辑,点击「保存」一次性发送 —— 灯带将以新参数重启\n\n**💡 提示**\n- 颜色不对(红色变绿色)—— 更改颜色顺序:WS2812 通常为 GRB,部分 WS2811 为 RGB 或 BRG\n- 数据引脚和传输方式(RMT/SPI)在固件中设定,此处无法修改\n\n⚙️ 若分组不是 `LST` —— 在组件设置中修改前缀(ID)。",
|
|
130
130
|
],
|
|
131
|
+
"component.info.WidgetPFD": [
|
|
132
|
+
"🎯 **Пилотажный дисплей (PFD)** — главный прибор для контроля полёта: положение аппарата в пространстве, скорость, высота, курс и состояние систем.\n\n**🔗 Связь с устройством** — поля группы `MAV` (телеметрия автопилота MAVLink), привязывать переменные не нужно\n- ✈️ `Roll`, `Pitch`, `Heading` — авиагоризонт и лента курса (приходят ~20 раз в секунду, движение сглаживается)\n- 📈 `GSpeed`, `ASpeed` — лента путевой скорости и воздушная числом; `Alt`, `Climb` — лента высоты над точкой взлёта и вариометр\n- 🧭 `Vx`, `Vy` — трек (розовая метка на ленте курса), `HomeLat`/`HomeLon` — расстояние и направление на точку взлёта (зелёная метка)\n- 🛰️ Строка состояния: `Armed`, `Mode` (название по типу аппарата `VType`), GPS `Fix`/`Sat`, батарея `BV`/`BC`/`BRem`\n- 📍 Под прибором: `Lat`/`Lon`, `AltMSL`, `Press`, `Temp`\n\n**🚦 Границы** (в настройках виджета)\n- Мин./макс. скорость, макс. высота — красные зоны на лентах; предельный крен — красная дуга, указатель крена краснеет\n- Низкий заряд — батарея подсвечивается\n- Нет данных дольше заданного времени — прибор перечёркнут и помечен «НЕТ ДАННЫХ»\n\n**📱 Размер** — в узкой ячейке ленты скорости и высоты заменяются числами поверх горизонта.\n\n⚙️ Другая группа вместо `MAV` (например собственная курсовертикаль) — поменяйте префикс (ID) в настройках виджета.",
|
|
133
|
+
'🎯 **Primary flight display (PFD)** — the main instrument for flight monitoring: attitude, speed, altitude, heading and system status.\n\n**🔗 Device link** — fields of the `MAV` group (MAVLink autopilot telemetry), no variable binding needed\n- ✈️ `Roll`, `Pitch`, `Heading` — attitude indicator and heading tape (~20 times a second, motion is smoothed)\n- 📈 `GSpeed`, `ASpeed` — ground speed tape and airspeed as a number; `Alt`, `Climb` — altitude above home tape and vertical speed\n- 🧭 `Vx`, `Vy` — track (magenta mark on the heading tape), `HomeLat`/`HomeLon` — distance and direction to home (green mark)\n- 🛰️ Status bar: `Armed`, `Mode` (name by vehicle type `VType`), GPS `Fix`/`Sat`, battery `BV`/`BC`/`BRem`\n- 📍 Below the instrument: `Lat`/`Lon`, `AltMSL`, `Press`, `Temp`\n\n**🚦 Limits** (in widget settings)\n- Min/max speed, max altitude — red bands on the tapes; bank limit — red arc, the bank pointer turns red\n- Low battery — the battery is highlighted\n- No data for longer than the set time — the instrument is crossed out and marked "NO DATA"\n\n**📱 Size** — in a narrow cell the speed and altitude tapes become numbers over the horizon.\n\n⚙️ A group other than `MAV` (e.g. the device\'s own AHRS) — change the prefix (ID) in widget settings.',
|
|
134
|
+
"🎯 **主飞行显示器(PFD)** —— 飞行监控的主要仪表:姿态、速度、高度、航向和系统状态。\n\n**🔗 与设备的关联** —— `MAV` 分组的字段(MAVLink 自驾仪遥测),无需绑定变量\n- ✈️ `Roll`、`Pitch`、`Heading` —— 姿态仪和航向带(约每秒 20 次,运动经过平滑)\n- 📈 `GSpeed`、`ASpeed` —— 地速带和空速数值;`Alt`、`Climb` —— 相对起飞点高度带和升降速度\n- 🧭 `Vx`、`Vy` —— 航迹(航向带上的粉色标记),`HomeLat`/`HomeLon` —— 到起飞点的距离和方向(绿色标记)\n- 🛰️ 状态栏:`Armed`、`Mode`(按机型 `VType` 显示名称)、GPS `Fix`/`Sat`、电池 `BV`/`BC`/`BRem`\n- 📍 仪表下方:`Lat`/`Lon`、`AltMSL`、`Press`、`Temp`\n\n**🚦 限值**(在组件设置中)\n- 最小/最大速度、最大高度 —— 速度带和高度带上的红色区域;最大坡度 —— 红色弧线,坡度指针变红\n- 低电量 —— 电池高亮\n- 超过设定时间无数据 —— 仪表被划叉并标记「无数据」\n\n**📱 尺寸** —— 在窄单元格中,速度带和高度带变为地平线上的数字。\n\n⚙️ 若分组不是 `MAV`(例如设备自身的航姿系统)—— 在组件设置中修改前缀(ID)。",
|
|
135
|
+
],
|
|
131
136
|
"component.info.WidgetStackInfo": [
|
|
132
137
|
"🎯 **Виджет «Память и задачи»** — отладка: свободная память и запас стека задач прошивки.\n\n**🔗 Связь с устройством** — один объект `StackInfo` из ответа на `GET StackInfo`\n- 📊 **Кучи** `Heap[]`: занято / всего, свободно и минимум свободного с момента старта; светлая часть полосы — пик занятости\n- 🧵 **Задачи** `Tasks[]`: имя, состояние, приоритет, ядро (ESP32), минимальный запас стека в байтах\n\n**⚙️ Настройки**\n- Порог запаса стека — меньше порога красным, меньше удвоенного — оранжевым\n- Автоопрос: выкл / 1 / 5 / 10 с; свёрнутый виджет не опрашивает\n\n💡 Задачи всегда отсортированы по запасу стека — самые рискованные сверху.",
|
|
133
138
|
"🎯 **Memory and tasks widget** — debugging: free memory and firmware task stack headroom.\n\n**🔗 Device link** — a single `StackInfo` object from the `GET StackInfo` response\n- 📊 **Heaps** `Heap[]`: used / total, free and minimum free since boot; the light part of the bar is the usage peak\n- 🧵 **Tasks** `Tasks[]`: name, state, priority, core (ESP32), minimum stack headroom in bytes\n\n**⚙️ Settings**\n- Stack headroom threshold — below it red, below double — orange\n- Auto-polling: off / 1 / 5 / 10 s; a collapsed widget doesn't poll\n\n💡 Tasks are always sorted by stack headroom — the riskiest on top.",
|
package/dist/types.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare const updateComponent: (component: UIComponent, updates: Partial<
|
|
|
15
15
|
}>) => {
|
|
16
16
|
access: "full" | "viewOnly" | "hidden" | undefined;
|
|
17
17
|
name: string | undefined;
|
|
18
|
-
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps<unknown> | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps | IWidgetStackInfoProps | IWidgetRGBStripProps;
|
|
18
|
+
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps<unknown> | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps | IWidgetStackInfoProps | IWidgetRGBStripProps | IWidgetPFDProps;
|
|
19
19
|
eventHandler: IUIComponentHandler | undefined;
|
|
20
20
|
id: string;
|
|
21
|
-
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem" | "WidgetStackInfo" | "WidgetRGBStrip";
|
|
21
|
+
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem" | "WidgetStackInfo" | "WidgetRGBStrip" | "WidgetPFD";
|
|
22
22
|
position: Required<Position>;
|
|
23
23
|
parentId: string;
|
|
24
24
|
};
|
|
@@ -26,8 +26,8 @@ export interface UIComponent {
|
|
|
26
26
|
id: string;
|
|
27
27
|
name?: string;
|
|
28
28
|
access?: "full" | "viewOnly" | "hidden";
|
|
29
|
-
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem" | "WidgetStackInfo" | "WidgetRGBStrip";
|
|
30
|
-
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps | IWidgetStackInfoProps | IWidgetRGBStripProps;
|
|
29
|
+
type: "Accordion" | "Button" | "ColorPicker" | "Divider" | "FileAttach" | "Graph" | "Input" | "Joystick" | "Map" | "PeriodChart" | "ProgressBar" | "Select" | "Slider" | "Switch" | "Table" | "Tabs" | "TextField" | "VideoViewer" | "Widget" | "WidgetWiFi" | "WidgetDeviceInfo" | "WidgetFileSystem" | "WidgetStackInfo" | "WidgetRGBStrip" | "WidgetPFD";
|
|
30
|
+
properties: IAccordionProps | IButtonProps | IColorPickerProps | IDividerProps | IFileAttachProps | IGraphProps | IInputProps | IJoystickProps | IMapProps | IPeriodChartProps | IProgressBarProps | ISelectProps | ISliderProps | ISwitchProps | ITableProps<object> | ITabsProps | ITextFieldProps | IWidgetProps | IWidgetWiFiProps | IWidgetDeviceInfoProps | IWidgetFileSystemProps | IWidgetStackInfoProps | IWidgetRGBStripProps | IWidgetPFDProps;
|
|
31
31
|
position: Required<Position>;
|
|
32
32
|
parentId: string;
|
|
33
33
|
eventHandler?: IUIComponentHandler;
|
|
@@ -655,3 +655,50 @@ export interface IWidgetRGBStripProps {
|
|
|
655
655
|
onChange?: (field: "Color" | "Effect" | "Speed", value: number | number[]) => void;
|
|
656
656
|
onSave?: (hardware: Pick<IWidgetRGBStripConfig, "Count" | "Model" | "Order">) => void;
|
|
657
657
|
}
|
|
658
|
+
export interface IWidgetPFDData {
|
|
659
|
+
Roll: number;
|
|
660
|
+
Pitch: number;
|
|
661
|
+
Heading: number;
|
|
662
|
+
GSpeed: number;
|
|
663
|
+
ASpeed: number;
|
|
664
|
+
Climb: number;
|
|
665
|
+
Alt: number;
|
|
666
|
+
AltMSL: number;
|
|
667
|
+
Lat: number;
|
|
668
|
+
Lon: number;
|
|
669
|
+
Vx: number;
|
|
670
|
+
Vy: number;
|
|
671
|
+
Fix: number;
|
|
672
|
+
Sat: number;
|
|
673
|
+
BV: number;
|
|
674
|
+
BC: number;
|
|
675
|
+
BRem: number;
|
|
676
|
+
Mode: number;
|
|
677
|
+
VType: number;
|
|
678
|
+
Armed: number;
|
|
679
|
+
HomeLat: number;
|
|
680
|
+
HomeLon: number;
|
|
681
|
+
Press: number;
|
|
682
|
+
Temp: number;
|
|
683
|
+
}
|
|
684
|
+
export interface IWidgetPFDLimits {
|
|
685
|
+
speedMin?: number;
|
|
686
|
+
speedMax?: number;
|
|
687
|
+
bankMax?: number;
|
|
688
|
+
altMax?: number;
|
|
689
|
+
battLow?: number;
|
|
690
|
+
staleMs?: number;
|
|
691
|
+
}
|
|
692
|
+
export interface IWidgetPFDProps {
|
|
693
|
+
id?: string;
|
|
694
|
+
wrapperClass?: string;
|
|
695
|
+
componentClass?: string;
|
|
696
|
+
label?: {
|
|
697
|
+
name?: string;
|
|
698
|
+
};
|
|
699
|
+
value?: Partial<IWidgetPFDData>;
|
|
700
|
+
keys?: Partial<Record<keyof IWidgetPFDData, string>>;
|
|
701
|
+
limits?: IWidgetPFDLimits;
|
|
702
|
+
collapsed?: boolean;
|
|
703
|
+
persistKey?: string;
|
|
704
|
+
}
|
package/package.json
CHANGED