poe-svelte-ui-lib 1.91.0 → 1.93.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 +24 -11
- package/dist/Accordion/Accordion.svelte +6 -4
- package/dist/Button/Button.svelte +1 -1
- package/dist/CopiedChip.svelte +1 -1
- package/dist/Graph/Graph.svelte +1 -1
- package/dist/Input/Input.svelte +1 -1
- package/dist/Modal.README.md +12 -11
- package/dist/Modal.svelte +1 -1
- package/dist/PeriodChart/PeriodChart.svelte +2 -2
- package/dist/Table/Table.svelte +3 -3
- package/dist/WidgetAccentPicker.svelte +5 -2
- package/dist/WidgetFileSystem/WidgetFileSystem.svelte +4 -4
- package/dist/WidgetMap/README.md +13 -9
- package/dist/WidgetMap/WidgetMap.svelte +11 -11
- package/dist/WidgetMap/mission/HelpText.svelte +41 -0
- package/dist/WidgetMap/mission/HelpText.svelte.d.ts +6 -0
- package/dist/WidgetMap/mission/MapAgro.svelte +108 -64
- package/dist/WidgetMap/mission/MapRoute.svelte +70 -51
- package/dist/WidgetMap/mission/MissionLayers.svelte +60 -22
- package/dist/WidgetMap/mission/MissionLayers.svelte.d.ts +2 -0
- package/dist/WidgetMap/mission/MissionSim.svelte +19 -9
- package/dist/WidgetMap/mission/MissionSim.svelte.d.ts +2 -0
- package/dist/WidgetMap/mission/coverage.js +31 -6
- package/dist/WidgetMap/mission/plan.js +64 -18
- package/dist/WidgetPFD/WidgetPFD.svelte +27 -18
- package/dist/WidgetStackInfo/WidgetStackInfo.svelte +4 -4
- package/dist/WidgetWiFi/WidgetWiFi.svelte +1 -1
- package/dist/locales/translations.js +15 -10
- package/dist/options.js +2 -1
- package/dist/protocols/xPoeDebugView.js +6 -6
- package/dist/theme.css +102 -49
- package/dist/widgetAccent.d.ts +8 -0
- package/dist/widgetAccent.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,11 +70,11 @@ npm install poe-svelte-ui-lib
|
|
|
70
70
|
|
|
71
71
|
У каждого компонента есть парный `<Компонент>Props.svelte`. Это панель свойств для конструктора DevCloud. Пропсы у всех панелей одинаковые:
|
|
72
72
|
|
|
73
|
-
| Пропс
|
|
74
|
-
|
|
75
|
-
| `component`
|
|
76
|
-
| `onPropertyChange` | Сообщает об изменении свойств, имени, доступа или события
|
|
77
|
-
| `forConstructor`
|
|
73
|
+
| Пропс | Что это |
|
|
74
|
+
| ------------------ | ---------------------------------------------------------------------------- |
|
|
75
|
+
| `component` | Компонент интерфейса и его свойства |
|
|
76
|
+
| `onPropertyChange` | Сообщает об изменении свойств, имени, доступа или события |
|
|
77
|
+
| `forConstructor` | `true` - полная панель конструктора, `false` - упрощённая для редактирования |
|
|
78
78
|
|
|
79
79
|
## Протоколы
|
|
80
80
|
|
|
@@ -102,6 +102,19 @@ npm install poe-svelte-ui-lib
|
|
|
102
102
|
|
|
103
103
|
Поверхности идут в четыре ступени: фон страницы `--bg-body`, панель `--back-color`, блок внутри панели `--container-color`, поле ввода `--field-color`. Для частых случаев есть готовые классы. `ui-panel` для блока прямо на фоне страницы, `ui-inset` для блока внутри панели.
|
|
104
104
|
|
|
105
|
+
Текст идёт в три ступени: основной `--font-color`, вторичный `--muted-color`, подсказки `--faint-color`. Все три проходят WCAG AA (не ниже 4,5:1) на любом фоне темы. Текст не делают полупрозрачным (`opacity`), для вторичного текста берут `--muted-color`. Мельче 11px подписи не делают.
|
|
106
|
+
|
|
107
|
+
Цвета ролей взяты у Apple: красный, оранжевый, жёлтый, зелёный, голубой, синий, фиолетовый, розовый и серый. У каждого цвета четыре варианта:
|
|
108
|
+
|
|
109
|
+
- `--*-color` - заливка. Текст на ней белый, а на оранжевой и жёлтой тёмный (`--on-orange`, `--on-yellow`). Компоненты берут его из `--on-accent`, классы `bg-orange` и `bg-yellow` подставляют его сами;
|
|
110
|
+
- `--*-text` - цветной текст;
|
|
111
|
+
- `--*-soft` - мягкий фон блока или строки;
|
|
112
|
+
- `--*-mark` - чистый цвет Apple для полос, точек и индикаторов. Для текста он не годится, жёлтый и голубой на белом не читаются.
|
|
113
|
+
|
|
114
|
+
Акцентный цвет `--accent-color` - это синий Apple.
|
|
115
|
+
|
|
116
|
+
Цветной текст пишут через `--red-text`, `--green-text`, `--orange-text` и так далее (классы `text-(--red-text)`), а не через `--red-color`. Заливки `--*-color` как текст плохо читаются: в тёмной теме они тёмные, а жёлтая и оранжевая слишком светлые на белом.
|
|
117
|
+
|
|
105
118
|
Панели не делают полупрозрачными, на фоне они теряются. Выпадающие меню делают через `ui-panel` с тенью `--elevation-3`.
|
|
106
119
|
|
|
107
120
|
Стили тегов в приложении пишите только внутри `@layer base`. Правило вне слоя сильнее утилит Tailwind, и подписи в компонентах станут крупнее, чем задумано.
|
|
@@ -148,13 +161,13 @@ export const T = createTranslator(translations)
|
|
|
148
161
|
|
|
149
162
|
**Короткое**, по кнопке (i). Лежит в словаре под ключом `component.info.<Тип>`. Его показывают витрина и конструктор DevCloud. Разделы идут в таком порядке, ненужные пропускаются:
|
|
150
163
|
|
|
151
|
-
| Раздел
|
|
152
|
-
|
|
153
|
-
| Назначение
|
|
164
|
+
| Раздел | Что писать |
|
|
165
|
+
| ------------ | ------------------------------------------------------------------------------- |
|
|
166
|
+
| Назначение | Одна-две фразы: что это и когда применять |
|
|
154
167
|
| Пакет данных | Откуда данные, каким пакетом и как часто их слать, что компонент отправляет сам |
|
|
155
|
-
| Ключи
|
|
156
|
-
| Особенности
|
|
157
|
-
| Настройки
|
|
168
|
+
| Ключи | По пункту на ключ: тип, единицы, смысл |
|
|
169
|
+
| Особенности | Поведение, ограничения, советы |
|
|
170
|
+
| Настройки | Что меняется в панели свойств |
|
|
158
171
|
|
|
159
172
|
**Подробное**, `src/lib/<Компонент>/README.md`. Оно открывается на вкладке «Инфо» витрины. Начало повторяет короткое описание и не должно ему противоречить. Дальше идут пропсы, примеры, панель свойств и внутреннее устройство.
|
|
160
173
|
|
|
@@ -29,15 +29,17 @@
|
|
|
29
29
|
<div class="overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
|
|
30
30
|
<!-- Фон заголовка - мягкий оттенок мастер-цвета (светлый в светлой теме, тёмный в тёмной), текст - мастер-цвет с цветом шрифта 60/40, раскрытое содержимое остаётся на чистом фоне (--back-color). -->
|
|
31
31
|
<!-- Заголовок: кнопка сворачивания на всю ширину и необязательные кнопки справа (actions) - вне неё, чтобы не сворачивать блок -->
|
|
32
|
-
<div
|
|
32
|
+
<div
|
|
33
|
+
class="flex w-full items-center bg-(--accent-soft) transition-colors duration-150 hover:bg-[color-mix(in_srgb,var(--accent-color)_12%,var(--accent-soft))]"
|
|
34
|
+
>
|
|
33
35
|
<button
|
|
34
|
-
class="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left text-[color-mix(in_srgb,var(--accent-color)
|
|
36
|
+
class="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))]"
|
|
35
37
|
onclick={toggle}
|
|
36
38
|
>
|
|
37
39
|
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
38
40
|
{#if label?.icon}
|
|
39
41
|
<span
|
|
40
|
-
class="flex h-[18px] w-[18px] shrink-0 items-center justify-center text-[color-mix(in_srgb,var(--accent-color)
|
|
42
|
+
class="flex h-[18px] w-[18px] shrink-0 items-center justify-center text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))] [&_svg]:h-full [&_svg]:w-full"
|
|
41
43
|
>
|
|
42
44
|
{#if typeof label?.icon === "string"}
|
|
43
45
|
{@html label.icon}
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
|
|
53
55
|
<svg
|
|
54
56
|
xmlns="http://www.w3.org/2000/svg"
|
|
55
|
-
class="h-[14px] w-[14px] shrink-0 text-[color-mix(in_srgb,var(--accent-color)
|
|
57
|
+
class="h-[14px] w-[14px] shrink-0 text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))] transition-transform duration-200"
|
|
56
58
|
style="transform: rotate({isOpen ? 180 : 0}deg)"
|
|
57
59
|
viewBox="0 0 24 24"
|
|
58
60
|
>
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
{#if content.name?.trim()}
|
|
127
127
|
<span class="truncate">{content.name}</span>
|
|
128
128
|
{#if keyBind.key && keyBind.showHint}
|
|
129
|
-
<span class="ml-1 text-[11px] font-medium tabular-nums opacity-
|
|
129
|
+
<span class="ml-1 text-[11px] font-medium tabular-nums opacity-80">
|
|
130
130
|
{keyBind.ctrlKey ? "Ctrl+" : ""}{keyBind.shiftKey ? "Shift+" : ""}{keyBind.altKey ? "Alt+" : ""}{keyBind.key}
|
|
131
131
|
</span>
|
|
132
132
|
{/if}
|
package/dist/CopiedChip.svelte
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
{#if show}
|
|
10
10
|
<span
|
|
11
|
-
class="{className} absolute flex items-center gap-1 rounded-lg bg-(--green-soft) px-2 py-1 text-[11px] font-bold whitespace-nowrap text-(--green-
|
|
11
|
+
class="{className} absolute flex items-center gap-1 rounded-lg bg-(--green-soft) px-2 py-1 text-[11px] font-bold whitespace-nowrap text-(--green-text)"
|
|
12
12
|
transition:fade={{ duration: 150 }}
|
|
13
13
|
>
|
|
14
14
|
{$T("ui.copied")}
|
package/dist/Graph/Graph.svelte
CHANGED
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
const { padding, graphWidth, graphHeight, minX, timeSpan, yMin, yMax, getX, getY } = computeGeometry()
|
|
313
313
|
|
|
314
314
|
/* Сетка в стиле PeriodChart: пунктир цветом --faint-color темы, подписи тем же цветом без прозрачности */
|
|
315
|
-
const gridColor = getComputedStyle(canvas).getPropertyValue("--faint-color").trim() || "#
|
|
315
|
+
const gridColor = getComputedStyle(canvas).getPropertyValue("--faint-color").trim() || "#5f6774"
|
|
316
316
|
ctx.strokeStyle = gridColor
|
|
317
317
|
ctx.fillStyle = gridColor
|
|
318
318
|
ctx.lineWidth = 1
|
package/dist/Input/Input.svelte
CHANGED
|
@@ -245,6 +245,6 @@
|
|
|
245
245
|
|
|
246
246
|
{#if help.regExp && !isValid}
|
|
247
247
|
<!-- Текст ошибки в потоке под полем и только когда поле невалидно: место заранее не резервируется, на соседей не наезжает -->
|
|
248
|
-
<span class="block text-[12px] font-semibold text-(--red-
|
|
248
|
+
<span class="block text-[12px] font-semibold text-(--red-text)">{$T("ui.invalid_format")}</span>
|
|
249
249
|
{/if}
|
|
250
250
|
</div>
|
package/dist/Modal.README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
## Особенности
|
|
8
8
|
|
|
9
9
|
- Подложка затемняет страницу на 40% и размывает её.
|
|
10
|
+
- Окно не выше 90% экрана. Длинное содержимое прокручивается внутри, шапка с крестиком остаётся на месте.
|
|
10
11
|
- Escape и клик мимо окна закрывают только верхнее окно из открытых.
|
|
11
12
|
- Выпадающие списки `Select` внутри окна вынесены в `body`. Клик по ним не считается кликом мимо окна.
|
|
12
13
|
- Содержимое передаётся сниппетом `main`, обычные дочерние элементы не подойдут. Подвал `footer` необязателен.
|
|
@@ -14,16 +15,16 @@
|
|
|
14
15
|
|
|
15
16
|
## Пропсы
|
|
16
17
|
|
|
17
|
-
| Название
|
|
18
|
-
|
|
19
|
-
| `isOpen`
|
|
20
|
-
| `title`
|
|
21
|
-
| `width`
|
|
22
|
-
| `wrapperClass` | `string`
|
|
23
|
-
| `mainClass`
|
|
24
|
-
| `main`
|
|
25
|
-
| `footer`
|
|
26
|
-
| `onCancel`
|
|
18
|
+
| Название | Тип | По умолчанию | Описание |
|
|
19
|
+
| -------------- | ------------ | ------------ | ---------------------------------------------------------- |
|
|
20
|
+
| `isOpen` | `boolean` | `false` | Открыто ли окно. Можно связать через `bind:isOpen` |
|
|
21
|
+
| `title` | `string` | | Заголовок |
|
|
22
|
+
| `width` | `string` | `""` | Ширина, например `"600px"`. По умолчанию из `wrapperClass` |
|
|
23
|
+
| `wrapperClass` | `string` | `""` | Классы окна |
|
|
24
|
+
| `mainClass` | `string` | `""` | Классы области содержимого |
|
|
25
|
+
| `main` | `Snippet` | обязателен | Содержимое |
|
|
26
|
+
| `footer` | `Snippet` | | Подвал, обычно кнопки |
|
|
27
|
+
| `onCancel` | `() => void` | закрыть окно | Окно закрывают крестиком, кликом мимо или Escape |
|
|
27
28
|
|
|
28
29
|
## Пример
|
|
29
30
|
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
|
|
53
54
|
```ts
|
|
54
55
|
ModalStack.subscribe // список id в порядке открытия
|
|
55
|
-
ModalStack.open(id)
|
|
56
|
+
ModalStack.open(id) // добавить в конец
|
|
56
57
|
ModalStack.close(id) // убрать
|
|
57
58
|
```
|
|
58
59
|
|
package/dist/Modal.svelte
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
<div
|
|
83
83
|
data-modal
|
|
84
84
|
class={twMerge(
|
|
85
|
-
"flex w-300 flex-col overflow-hidden rounded-[20px] border border-(--hairline-color) bg-(--back-color) shadow-(--elevation-3)",
|
|
85
|
+
"flex max-h-[90vh] w-300 flex-col overflow-hidden rounded-[20px] border border-(--hairline-color) bg-(--back-color) shadow-(--elevation-3)",
|
|
86
86
|
wrapperClass,
|
|
87
87
|
)}
|
|
88
88
|
style="width: {width}; z-index: {zIndex};"
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<!-- Шкала Y: подписи нулевой высоты, центр каждой ровно на своей линии сетки -->
|
|
111
111
|
<div class="flex h-40 shrink-0 flex-col justify-between">
|
|
112
112
|
{#each axis.ticks as tick}
|
|
113
|
-
<div class="flex h-0 items-center justify-end text-[
|
|
113
|
+
<div class="flex h-0 items-center justify-end text-[11px] font-medium text-(--faint-color) tabular-nums">
|
|
114
114
|
{formatTick(tick)}
|
|
115
115
|
</div>
|
|
116
116
|
{/each}
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
|
|
153
153
|
<div class="flex w-full gap-1">
|
|
154
154
|
{#each values as _, i}
|
|
155
|
-
<div class="flex-1 truncate text-center text-[
|
|
155
|
+
<div class="flex-1 truncate text-center text-[11px] font-bold text-(--faint-color)">
|
|
156
156
|
{i % labelStep === 0 ? barLabel(i) : ""}
|
|
157
157
|
</div>
|
|
158
158
|
{/each}
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -263,7 +263,7 @@
|
|
|
263
263
|
<div class="relative flex h-full w-full flex-col overflow-hidden rounded-[14px] border border-(--hairline-color) bg-(--back-color)">
|
|
264
264
|
<!-- Table Header -->
|
|
265
265
|
<div
|
|
266
|
-
class="grid border-b border-(--hairline-color) bg-[var(--table-header-color,var(--accent-soft))] text-[11px] font-bold tracking-[0.06em] text-[color-mix(in_srgb,var(--accent-color)
|
|
266
|
+
class="grid border-b border-(--hairline-color) bg-[var(--table-header-color,var(--accent-soft))] text-[11px] font-bold tracking-[0.06em] text-[color-mix(in_srgb,var(--accent-color)_35%,var(--font-color))] uppercase"
|
|
267
267
|
style={`grid-template-columns: ${gridTemplate}; padding-right: ${scrollbarWidth}px;`}
|
|
268
268
|
>
|
|
269
269
|
{#each header as column, index (column)}
|
|
@@ -294,7 +294,7 @@
|
|
|
294
294
|
{#if dataBuffer.clearButton}
|
|
295
295
|
<div class="flex items-center justify-center tracking-normal normal-case">
|
|
296
296
|
<Button
|
|
297
|
-
componentClass={twMerge("size-7 p-1.5 text-(--muted-color) hover:text-(--red-
|
|
297
|
+
componentClass={twMerge("size-7 p-1.5 text-(--muted-color) hover:text-(--red-text)", dataBuffer.clearClass)}
|
|
298
298
|
content={{ icon: ButtonClear, info: { text: $T("common.table.clear"), side: "left" } }}
|
|
299
299
|
onClick={clearBuffer}
|
|
300
300
|
/>
|
|
@@ -380,7 +380,7 @@
|
|
|
380
380
|
{#if isDropdownOpen?.x === j && isDropdownOpen.y === i && isDropdownOpen.index === index}
|
|
381
381
|
{@const cords = document.getElementById(`select${i}-${j}-${index}`)?.getBoundingClientRect()}
|
|
382
382
|
<div
|
|
383
|
-
class="fixed z-50 overflow-
|
|
383
|
+
class="fixed z-50 max-h-[40vh] overflow-y-auto rounded-[10px] border border-(--hairline-color) bg-(--back-color) p-1 shadow-(--elevation-2)"
|
|
384
384
|
style="top: calc({cords?.bottom}px + 4px); left: {cords?.left}px; width: {cords?.width}px;"
|
|
385
385
|
transition:slide={{ duration: selectSlideDuration }}
|
|
386
386
|
>
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
onUpdate: (value: string) => void
|
|
15
15
|
wrapperClass?: string
|
|
16
16
|
} = $props()
|
|
17
|
+
|
|
18
|
+
/* bg-max не показываем: без выбора виджет и так синий, это цвет по умолчанию */
|
|
19
|
+
const options = $derived($optionsStore.COLOR_OPTIONS.filter((o) => o.value !== "bg-max"))
|
|
17
20
|
</script>
|
|
18
21
|
|
|
19
22
|
<Select
|
|
20
23
|
{wrapperClass}
|
|
21
24
|
label={{ name: $T("ui.accent") }}
|
|
22
25
|
type="buttons"
|
|
23
|
-
options
|
|
24
|
-
value={
|
|
26
|
+
{options}
|
|
27
|
+
value={options.find((o) => o.value === value) ?? options.find((o) => o.value === "bg-blue")}
|
|
25
28
|
onUpdate={(option) => onUpdate((option as IOption<string>).value ?? "")}
|
|
26
29
|
/>
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
let files = $derived(value?.List ?? [])
|
|
59
59
|
let usedPercent = $derived(total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0)
|
|
60
60
|
/* Полоса - мастер-цвет виджета; почти заполненную ФС (закачка скоро начнёт падать) выдаёт только процент */
|
|
61
|
-
let percentColor = $derived(usedPercent >= 90 ? "var(--red-
|
|
61
|
+
let percentColor = $derived(usedPercent >= 90 ? "var(--red-text)" : usedPercent >= 75 ? "var(--orange-text)" : "var(--muted-color)")
|
|
62
62
|
|
|
63
63
|
/* Таблица файлов: путь обрезается «…», размер - уже отформатирован; действия - значки без текста: скачать и удалить (с подтверждением) */
|
|
64
64
|
type FileRow = IWidgetFileSystemFile & { SizeText: string }
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
data: {
|
|
75
75
|
icon: ButtonDelete,
|
|
76
76
|
title: $T("widget.common.delete"),
|
|
77
|
-
class: "bg-transparent px-0 text-(--red-
|
|
77
|
+
class: "bg-transparent px-0 text-(--red-text) hover:bg-(--red-color)/10",
|
|
78
78
|
onClick: (row: FileRow) => (pendingDelete = row.Name),
|
|
79
79
|
},
|
|
80
80
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
>
|
|
186
186
|
{/if}
|
|
187
187
|
{#if downloadError}
|
|
188
|
-
<span class="mt-2 text-[12px] text-(--red-
|
|
188
|
+
<span class="mt-2 text-[12px] text-(--red-text)" transition:slide={{ duration: 100 }}>{downloadError}</span>
|
|
189
189
|
{/if}
|
|
190
190
|
</div>
|
|
191
191
|
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
</div>
|
|
208
208
|
{/if}
|
|
209
209
|
{#if uploadError}
|
|
210
|
-
<span class="text-[12px] text-(--red-
|
|
210
|
+
<span class="text-[12px] text-(--red-text)" transition:slide={{ duration: 100 }}>{uploadError}</span>
|
|
211
211
|
{/if}
|
|
212
212
|
</div>
|
|
213
213
|
{/if}
|
package/dist/WidgetMap/README.md
CHANGED
|
@@ -60,18 +60,19 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
60
60
|
- MapLibre загружается с CDN один раз на страницу. Без интернета видна заглушка «Карта не загружена».
|
|
61
61
|
- Источник подложки (CARTO и OpenStreetMap) указывать обязательно. Подпись свёрнута в кнопку «i» в углу карты.
|
|
62
62
|
- Сворачивание, мастер-цвет и кнопка «?» с подсказками работают как у `WidgetWiFi`.
|
|
63
|
+
- Мастер-цвет красит весь виджет: кнопки, линию маршрута, точки, точку взлёта, переходы и аппарат в имитации. Цвета со смыслом не меняются: поля зелёные, запретные зоны красные, развороты оранжевые, «Очистить», удаление и ошибка отправки красные.
|
|
63
64
|
|
|
64
65
|
## Режим имитации
|
|
65
66
|
|
|
66
67
|
`isTest` включает имитацию шести аппаратов `SIM-1`-`SIM-6` без устройства. Они летают над одним районом Минска, в пределах пары километров, и обновляются раз в полсекунды. На связи всегда три аппарата. Через 40-100 секунд один из них пропадает, и его сразу сменяет тот, кто молчит дольше всех. Поэтому видно все состояния маркера: свежий, красный устаревший, скрытый и снова появившийся. При включении карта переходит к этому району. Пока режим включён, `value` и `groups` не учитываются. В конструкторе все остальные свойства становятся бледными и недоступными, пока имитацию не выключат.
|
|
67
68
|
|
|
68
|
-
В режимах «Маршрут» и «Агро» `isTest` включает имитацию полёта по рассчитанной траектории. Аппарат летит по маршруту со скоростями его точек или по проходам «Агро» и оставляет
|
|
69
|
+
В режимах «Маршрут» и «Агро» `isTest` включает имитацию полёта по рассчитанной траектории. Аппарат летит по маршруту со скоростями его точек или по проходам «Агро» и оставляет след цвета виджета. В «Агро» маркер - квадрокоптер, он зелёный, пока идёт распыление. Панель внизу карты по центру показывает время полёта, процент пройденного пути, высоту, скорость и распыление; там же запуск и пауза, ускорение ×1, ×5, ×10 или ×25 (по умолчанию ×5) и полоса прокрутки пути: её перетаскивание ставит полёт на паузу, чтобы рассмотреть любое место траектории. Полёт запускается вручную кнопкой ▶ и останавливается в конце пути; повторный запуск начинает его заново. При любом изменении маршрута или полей аппарат возвращается в начало и встаёт на паузу. Аппараты из `groups` в это время не рисуются.
|
|
69
70
|
|
|
70
71
|
## Режим «Маршрут»
|
|
71
72
|
|
|
72
73
|
`mode="route"` - обычный планировщик миссии: точки по порядку облёта, у каждой своя высота и скорость. Аппараты из `groups` рисуются поверх маршрута, как в режиме «Объекты».
|
|
73
74
|
|
|
74
|
-
1.
|
|
75
|
+
1. **«Редактор точек»** на карте включает правку. Клик по карте добавляет точку в конец маршрута. Новая точка берёт высоту и скорость предыдущей, первая получает 30 м и 8 м/с. Esc или двойной клик заканчивают правку.
|
|
75
76
|
2. В режиме правки точки можно перетаскивать. Потяните за середину отрезка, чтобы вставить точку между соседними. Правый клик удаляет точку. Без правки на карте видны только номера точек, их не сдвинуть случайно. Точку взлёта `H` можно перетаскивать всегда.
|
|
76
77
|
3. Блок **«Итог»**: сверху расчёт (число точек, длина пути, время, наибольшая высота), ниже список точек с высотой, скоростью, расстоянием от предыдущей точки и удалением; порядок меняется перетаскиванием строки за ручку, как в списках панелей свойств, под ним флажок возврата с посадкой (RTL). Клик по номеру показывает точку на карте.
|
|
77
78
|
|
|
@@ -93,18 +94,21 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
93
94
|
|
|
94
95
|
`mode="agro"` превращает карту в генератор миссии для агродрона. Оператор обводит на карте одно или несколько полей и запретные зоны, задаёт угол проходов и ширину захвата. Карта строит траекторию, которая покрывает все поля, обходит зоны и выключает распылитель на разворотах и переходах. Готовую миссию можно скачать, скопировать или отправить на борт в формате QGC WPL 110 для автопилота ArduPilot.
|
|
95
96
|
|
|
96
|
-
Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются
|
|
97
|
+
Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются три демо-поля под Минском: большое с рощей, опорой ЛЭП и постройкой, среднее сложной формы с выступами, крюком и прудом, малое с вырезом и деревом. Между полями стоят зона-«галочка» и большая зона под средним полем, она задевает все три поля; перелёты их обходят. Ширина захвата по умолчанию 5 м, отступ от границ 2 м. Ширина захвата по умолчанию 5 м, отступ от границ 2 м.
|
|
97
98
|
|
|
98
99
|
### Как пользоваться
|
|
99
100
|
|
|
100
|
-
1.
|
|
101
|
-
2.
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
1. **«Редактор полей»** на карте: кликами по карте обведите участок. Контур замыкается кликом по первой точке, двойным кликом, Enter или кнопкой «Замкнуть». Backspace убирает последнюю точку, Esc сбрасывает начатый контур. Следующий контур станет ещё одним полем.
|
|
102
|
+
2. **«Редактор запретных зон»** на карте: обведите места, где нельзя лететь и распылять: деревья, опоры ЛЭП, постройки, водоёмы. Зон может быть сколько угодно. Зона может лежать внутри поля, выходить за его край или стоять вне полей. Из проходов вырезается только часть зоны внутри поля, и площадь поля уменьшается только на эту часть. Зона относится к каждому полю, которое задевает.
|
|
103
|
+
|
|
104
|
+
**Важно.** Перелёты от точки взлёта к полю, между полями и домой обходят все зоны с отступом, в том числе стоящие вне полей. Опоры ЛЭП, вышки, деревья и постройки на пути между полями обязательно обводите запретными зонами. Иначе аппарат полетит напрямую и может в них врезаться.
|
|
105
|
+
|
|
106
|
+
3. **Точка взлёта** (цвета виджета) видна всегда, её можно перетащить в любой момент.
|
|
107
|
+
4. В блоке **«Маршрут и внесение»** задайте ширину захвата, отступ от границ, высоту, скорость, норму внесения и объём бака. У каждого поля свой угол проходов: новое поле сразу получает лучший угол, «Авторасчёт» пересчитывает углы всех полей приближениями по полному расчёту траектории: шаг 5° по всем направлениям и вдоль сторон поля, затем 1° около трёх лучших, затем 0,1° около победителя; лучший угол даёт наименьшую цену: время облёта плюс около 4 с на разворот плюс штраф за недолив, где метр недолитого прохода стоит как три метра полёта. Пока идёт расчёт, на карте видно колесо ожидания «Расчёт оптимального маршрута» с полосой и процентом (на большом поле это несколько секунд), а ползунок «Угол для всех полей» ставит всем один угол, например чтобы проходы шли вдоль рядов посадки. Угол отдельного поля правится в его строке в блоке «Итог». Кнопка «i» справа от «Авторасчёта» открывает подробное описание алгоритма: проходы, ячейки, порядок облёта, штрафы и поиск угла приближениями. Текст лежит в словаре под ключом `widget.mission.autoInfo`.
|
|
104
108
|
5. Блок **«Итог»** показывает контуры, площадь к обработке, покрытие, длину пути, время, расход раствора и число дозаправок.
|
|
105
109
|
6. **«Скачать»**, **«Копировать»** или **«Отправить»** миссию.
|
|
106
110
|
|
|
107
|
-
Точки контуров видны и перетаскиваются, только пока
|
|
111
|
+
Точки контуров видны и перетаскиваются, только пока включён «Редактор полей» или «Редактор запретных зон». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
|
|
108
112
|
|
|
109
113
|
### Как строится траектория
|
|
110
114
|
|
|
@@ -170,7 +174,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
|
|
|
170
174
|
| `onSend` | `(mission, plan) => void \| Promise` | | Отправка миссии, `mission` - текст QGC WPL 110 |
|
|
171
175
|
| `label.name` | `string` | | Заголовок. Пусто - «Карта» на языке пользователя |
|
|
172
176
|
| `wrapperClass` | `string` | `""` | Классы обёртки |
|
|
173
|
-
| `componentClass` | `string` | `""` | Мастер-цвет `bg-*`
|
|
177
|
+
| `componentClass` | `string` | `""` | Мастер-цвет `bg-*` всего виджета |
|
|
174
178
|
| `collapsed` | `boolean` | `false` | Свёрнут ли |
|
|
175
179
|
| `persistKey` | `string` | | Ключ для запоминания свёрнутого состояния |
|
|
176
180
|
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
<div class="flex flex-col items-center justify-center leading-none">
|
|
329
329
|
<div
|
|
330
330
|
class="flex size-8 shrink-0 items-center justify-center [&_svg]:h-full [&_svg]:max-h-full [&_svg]:w-full [&_svg]:max-w-full
|
|
331
|
-
{isFresh(board) ? 'text-(--green-
|
|
331
|
+
{isFresh(board) ? 'text-(--green-text)' : 'text-(--red-text)'}"
|
|
332
332
|
style="rotate: {board.heading}deg;"
|
|
333
333
|
>
|
|
334
334
|
{#if markerIcon}
|
|
@@ -366,29 +366,29 @@
|
|
|
366
366
|
<div class="flex items-center gap-1.5">
|
|
367
367
|
<span class="size-2 shrink-0 rounded-full {isFresh(board) ? 'bg-(--green-color)' : 'bg-(--red-color)'}"></span>
|
|
368
368
|
<h6 class="text-sm leading-none font-bold">{board.name}</h6>
|
|
369
|
-
{#if board.name !== board.group}<span class="text-[
|
|
369
|
+
{#if board.name !== board.group}<span class="text-[11px] font-semibold uppercase text-(--muted-color)">{board.group}</span>{/if}
|
|
370
370
|
</div>
|
|
371
371
|
|
|
372
372
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1 rounded-lg bg-(--container-color) p-2 text-xs">
|
|
373
373
|
<div>
|
|
374
|
-
<p class="text-[
|
|
374
|
+
<p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Lat / Lon</p>
|
|
375
375
|
<p class="font-medium">{board.lat.toFixed(6)}, {board.lon.toFixed(6)}</p>
|
|
376
376
|
</div>
|
|
377
377
|
<div>
|
|
378
|
-
<p class="text-[
|
|
378
|
+
<p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Alt</p>
|
|
379
379
|
<p class="font-medium">{board.alt.toFixed(1)} m</p>
|
|
380
380
|
</div>
|
|
381
381
|
<div>
|
|
382
|
-
<p class="text-[
|
|
382
|
+
<p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Heading</p>
|
|
383
383
|
<p class="font-medium">{board.heading.toFixed(1)}°</p>
|
|
384
384
|
</div>
|
|
385
385
|
<div>
|
|
386
|
-
<p class="text-[
|
|
386
|
+
<p class="text-[11px] font-semibold tracking-wide uppercase text-(--muted-color)">Satellites</p>
|
|
387
387
|
<p class="font-medium">{board.sat}</p>
|
|
388
388
|
</div>
|
|
389
389
|
</div>
|
|
390
390
|
|
|
391
|
-
{#if board.sn}<p class="text-center font-mono text-[
|
|
391
|
+
{#if board.sn}<p class="text-center font-mono text-[11px] tracking-wide uppercase">{board.sn}</p>{/if}
|
|
392
392
|
|
|
393
393
|
<div class="grid grid-cols-[1fr_auto_1fr] items-center border-t border-(--hairline-color) pt-1">
|
|
394
394
|
<div class="relative flex items-center justify-self-start">
|
|
@@ -415,7 +415,7 @@
|
|
|
415
415
|
|
|
416
416
|
<button
|
|
417
417
|
class="flex cursor-pointer items-center gap-1 rounded-lg border px-2 py-0.5 text-xs font-semibold transition {isFollowed
|
|
418
|
-
? 'border-(--accent-color) bg-(--accent-color) text-
|
|
418
|
+
? 'border-(--accent-color) bg-(--accent-color) text-(--on-accent)'
|
|
419
419
|
: 'border-(--hairline-color) bg-transparent opacity-80 hover:opacity-100'}"
|
|
420
420
|
aria-pressed={isFollowed}
|
|
421
421
|
onclick={() => toggleFollow(board.group)}
|
|
@@ -431,7 +431,7 @@
|
|
|
431
431
|
</button>
|
|
432
432
|
|
|
433
433
|
<button
|
|
434
|
-
class="cursor-pointer justify-self-end rounded-lg border-none bg-transparent p-0.5 text-(--red-
|
|
434
|
+
class="cursor-pointer justify-self-end rounded-lg border-none bg-transparent p-0.5 text-(--red-text) transition hover:opacity-80"
|
|
435
435
|
aria-label="Hide"
|
|
436
436
|
title="Hide"
|
|
437
437
|
onclick={() => hideBoard(board.group)}
|
|
@@ -503,10 +503,10 @@
|
|
|
503
503
|
/>
|
|
504
504
|
<span
|
|
505
505
|
class="text-[12px] font-semibold tabular-nums {freshCount === 0
|
|
506
|
-
? 'text-(--red-
|
|
506
|
+
? 'text-(--red-text)'
|
|
507
507
|
: freshCount < groupList.length
|
|
508
508
|
? 'text-(--muted-color)'
|
|
509
|
-
: 'text-(--green-
|
|
509
|
+
: 'text-(--green-text)'}"
|
|
510
510
|
use:hint={{ text: $T("widget.map.hint.boards"), enabled: $hints }}
|
|
511
511
|
>
|
|
512
512
|
{freshCount} / {groupList.length}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- $lib/WidgetMap/mission/HelpText.svelte - справка из простого текста: строки "## " - заголовки, "- " - пункты списка, остальное - абзацы, **жирный** внутри строки -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
let { text }: { text: string } = $props()
|
|
4
|
+
|
|
5
|
+
type Block = { kind: "h"; text: string } | { kind: "p"; text: string } | { kind: "ul"; items: string[] }
|
|
6
|
+
|
|
7
|
+
let blocks = $derived.by(() => {
|
|
8
|
+
const out: Block[] = []
|
|
9
|
+
for (const raw of text.split("\n")) {
|
|
10
|
+
const line = raw.trim()
|
|
11
|
+
if (!line) continue
|
|
12
|
+
if (line.startsWith("## ")) out.push({ kind: "h", text: line.slice(3) })
|
|
13
|
+
else if (line.startsWith("- ")) {
|
|
14
|
+
const last = out[out.length - 1]
|
|
15
|
+
if (last?.kind === "ul") last.items.push(line.slice(2))
|
|
16
|
+
else out.push({ kind: "ul", items: [line.slice(2)] })
|
|
17
|
+
} else out.push({ kind: "p", text: line })
|
|
18
|
+
}
|
|
19
|
+
return out
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#snippet inline(line: string)}
|
|
24
|
+
{#each line.split("**") as part, i}{#if i % 2}<strong class="font-semibold">{part}</strong>{:else}{part}{/if}{/each}
|
|
25
|
+
{/snippet}
|
|
26
|
+
|
|
27
|
+
<div class="flex flex-col gap-1.5 text-[14px] leading-snug text-(--font-color)">
|
|
28
|
+
{#each blocks as block}
|
|
29
|
+
{#if block.kind === "h"}
|
|
30
|
+
<h3 class="m-0 mt-2.5 text-[15px] font-semibold">{@render inline(block.text)}</h3>
|
|
31
|
+
{:else if block.kind === "ul"}
|
|
32
|
+
<ul class="m-0 flex list-none flex-col gap-0.5 p-0">
|
|
33
|
+
{#each block.items as item}
|
|
34
|
+
<li class="relative m-0 pl-4 before:absolute before:left-0 before:text-(--accent-color) before:content-['•']">{@render inline(item)}</li>
|
|
35
|
+
{/each}
|
|
36
|
+
</ul>
|
|
37
|
+
{:else}
|
|
38
|
+
<p class="m-0">{@render inline(block.text)}</p>
|
|
39
|
+
{/if}
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|