poe-svelte-ui-lib 1.84.0 → 1.85.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/dist/Graph/GraphProps.svelte +34 -33
- package/dist/Graph/README.md +2 -2
- package/dist/WidgetMap/README.md +2 -1
- package/dist/WidgetMap/WidgetMap.svelte +121 -11
- package/dist/WidgetMap/WidgetMapProps.svelte +48 -42
- package/dist/WidgetPFD/README.md +1 -1
- package/dist/WidgetPFD/WidgetPFDProps.svelte +29 -24
- package/dist/locales/translations.js +1 -1
- package/package.json +1 -1
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
{#snippet GraphIsTest()}
|
|
29
29
|
<UI.Switch
|
|
30
|
-
|
|
31
|
-
label={{ name: $T("constructor.props.istest") }}
|
|
30
|
+
label={{ name: $T("constructor.props.simulation") }}
|
|
32
31
|
value={component.properties.isTest ? 1 : 0}
|
|
33
32
|
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
34
33
|
onChange={(value) => updateProperty("isTest", value === 1, component, onPropertyChange)}
|
|
@@ -44,35 +43,37 @@
|
|
|
44
43
|
/>
|
|
45
44
|
{/snippet}
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
<div class="flex flex-col gap-2">
|
|
47
|
+
<PropsGroup>
|
|
48
|
+
{@render GraphIsTest()}
|
|
49
|
+
</PropsGroup>
|
|
50
|
+
<!-- В режиме имитации остальные свойства не учитываются, поэтому вся панель бледная и недоступна -->
|
|
51
|
+
<div class={twMerge("flex flex-col gap-2 transition-opacity", component.properties.isTest && "opacity-40 grayscale")} inert={component.properties.isTest}>
|
|
52
|
+
{#if forConstructor}
|
|
53
|
+
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
54
|
+
<CommonSnippets snippet="Access" {component} {onPropertyChange} />
|
|
55
|
+
<CommonSnippets snippet="Variable" {VARIABLE_OPTIONS} {component} {onPropertyChange} />
|
|
56
|
+
</PropsGroup>
|
|
57
|
+
<PropsGroup label={$T("constructor.props.group.appearance")}>
|
|
58
|
+
<CommonSnippets snippet="Label" {component} {onPropertyChange} />
|
|
59
|
+
<CommonSnippets snippet="LabelAlign" initialValue={initialAlign} {component} {onPropertyChange} />
|
|
60
|
+
</PropsGroup>
|
|
61
|
+
<PropsGroup label={$T("constructor.props.group.behavior")}>
|
|
62
|
+
{@render GraphSettings()}
|
|
63
|
+
</PropsGroup>
|
|
64
|
+
{:else}
|
|
65
|
+
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
66
|
+
<CommonSnippets snippet="Identificator" {component} {onPropertyChange} />
|
|
67
|
+
<CommonSnippets snippet="Access" {component} {onPropertyChange} />
|
|
68
|
+
</PropsGroup>
|
|
69
|
+
<PropsGroup label={$T("constructor.props.group.appearance")}>
|
|
70
|
+
<CommonSnippets snippet="Label" {component} {onPropertyChange} />
|
|
71
|
+
<CommonSnippets snippet="LabelClass" initialValue={initialAlign} {component} {onPropertyChange} />
|
|
72
|
+
</PropsGroup>
|
|
73
|
+
<PropsGroup label={$T("constructor.props.group.behavior")}>
|
|
74
|
+
<CommonSnippets snippet="WrapperClass" {component} {onPropertyChange} />
|
|
75
|
+
{@render GraphSettings()}
|
|
76
|
+
</PropsGroup>
|
|
77
|
+
{/if}
|
|
61
78
|
</div>
|
|
62
|
-
|
|
63
|
-
<div class="flex flex-col gap-2">
|
|
64
|
-
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
65
|
-
<CommonSnippets snippet="Identificator" {component} {onPropertyChange} />
|
|
66
|
-
<CommonSnippets snippet="Access" {component} {onPropertyChange} />
|
|
67
|
-
</PropsGroup>
|
|
68
|
-
<PropsGroup label={$T("constructor.props.group.appearance")}>
|
|
69
|
-
<CommonSnippets snippet="Label" {component} {onPropertyChange} />
|
|
70
|
-
<CommonSnippets snippet="LabelClass" initialValue={initialAlign} {component} {onPropertyChange} />
|
|
71
|
-
</PropsGroup>
|
|
72
|
-
<PropsGroup label={$T("constructor.props.group.behavior")}>
|
|
73
|
-
<CommonSnippets snippet="WrapperClass" {component} {onPropertyChange} />
|
|
74
|
-
{@render GraphIsTest()}
|
|
75
|
-
{@render GraphSettings()}
|
|
76
|
-
</PropsGroup>
|
|
77
|
-
</div>
|
|
78
|
-
{/if}
|
|
79
|
+
</div>
|
package/dist/Graph/README.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
**Статический.** Если передан непустой `historyData`, график рисует готовый ряд с отметками времени, например энергию за сутки по часам. Точки сортируются по времени сами. Новый массив `historyData` перерисовывает график целиком.
|
|
37
37
|
|
|
38
|
-
`isTest` включает случайные
|
|
38
|
+
`isTest` включает режим имитации: случайные данные, точка каждые 50 мс. Удобно, чтобы посмотреть график без устройства. Пока режим включён, данные устройства и `refreshRate` не учитываются. В конструкторе все остальные свойства становятся бледными и недоступными, пока имитацию не выключат.
|
|
39
39
|
|
|
40
40
|
## Пропсы
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
| `streamingData.data` | `{ name, value }[] \| null` | `[]` | Текущие значения линий для живого режима |
|
|
48
48
|
| `refreshRate` | `number` | `0` | Период добавления точек, мс. `0` — по изменению значений |
|
|
49
49
|
| `historyData` | `{ name, points: { value, timestamp }[] }[]` | `[]` | Готовый ряд для статического режима. `timestamp` в миллисекундах, как `Date.now()` |
|
|
50
|
-
| `isTest` | `boolean` | `false` |
|
|
50
|
+
| `isTest` | `boolean` | `false` | Режим имитации: случайные данные, точка каждые 50 мс |
|
|
51
51
|
|
|
52
52
|
Колбэков нет.
|
|
53
53
|
|
package/dist/WidgetMap/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
52
52
|
- **Траектория.** Точка добавляется только при смене координат. Курс и высота могут меняться чаще.
|
|
53
53
|
- **«Показать все»** в шапке вписывает все видимые объекты в экран, один объект приближает. Сама карта вид не меняет, ведь объектов может быть много.
|
|
54
54
|
- **Клик по маркеру** открывает карточку: имя, группа, координаты, высота, курс, спутники, серийный номер. Кнопки «Копировать» (всё в буфер обмена) и «Скрыть» (до следующего обновления объекта).
|
|
55
|
+
- **Слежение.** Кнопка «Следить» в карточке залипает. Один отслеживаемый объект всегда в центре карты. Несколько вписываются в карту при включении слежения, дальше карта держит в центре их середину. Масштаб остаётся за оператором: карта сама отдаляется, только когда объект уходит за край. Под маркером отслеживаемого объекта точка, она видна и при закрытой карточке. Если объект пропал с карты, слежение за ним продолжится, когда он появится снова. «Скрыть» снимает слежение.
|
|
55
56
|
- **Шапка** показывает, сколько объектов на связи. Зелёный значит все, серый часть, красный ни одного.
|
|
56
57
|
- **Только 2D.** Наклонить карту нельзя. Правая кнопка мыши только поворачивает карту, компас под кнопками масштаба возвращает её на север.
|
|
57
58
|
- Карта светлая или тёмная по теме страницы и переключается вместе с ней.
|
|
@@ -61,7 +62,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
61
62
|
|
|
62
63
|
## Режим имитации
|
|
63
64
|
|
|
64
|
-
`isTest` включает
|
|
65
|
+
`isTest` включает имитацию шести аппаратов `SIM-1`-`SIM-6` без устройства. Они летают над одним районом Минска, в пределах пары километров, и обновляются раз в полсекунды. На связи всегда три аппарата. Через 40-100 секунд один из них пропадает, и его сразу сменяет тот, кто молчит дольше всех. Поэтому видно все состояния маркера: свежий, красный устаревший, скрытый и снова появившийся. При включении карта переходит к этому району. Пока режим включён, `value` и `groups` не учитываются. В конструкторе все остальные свойства становятся бледными и недоступными, пока имитацию не выключат.
|
|
65
66
|
|
|
66
67
|
## Пропсы
|
|
67
68
|
|
|
@@ -65,17 +65,69 @@
|
|
|
65
65
|
/* (0, 0) и нечисловые координаты — признак отсутствия фикса */
|
|
66
66
|
const hasFix = (lat: unknown, lon: unknown) => Number.isFinite(lat) && Number.isFinite(lon) && (lat !== 0 || lon !== 0)
|
|
67
67
|
|
|
68
|
-
/* Режим имитации:
|
|
69
|
-
const
|
|
70
|
-
const
|
|
68
|
+
/* Режим имитации: шесть аппаратов над одним районом сами выходят на связь и пропадают, данные и группы устройства не учитываются */
|
|
69
|
+
const SIM_COUNT = 6
|
|
70
|
+
const SIM_ONLINE = 3
|
|
71
|
+
const SIM_GROUPS = Array.from({ length: SIM_COUNT }, (_, i) => `SIM-${i + 1}`)
|
|
72
|
+
const SIM_SN = [
|
|
73
|
+
"10.10.001-01_0000804500006B3600004000:F76F",
|
|
74
|
+
"10.10.001-01_0000441B0000F68C0000561C:36B1",
|
|
75
|
+
"20.00.000-01_000068B60000B33B0000A520:5180",
|
|
76
|
+
"00.00.000-00_0000AC670000B22B00007428:DBC5",
|
|
77
|
+
"25.00.000-02_0000A0850000E31300004C5C:8D50",
|
|
78
|
+
"50.00.000-00_0000DCDA00000C1800003194:2BC6",
|
|
79
|
+
]
|
|
80
|
+
/* Сдвиг маршрута каждого аппарата, м: север, восток — чтобы не летали друг за другом */
|
|
81
|
+
const SIM_SHIFT: [number, number][] = [
|
|
82
|
+
[0, 0],
|
|
83
|
+
[-400, 700],
|
|
84
|
+
[700, -900],
|
|
85
|
+
[-800, 600],
|
|
86
|
+
[1100, 900],
|
|
87
|
+
[-600, -1200],
|
|
88
|
+
]
|
|
89
|
+
const randomSec = (min: number, max: number) => (min + Math.random() * (max - min)) * 1000
|
|
71
90
|
let simValue: Record<string, Partial<IWidgetMapData>> | undefined = $state()
|
|
72
91
|
$effect(() => {
|
|
73
92
|
if (!isTest) return
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
/* На связи всегда три аппарата: когда один пропадает, его сменяет тот, кто молчит дольше всех; молчащий маркер успевает покраснеть и скрыться */
|
|
94
|
+
const fleet = SIM_GROUPS.map((group, i) => ({
|
|
95
|
+
group,
|
|
96
|
+
sim: createFlightSim(i % 7),
|
|
97
|
+
online: i < SIM_ONLINE,
|
|
98
|
+
switchAt: i < SIM_ONLINE ? Date.now() + randomSec(20 + i * 25, 40 + i * 25) : 0,
|
|
99
|
+
offlineSince: i,
|
|
100
|
+
}))
|
|
101
|
+
const kmLat = 111320
|
|
102
|
+
const kmLon = 111320 * Math.cos((HOME.lat * Math.PI) / 180)
|
|
103
|
+
const frames: Record<string, Partial<IWidgetMapData>> = {}
|
|
104
|
+
/* Модель летит всегда; пока аппарат без связи, его последний кадр не меняется — маркер краснеет, затем скрывается */
|
|
105
|
+
const tick = (dt: number) => {
|
|
106
|
+
const t = Date.now()
|
|
107
|
+
fleet.forEach((f, i) => {
|
|
108
|
+
const data = f.sim(dt)
|
|
109
|
+
if (f.online && t >= f.switchAt) {
|
|
110
|
+
const next = fleet.filter((x) => !x.online).sort((a, b) => a.offlineSince - b.offlineSince)[0]
|
|
111
|
+
f.online = false
|
|
112
|
+
f.offlineSince = t
|
|
113
|
+
next.online = true
|
|
114
|
+
next.switchAt = t + randomSec(40, 100)
|
|
115
|
+
}
|
|
116
|
+
if (!f.online) return
|
|
117
|
+
frames[f.group] = {
|
|
118
|
+
Lat: data.Lat + SIM_SHIFT[i][0] / kmLat,
|
|
119
|
+
Lon: data.Lon + SIM_SHIFT[i][1] / kmLon,
|
|
120
|
+
Alt: data.Alt + i * 15,
|
|
121
|
+
Heading: data.Heading,
|
|
122
|
+
Sat: data.Sat - (i % 3),
|
|
123
|
+
DevName: f.group,
|
|
124
|
+
DevSN: SIM_SN[i],
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
simValue = { ...frames }
|
|
128
|
+
}
|
|
129
|
+
tick(0)
|
|
130
|
+
const timer = setInterval(() => tick(0.5), 500)
|
|
79
131
|
return () => {
|
|
80
132
|
clearInterval(timer)
|
|
81
133
|
simValue = undefined
|
|
@@ -171,8 +223,44 @@
|
|
|
171
223
|
const hideBoard = (group: string) => {
|
|
172
224
|
const b = boards.find((x) => x.group === group)
|
|
173
225
|
if (b) b.hiddenAt = b.lastChange
|
|
226
|
+
followed = followed.filter((g) => g !== group)
|
|
174
227
|
}
|
|
175
228
|
|
|
229
|
+
/* Слежение: один объект держится в центре карты, несколько — вписываются в карту при включении и дальше только центрируются; масштаб остаётся за оператором, карта отдаляется сама, лишь когда объект уходит за край */
|
|
230
|
+
let followed: string[] = $state([])
|
|
231
|
+
const toggleFollow = (group: string) => (followed = followed.includes(group) ? followed.filter((g) => g !== group) : [...followed, group])
|
|
232
|
+
const FOLLOW_PADDING = 60
|
|
233
|
+
let lastFollowKey = ""
|
|
234
|
+
$effect(() => {
|
|
235
|
+
const map = mapInstance
|
|
236
|
+
const targets = visibleBoards.filter((b) => followed.includes(b.group))
|
|
237
|
+
if (!map || targets.length === 0) return
|
|
238
|
+
untrack(() => {
|
|
239
|
+
if (targets.length === 1) return map.easeTo({ center: [targets[0].lon, targets[0].lat], duration: 300 })
|
|
240
|
+
const lons = targets.map((b) => b.lon)
|
|
241
|
+
const lats = targets.map((b) => b.lat)
|
|
242
|
+
const bounds: [[number, number], [number, number]] = [
|
|
243
|
+
[Math.min(...lons), Math.min(...lats)],
|
|
244
|
+
[Math.max(...lons), Math.max(...lats)],
|
|
245
|
+
]
|
|
246
|
+
const key = targets.map((b) => b.group).join(" ")
|
|
247
|
+
if (key !== lastFollowKey) {
|
|
248
|
+
lastFollowKey = key
|
|
249
|
+
return map.fitBounds(bounds, { padding: FOLLOW_PADDING, maxZoom: 16, duration: 300 })
|
|
250
|
+
}
|
|
251
|
+
const center: [number, number] = [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]
|
|
252
|
+
/* Помещаются ли все объекты, если поставить их середину в центр */
|
|
253
|
+
const { width, height } = map.getContainer().getBoundingClientRect()
|
|
254
|
+
const mid = map.project(center)
|
|
255
|
+
const fits = targets.every((b) => {
|
|
256
|
+
const p = map.project([b.lon, b.lat])
|
|
257
|
+
return Math.abs(p.x - mid.x) <= width / 2 - FOLLOW_PADDING && Math.abs(p.y - mid.y) <= height / 2 - FOLLOW_PADDING
|
|
258
|
+
})
|
|
259
|
+
if (fits) map.easeTo({ center, duration: 300 })
|
|
260
|
+
else map.fitBounds(bounds, { padding: FOLLOW_PADDING, maxZoom: map.getZoom(), duration: 300 })
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
176
264
|
const copyBoard = (b: Board) => {
|
|
177
265
|
navigator.clipboard.writeText(
|
|
178
266
|
`DevName: ${b.name}\nDevSN: ${b.sn}\nGroup: ${b.group}\nLat: ${b.lat.toFixed(6)} | Lon: ${b.lon.toFixed(6)} | Alt: ${b.alt.toFixed(1)}\nHeading: ${b.heading.toFixed(1)} | Satellites: ${b.sat}`,
|
|
@@ -287,6 +375,7 @@
|
|
|
287
375
|
{/if}
|
|
288
376
|
|
|
289
377
|
{#each visibleBoards as board (board.group)}
|
|
378
|
+
{@const isFollowed = followed.includes(board.group)}
|
|
290
379
|
<Marker lnglat={{ lng: board.lon, lat: board.lat }}>
|
|
291
380
|
{#snippet content()}
|
|
292
381
|
<div class="flex flex-col items-center justify-center leading-none">
|
|
@@ -318,6 +407,10 @@
|
|
|
318
407
|
{/if}
|
|
319
408
|
</div>
|
|
320
409
|
<p class="font-bold">{board.name}</p>
|
|
410
|
+
<!-- Метка слежения видна и при закрытом окне деталей -->
|
|
411
|
+
{#if isFollowed}
|
|
412
|
+
<span class="mt-0.5 size-2 rounded-full bg-(--accent-color) ring-2 ring-white" title={$T("widget.map.follow")}></span>
|
|
413
|
+
{/if}
|
|
321
414
|
</div>
|
|
322
415
|
{/snippet}
|
|
323
416
|
|
|
@@ -350,8 +443,8 @@
|
|
|
350
443
|
|
|
351
444
|
{#if board.sn}<p class="text-center font-mono text-[0.6rem] tracking-wide uppercase">{board.sn}</p>{/if}
|
|
352
445
|
|
|
353
|
-
<div class="
|
|
354
|
-
<div class="relative flex items-center">
|
|
446
|
+
<div class="grid grid-cols-[1fr_auto_1fr] items-center border-t border-(--hairline-color) pt-1">
|
|
447
|
+
<div class="relative flex items-center justify-self-start">
|
|
355
448
|
<button
|
|
356
449
|
class="flex cursor-pointer items-center gap-1 rounded-lg border-none bg-transparent px-1 py-0.5 opacity-70 transition hover:opacity-100"
|
|
357
450
|
onclick={(e) => {
|
|
@@ -374,7 +467,24 @@
|
|
|
374
467
|
</div>
|
|
375
468
|
|
|
376
469
|
<button
|
|
377
|
-
class="cursor-pointer rounded-lg border
|
|
470
|
+
class="flex cursor-pointer items-center gap-1 rounded-lg border px-2 py-0.5 text-xs font-semibold transition {isFollowed
|
|
471
|
+
? 'border-(--accent-color) bg-(--accent-color) text-white'
|
|
472
|
+
: 'border-(--hairline-color) bg-transparent opacity-80 hover:opacity-100'}"
|
|
473
|
+
aria-pressed={isFollowed}
|
|
474
|
+
onclick={() => toggleFollow(board.group)}
|
|
475
|
+
>
|
|
476
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 24 24">
|
|
477
|
+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5">
|
|
478
|
+
<circle cx="12" cy="12" r="7" />
|
|
479
|
+
<circle cx="12" cy="12" r="2.5" fill={isFollowed ? "currentColor" : "none"} />
|
|
480
|
+
<path d="M12 2v3m0 14v3M2 12h3m14 0h3" />
|
|
481
|
+
</g>
|
|
482
|
+
</svg>
|
|
483
|
+
{$T("widget.map.follow")}
|
|
484
|
+
</button>
|
|
485
|
+
|
|
486
|
+
<button
|
|
487
|
+
class="cursor-pointer justify-self-end rounded-lg border-none bg-transparent p-0.5 text-(--red-color) transition hover:opacity-80"
|
|
378
488
|
aria-label="Hide"
|
|
379
489
|
title="Hide"
|
|
380
490
|
onclick={() => hideBoard(board.group)}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
|
|
9
9
|
import CommonSnippets from "../CommonSnippets.svelte"
|
|
10
10
|
import PropsGroup from "../PropsGroup.svelte"
|
|
11
|
+
import { twMerge } from "tailwind-merge"
|
|
11
12
|
|
|
12
13
|
const {
|
|
13
14
|
component,
|
|
@@ -37,53 +38,58 @@
|
|
|
37
38
|
</script>
|
|
38
39
|
|
|
39
40
|
<div class="flex flex-col gap-2">
|
|
40
|
-
<PropsGroup
|
|
41
|
-
<TextsEditor {component} {onPropertyChange} />
|
|
41
|
+
<PropsGroup>
|
|
42
42
|
<UI.Switch
|
|
43
43
|
label={{ name: $T("constructor.props.simulation") }}
|
|
44
44
|
value={component.properties.isTest ? 1 : 0}
|
|
45
45
|
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
46
46
|
onChange={(value) => updateProperty("isTest", value === 1, component, onPropertyChange)}
|
|
47
47
|
/>
|
|
48
|
-
<UI.Input label={{ name: $T("widget.map.groups") }} disabled={component.properties.isTest} value={(component.properties.groups ?? ["MAV"]).join(" ")} onUpdate={setGroups} />
|
|
49
|
-
<WidgetAccentPicker
|
|
50
|
-
value={component.properties.componentClass ?? ""}
|
|
51
|
-
onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
|
|
52
|
-
/>
|
|
53
|
-
</PropsGroup>
|
|
54
|
-
<PropsGroup label={$T("constructor.props.group.appearance")}>
|
|
55
|
-
<UI.Select
|
|
56
|
-
label={{ name: $T("widget.map.height") }}
|
|
57
|
-
type="buttons"
|
|
58
|
-
options={HEIGHT_OPTIONS}
|
|
59
|
-
value={HEIGHT_OPTIONS.find((opt) => opt.value === (component.properties.height ?? 400)) ?? HEIGHT_OPTIONS[1]}
|
|
60
|
-
onUpdate={(option) => updateProperty("height", (option as { value: number }).value, component, onPropertyChange)}
|
|
61
|
-
/>
|
|
62
|
-
<CommonSnippets
|
|
63
|
-
snippet="IconsLib"
|
|
64
|
-
initialValue={{
|
|
65
|
-
name: $T("constructor.props.markerIcon"),
|
|
66
|
-
icon: component.properties.markerIcon,
|
|
67
|
-
updateProperty: (icon: string) => updateProperty("markerIcon", icon as string, component, onPropertyChange),
|
|
68
|
-
icons: { array: ICONS },
|
|
69
|
-
}}
|
|
70
|
-
{component}
|
|
71
|
-
{onPropertyChange}
|
|
72
|
-
/>
|
|
73
|
-
</PropsGroup>
|
|
74
|
-
<PropsGroup label={$T("widget.map.defaults")}>
|
|
75
|
-
<UI.Switch
|
|
76
|
-
label={{ name: $T("constructor.props.map.track") }}
|
|
77
|
-
value={component.properties.trackEnabled ? 1 : 0}
|
|
78
|
-
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
79
|
-
onChange={(value) => updateProperty("trackEnabled", Boolean(value), component, onPropertyChange)}
|
|
80
|
-
/>
|
|
81
|
-
<UI.Select
|
|
82
|
-
label={{ name: $T("constructor.props.map.trackLength") }}
|
|
83
|
-
type="input"
|
|
84
|
-
options={TRACK_LENGTH_OPTIONS}
|
|
85
|
-
value={TRACK_LENGTH_OPTIONS.find((opt) => opt.value === (component.properties.trackLength ?? 1000))}
|
|
86
|
-
onUpdate={(option) => updateProperty("trackLength", (option as { value: number }).value, component, onPropertyChange)}
|
|
87
|
-
/>
|
|
88
48
|
</PropsGroup>
|
|
49
|
+
<!-- В режиме имитации остальные свойства не учитываются, поэтому вся панель бледная и недоступна -->
|
|
50
|
+
<div class={twMerge("flex flex-col gap-2 transition-opacity", component.properties.isTest && "opacity-40 grayscale")} inert={component.properties.isTest}>
|
|
51
|
+
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
52
|
+
<TextsEditor {component} {onPropertyChange} />
|
|
53
|
+
<UI.Input label={{ name: $T("widget.map.groups") }} value={(component.properties.groups ?? ["MAV"]).join(" ")} onUpdate={setGroups} />
|
|
54
|
+
<WidgetAccentPicker
|
|
55
|
+
value={component.properties.componentClass ?? ""}
|
|
56
|
+
onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
|
|
57
|
+
/>
|
|
58
|
+
</PropsGroup>
|
|
59
|
+
<PropsGroup label={$T("constructor.props.group.appearance")}>
|
|
60
|
+
<UI.Select
|
|
61
|
+
label={{ name: $T("widget.map.height") }}
|
|
62
|
+
type="buttons"
|
|
63
|
+
options={HEIGHT_OPTIONS}
|
|
64
|
+
value={HEIGHT_OPTIONS.find((opt) => opt.value === (component.properties.height ?? 400)) ?? HEIGHT_OPTIONS[1]}
|
|
65
|
+
onUpdate={(option) => updateProperty("height", (option as { value: number }).value, component, onPropertyChange)}
|
|
66
|
+
/>
|
|
67
|
+
<CommonSnippets
|
|
68
|
+
snippet="IconsLib"
|
|
69
|
+
initialValue={{
|
|
70
|
+
name: $T("constructor.props.markerIcon"),
|
|
71
|
+
icon: component.properties.markerIcon,
|
|
72
|
+
updateProperty: (icon: string) => updateProperty("markerIcon", icon as string, component, onPropertyChange),
|
|
73
|
+
icons: { array: ICONS },
|
|
74
|
+
}}
|
|
75
|
+
{component}
|
|
76
|
+
{onPropertyChange}
|
|
77
|
+
/>
|
|
78
|
+
</PropsGroup>
|
|
79
|
+
<PropsGroup label={$T("widget.map.defaults")}>
|
|
80
|
+
<UI.Switch
|
|
81
|
+
label={{ name: $T("constructor.props.map.track") }}
|
|
82
|
+
value={component.properties.trackEnabled ? 1 : 0}
|
|
83
|
+
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
84
|
+
onChange={(value) => updateProperty("trackEnabled", Boolean(value), component, onPropertyChange)}
|
|
85
|
+
/>
|
|
86
|
+
<UI.Select
|
|
87
|
+
label={{ name: $T("constructor.props.map.trackLength") }}
|
|
88
|
+
type="input"
|
|
89
|
+
options={TRACK_LENGTH_OPTIONS}
|
|
90
|
+
value={TRACK_LENGTH_OPTIONS.find((opt) => opt.value === (component.properties.trackLength ?? 1000))}
|
|
91
|
+
onUpdate={(option) => updateProperty("trackLength", (option as { value: number }).value, component, onPropertyChange)}
|
|
92
|
+
/>
|
|
93
|
+
</PropsGroup>
|
|
94
|
+
</div>
|
|
89
95
|
</div>
|
package/dist/WidgetPFD/README.md
CHANGED
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
|
|
115
115
|
## Режим имитации
|
|
116
116
|
|
|
117
|
-
`isTest` включает полёт без устройства: прибор получает кадр каждые 50 мс от той же модели полёта, что в прошивке 20.00.000-01. Это облёт маршрута над Минском. Пока режим включён, `value`, `id` и `limits.staleMs` не
|
|
117
|
+
`isTest` включает полёт без устройства: прибор получает кадр каждые 50 мс от той же модели полёта, что в прошивке 20.00.000-01. Это облёт маршрута над Минском. Пока режим включён, `value`, `id` и `limits.staleMs` не учитываются. В конструкторе все остальные свойства становятся бледными и недоступными, пока имитацию не выключат. Сохранённые заголовок, цвет и границы зон на имитации видны как есть. Модель полёта лежит в `src/lib/flightSim.ts`.
|
|
118
118
|
|
|
119
119
|
## Пропсы
|
|
120
120
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { updateProperty, type UIComponent, type IWidgetPFDProps, type IWidgetPFDLimits } from "../types"
|
|
7
7
|
import WidgetAccentPicker from "../WidgetAccentPicker.svelte"
|
|
8
8
|
import PropsGroup from "../PropsGroup.svelte"
|
|
9
|
+
import { twMerge } from "tailwind-merge"
|
|
9
10
|
|
|
10
11
|
const {
|
|
11
12
|
component,
|
|
@@ -27,35 +28,39 @@
|
|
|
27
28
|
</script>
|
|
28
29
|
|
|
29
30
|
<div class="flex flex-col gap-2">
|
|
30
|
-
<PropsGroup
|
|
31
|
-
<TextsEditor {component} {onPropertyChange} />
|
|
31
|
+
<PropsGroup>
|
|
32
32
|
<UI.Switch
|
|
33
33
|
label={{ name: $T("constructor.props.simulation") }}
|
|
34
34
|
value={component.properties.isTest ? 1 : 0}
|
|
35
35
|
options={[{ id: crypto.randomUUID(), value: 0, class: "" }]}
|
|
36
36
|
onChange={(value) => updateProperty("isTest", value === 1, component, onPropertyChange)}
|
|
37
37
|
/>
|
|
38
|
-
<UI.Input
|
|
39
|
-
label={{ name: $T("pfd.props.group") }}
|
|
40
|
-
disabled={component.properties.isTest}
|
|
41
|
-
value={component.properties.id ?? "MAV"}
|
|
42
|
-
onUpdate={(value) => updateProperty("id", value as string, component, onPropertyChange)}
|
|
43
|
-
/>
|
|
44
|
-
<WidgetAccentPicker
|
|
45
|
-
value={component.properties.componentClass ?? ""}
|
|
46
|
-
onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
|
|
47
|
-
/>
|
|
48
|
-
</PropsGroup>
|
|
49
|
-
<PropsGroup label={$T("pfd.limits")}>
|
|
50
|
-
<div class="grid grid-cols-2 gap-2">
|
|
51
|
-
<UI.Input label={{ name: $T("pfd.props.speed_min") }} type="number" value={limits.speedMin} onUpdate={(v) => setLimit("speedMin", v)} />
|
|
52
|
-
<UI.Input label={{ name: $T("pfd.props.speed_max") }} type="number" value={limits.speedMax} onUpdate={(v) => setLimit("speedMax", v)} />
|
|
53
|
-
<UI.Input label={{ name: $T("pfd.props.bank_max") }} type="number" value={limits.bankMax} onUpdate={(v) => setLimit("bankMax", v)} />
|
|
54
|
-
<UI.Input label={{ name: $T("pfd.props.alt_max") }} type="number" value={limits.altMax} onUpdate={(v) => setLimit("altMax", v)} />
|
|
55
|
-
<UI.Input label={{ name: $T("pfd.props.g_min") }} type="number" value={limits.gMin} onUpdate={(v) => setLimit("gMin", v)} />
|
|
56
|
-
<UI.Input label={{ name: $T("pfd.props.g_max") }} type="number" value={limits.gMax} onUpdate={(v) => setLimit("gMax", v)} />
|
|
57
|
-
<UI.Input label={{ name: $T("pfd.props.batt_low") }} type="number" value={limits.battLow} onUpdate={(v) => setLimit("battLow", v)} />
|
|
58
|
-
<UI.Input disabled={component.properties.isTest} label={{ name: $T("pfd.props.stale") }} type="number" value={limits.staleMs ?? 2000} onUpdate={(v) => setLimit("staleMs", v)} />
|
|
59
|
-
</div>
|
|
60
38
|
</PropsGroup>
|
|
39
|
+
<!-- В режиме имитации остальные свойства не учитываются, поэтому вся панель бледная и недоступна -->
|
|
40
|
+
<div class={twMerge("flex flex-col gap-2 transition-opacity", component.properties.isTest && "opacity-40 grayscale")} inert={component.properties.isTest}>
|
|
41
|
+
<PropsGroup label={$T("constructor.props.group.general")}>
|
|
42
|
+
<TextsEditor {component} {onPropertyChange} />
|
|
43
|
+
<UI.Input
|
|
44
|
+
label={{ name: $T("pfd.props.group") }}
|
|
45
|
+
value={component.properties.id ?? "MAV"}
|
|
46
|
+
onUpdate={(value) => updateProperty("id", value as string, component, onPropertyChange)}
|
|
47
|
+
/>
|
|
48
|
+
<WidgetAccentPicker
|
|
49
|
+
value={component.properties.componentClass ?? ""}
|
|
50
|
+
onUpdate={(value) => updateProperty("componentClass", value, component, onPropertyChange)}
|
|
51
|
+
/>
|
|
52
|
+
</PropsGroup>
|
|
53
|
+
<PropsGroup label={$T("pfd.limits")}>
|
|
54
|
+
<div class="grid grid-cols-2 gap-2">
|
|
55
|
+
<UI.Input label={{ name: $T("pfd.props.speed_min") }} type="number" value={limits.speedMin} onUpdate={(v) => setLimit("speedMin", v)} />
|
|
56
|
+
<UI.Input label={{ name: $T("pfd.props.speed_max") }} type="number" value={limits.speedMax} onUpdate={(v) => setLimit("speedMax", v)} />
|
|
57
|
+
<UI.Input label={{ name: $T("pfd.props.bank_max") }} type="number" value={limits.bankMax} onUpdate={(v) => setLimit("bankMax", v)} />
|
|
58
|
+
<UI.Input label={{ name: $T("pfd.props.alt_max") }} type="number" value={limits.altMax} onUpdate={(v) => setLimit("altMax", v)} />
|
|
59
|
+
<UI.Input label={{ name: $T("pfd.props.g_min") }} type="number" value={limits.gMin} onUpdate={(v) => setLimit("gMin", v)} />
|
|
60
|
+
<UI.Input label={{ name: $T("pfd.props.g_max") }} type="number" value={limits.gMax} onUpdate={(v) => setLimit("gMax", v)} />
|
|
61
|
+
<UI.Input label={{ name: $T("pfd.props.batt_low") }} type="number" value={limits.battLow} onUpdate={(v) => setLimit("battLow", v)} />
|
|
62
|
+
<UI.Input label={{ name: $T("pfd.props.stale") }} type="number" value={limits.staleMs ?? 2000} onUpdate={(v) => setLimit("staleMs", v)} />
|
|
63
|
+
</div>
|
|
64
|
+
</PropsGroup>
|
|
65
|
+
</div>
|
|
61
66
|
</div>
|
|
@@ -295,7 +295,6 @@ const translations = {
|
|
|
295
295
|
"constructor.props.divider.width": { ru: "Толщина полосы", en: "Line width", zh: "线条粗细" },
|
|
296
296
|
"constructor.props.disabled": { ru: "Отключить", en: "Disable", zh: "禁用" },
|
|
297
297
|
"constructor.props.readonly": { ru: "Только чтение", en: "Read only", zh: "只读" },
|
|
298
|
-
"constructor.props.istest": { ru: "Тестовые данные", en: "Test data", zh: "测试数据" },
|
|
299
298
|
"constructor.props.simulation": { ru: "Режим имитации", en: "Simulation mode", zh: "模拟模式" },
|
|
300
299
|
"constructor.props.refreshrate": { ru: "Скорость обновления", en: "Refresh rate", zh: "刷新速率" },
|
|
301
300
|
"constructor.props.variable": { ru: "Имя переменной", en: "Variable name", zh: "变量名" },
|
|
@@ -750,6 +749,7 @@ const translations = {
|
|
|
750
749
|
"widget.rgb.count_invalid": { ru: "Количество — целое от 1 до {max}", en: "Count must be an integer from 1 to {max}", zh: "数量必须是 1 到 {max} 的整数" },
|
|
751
750
|
"widget.rgb.group": { ru: "Группа ленты (ID)", en: "Strip group (ID)", zh: "灯带组 (ID)" },
|
|
752
751
|
"widget.map.title": { ru: "Карта", en: "Map", zh: "地图" },
|
|
752
|
+
"widget.map.follow": { ru: "Следить", en: "Follow", zh: "跟踪" },
|
|
753
753
|
"widget.map.groups": { ru: "Группы полей (через пробел)", en: "Field groups (space-separated)", zh: "字段组(以空格分隔)" },
|
|
754
754
|
"widget.map.height": { ru: "Высота карты", en: "Map height", zh: "地图高度" },
|
|
755
755
|
"widget.map.defaults": {
|