poe-svelte-ui-lib 1.93.0 → 1.95.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.
@@ -26,7 +26,7 @@
26
26
  }: IWidgetDeviceInfoProps = $props()
27
27
 
28
28
  /* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetDeviceInfo */
29
- const hints = hintsSetting("WidgetDeviceInfo")
29
+ const hints = $derived(hintsSetting("WidgetDeviceInfo", persistKey))
30
30
 
31
31
  let confirmReset = $state(false)
32
32
  const factoryReset = () => {
@@ -30,7 +30,7 @@
30
30
  }: IWidgetFileSystemProps = $props()
31
31
 
32
32
  /* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetFileSystem */
33
- const hints = hintsSetting("WidgetFileSystem")
33
+ const hints = $derived(hintsSetting("WidgetFileSystem", persistKey))
34
34
 
35
35
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
36
36
 
@@ -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
  - **Слежение.** Кнопка «Следить» в карточке залипает. Один отслеживаемый объект всегда в центре карты. Несколько вписываются в карту при включении слежения, дальше карта держит в центре их середину. Масштаб остаётся за оператором: карта сама отдаляется, только когда объект уходит за край. Под маркером отслеживаемого объекта точка, она видна и при закрытой карточке. Если объект пропал с карты, слежение за ним продолжится, когда он появится снова. «Скрыть» снимает слежение.
57
58
  - **Шапка** показывает, сколько объектов на связи. Зелёный значит все, серый часть, красный ни одного.
@@ -94,7 +95,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
94
95
 
95
96
  `mode="agro"` превращает карту в генератор миссии для агродрона. Оператор обводит на карте одно или несколько полей и запретные зоны, задаёт угол проходов и ширину захвата. Карта строит траекторию, которая покрывает все поля, обходит зоны и выключает распылитель на разворотах и переходах. Готовую миссию можно скачать, скопировать или отправить на борт в формате QGC WPL 110 для автопилота ArduPilot.
96
97
 
97
- Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются три демо-поля под Минском: большое с рощей, опорой ЛЭП и постройкой, среднее сложной формы с выступами, крюком и прудом, малое с вырезом и деревом. Между полями стоят зона-«галочка» и большая зона под средним полем, она задевает все три поля; перелёты их обходят. Ширина захвата по умолчанию 5 м, отступ от границ 2 м. Ширина захвата по умолчанию 5 м, отступ от границ 2 м.
98
+ Аппараты из `groups` рисуются поверх миссии так же, как в режиме «Объекты»: маркер, карточка по клику и траектория, если она включена. План хранит владелец карты через `plan` и `onPlanChange`, миссия уходит наружу через `onSend`. Без `plan` показываются три демо-поля под Минском: большое сложной формы с прорезью и выступами, рощей, опорой ЛЭП и постройкой, среднее с выступами, крюком и прудом, малое с вырезом и деревом. Между полями стоит большая треугольная зона, внизу зона, которая задевает все три поля; перелёты их обходят. Ширина захвата по умолчанию 5 м, отступ от границ 2 м. Ширина захвата по умолчанию 5 м, отступ от границ 2 м.
98
99
 
99
100
  ### Как пользоваться
100
101
 
@@ -108,7 +109,7 @@ Telemetry_SendObject(g_telemetry_stream, g_mcfg.DevSN, "Data", dev, sizeof(dev)
108
109
  5. Блок **«Итог»** показывает контуры, площадь к обработке, покрытие, длину пути, время, расход раствора и число дозаправок.
109
110
  6. **«Скачать»**, **«Копировать»** или **«Отправить»** миссию.
110
111
 
111
- Точки контуров видны и перетаскиваются, только пока включён «Редактор полей» или «Редактор запретных зон». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
112
+ Точки контуров видны и перетаскиваются, только пока включён «Редактор полей» или «Редактор запретных зон». Потяните за середину стороны, чтобы добавить точку. Правый клик удаляет точку. Поля и зоны удаляются крестиком в блоке «Итог». Там же видно, где какое поле или зона: при наведении на строку контур подсвечивается на карте, нажатие на название приближает к нему карту. Подписей на самой карте нет, маленькой зоне их некуда поставить. Режим выключается той же кнопкой, кнопкой «Готово» или Esc.
112
113
 
113
114
  ### Как строится траектория
114
115
 
@@ -44,7 +44,7 @@
44
44
  }: IWidgetMapProps = $props()
45
45
 
46
46
  /* Подсказки - кнопка «?» в шапке, выбор общий для всех WidgetMap */
47
- const hints = hintsSetting("WidgetMap")
47
+ const hints = $derived(hintsSetting("WidgetMap", persistKey))
48
48
 
49
49
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
50
50
 
@@ -11,6 +11,8 @@
11
11
  import * as UI from "../.."
12
12
  import { MapLibre, NavigationControl, ScaleControl, FullScreenControl, CustomControl } from "../mapWrapper"
13
13
  import MissionLayers from "./MissionLayers.svelte"
14
+ import FitIcon from "../FitIcon.svelte"
15
+ import { createAutoFit, fitMapTo } from "./fit"
14
16
  import MissionSim from "./MissionSim.svelte"
15
17
  import SpinnerIcon from "./SpinnerIcon.svelte"
16
18
  import HelpText from "./HelpText.svelte"
@@ -52,7 +54,7 @@
52
54
  onSend,
53
55
  }: Props = $props()
54
56
 
55
- const hints = hintsSetting("WidgetMap")
57
+ const hints = $derived(hintsSetting("WidgetMap", persistKey))
56
58
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
57
59
 
58
60
  let hasMergedPersisted = false
@@ -285,6 +287,30 @@
285
287
  geometry: { type: "LineString", coordinates: s.points },
286
288
  })),
287
289
  })
290
+ /* Поле или зона, на которые навели в списке «Итога», подсвечиваются на карте; нажатие на название приближает карту к контуру */
291
+ let highlighted = $state<{ kind: "field" | "hole"; index: number } | null>(null)
292
+ let highlightGeo = $derived.by(() => {
293
+ if (!highlighted) return null
294
+ const ring = highlighted.kind === "field" ? fields[highlighted.index] : plan.holes[highlighted.index]
295
+ if (!ring || ring.length < 3) return null
296
+ return {
297
+ type: "FeatureCollection",
298
+ features: [{ type: "Feature", properties: { kind: highlighted.kind }, geometry: { type: "Polygon", coordinates: [closed(ring)] } }],
299
+ }
300
+ })
301
+ const flyTo = (ring: LngLat[]) => {
302
+ if (!mapInstance || ring.length < 3) return
303
+ const lngs = ring.map((p) => p[0])
304
+ const lats = ring.map((p) => p[1])
305
+ mapInstance.fitBounds(
306
+ [
307
+ [Math.min(...lngs), Math.min(...lats)],
308
+ [Math.max(...lngs), Math.max(...lats)],
309
+ ],
310
+ { padding: 60, maxZoom: 19, duration: 500 },
311
+ )
312
+ }
313
+
288
314
  let draftGeo = $derived({
289
315
  type: "FeatureCollection",
290
316
  features: drawing
@@ -406,6 +432,14 @@
406
432
  void isDarkMode
407
433
  mapColors = readMapColors(rootEl)
408
434
  })
435
+ /* «Показать все» в шапке и вписывание при появлении карты */
436
+ const missionPoints = (): LngLat[] => [...fields.flat(), ...plan.holes.flat(), ...(plan.start ? [plan.start] : [])]
437
+ const fitAll = (animate = true) => fitMapTo(mapInstance, missionPoints(), animate)
438
+ const autoFit = createAutoFit()
439
+ $effect(() => {
440
+ const map = mapInstance
441
+ untrack(() => autoFit(map, () => fitAll(false)))
442
+ })
409
443
 
410
444
  /* Мелкие зоны в м², остальное в га */
411
445
  const areaText = (m2: number) => (m2 < 1000 ? `${fmt(m2, 0)} ${$T("widget.mission.m2")}` : `${fmt(m2 / 10000, 2)} ${$T("widget.mission.ha")}`)
@@ -470,22 +504,31 @@
470
504
  >
471
505
  <WidgetHeader icon={MissionIcon} label={label?.name || $T("widget.mission.title")} bind:collapsed {hints}>
472
506
  {#snippet right()}
473
- <span class="flex items-center gap-3 text-[12px] font-semibold text-(--muted-color) tabular-nums">
474
- {#if headerProgress !== null}
475
- <span class="flex items-center gap-1.5" title={simulate ? $T("widget.sim.title") : $T("widget.mission.progressHint")}>
476
- <span class="h-1.5 w-14 overflow-hidden rounded-full bg-(--hairline-color)"
477
- ><span class="block h-full rounded-full bg-(--accent-color)" style="width: {Math.round(headerProgress * 100)}%"></span></span
507
+ <div class="flex items-center gap-3">
508
+ <UI.Button
509
+ wrapperClass="w-auto"
510
+ componentClass="bg-transparent px-3"
511
+ content={{ icon: FitIcon, name: $T("constructor.props.map.fitAll"), info: { text: $T("widget.mission.fitHint"), side: "bottom" } }}
512
+ disabled={collapsed || missionPoints().length === 0}
513
+ onClick={() => fitAll()}
514
+ />
515
+ <span class="flex items-center gap-3 text-[12px] font-semibold text-(--muted-color) tabular-nums">
516
+ {#if headerProgress !== null}
517
+ <span class="flex items-center gap-1.5" title={simulate ? $T("widget.sim.title") : $T("widget.mission.progressHint")}>
518
+ <span class="h-1.5 w-14 overflow-hidden rounded-full bg-(--hairline-color)"
519
+ ><span class="block h-full rounded-full bg-(--accent-color)" style="width: {Math.round(headerProgress * 100)}%"></span></span
520
+ >
521
+ {Math.round(headerProgress * 100)}%{#if live}
522
+ · {$T("widget.mission.treated")} {fmt(treatedHa, 2)} {$T("widget.mission.ha")}{/if}
523
+ </span>
524
+ {/if}
525
+ {#if stats}
526
+ <span class="text-[12px] font-semibold text-(--muted-color) tabular-nums"
527
+ >{fmt(stats.ha, 2)} {$T("widget.mission.ha")} · {fmt(stats.total, 2)} {$T("widget.mission.km")}</span
478
528
  >
479
- {Math.round(headerProgress * 100)}%{#if live}
480
- · {$T("widget.mission.treated")} {fmt(treatedHa, 2)} {$T("widget.mission.ha")}{/if}
481
- </span>
482
- {/if}
483
- {#if stats}
484
- <span class="text-[12px] font-semibold text-(--muted-color) tabular-nums"
485
- >{fmt(stats.ha, 2)} {$T("widget.mission.ha")} · {fmt(stats.total, 2)} {$T("widget.mission.km")}</span
486
- >
487
- {/if}
488
- </span>
529
+ {/if}
530
+ </span>
531
+ </div>
489
532
  {/snippet}
490
533
  </WidgetHeader>
491
534
 
@@ -542,6 +585,7 @@
542
585
  {onDragEnd}
543
586
  {onHandleRightClick}
544
587
  colors={mapColors}
588
+ highlight={highlightGeo}
545
589
  onCursor={(ll) => (cursor = ll)}
546
590
  />
547
591
  {@render overlay?.()}
@@ -682,9 +726,16 @@
682
726
  <span class="text-[11px] font-semibold tracking-wide text-(--faint-color) uppercase">{$T("widget.mission.zones")}</span>
683
727
  <div class="flex flex-col gap-1.5 rounded-lg bg-(--container-color) p-3 text-[13px] tabular-nums">
684
728
  {#each fields as field, i}
685
- <div class="flex items-center justify-between gap-2">
686
- <span class="flex items-center gap-2"
687
- ><span class="size-2.5 rounded-sm bg-(--green-mark)"></span>{$T("widget.mission.field")} {i + 1}</span
729
+ <div
730
+ class="-mx-1.5 flex items-center justify-between gap-2 rounded-md px-1.5 transition-colors duration-150 hover:bg-(--green-soft)"
731
+ role="listitem"
732
+ onmouseenter={() => (highlighted = { kind: "field", index: i })}
733
+ onmouseleave={() => (highlighted = null)}
734
+ >
735
+ <button
736
+ class="flex cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left text-(--font-color)"
737
+ title={$T("widget.mission.showOnMap")}
738
+ onclick={() => flyTo(field)}><span class="size-2.5 rounded-sm bg-(--green-mark)"></span>{$T("widget.mission.field")} {i + 1}</button
688
739
  >
689
740
  <span class="flex items-center gap-3">
690
741
  <span class="flex items-center gap-1" use:hint={{ text: $T("widget.mission.hint.fieldAngle"), enabled: $hints }}>
@@ -706,8 +757,17 @@
706
757
  <span class="text-(--muted-color)">{$T("widget.mission.noField")}</span>
707
758
  {/each}
708
759
  {#each plan.holes as hole, i}
709
- <div class="flex items-center justify-between gap-2">
710
- <span class="flex items-center gap-2"><span class="size-2.5 rounded-sm bg-(--red-mark)"></span>{$T("widget.mission.hole")} {i + 1}</span>
760
+ <div
761
+ class="-mx-1.5 flex items-center justify-between gap-2 rounded-md px-1.5 transition-colors duration-150 hover:bg-(--red-soft)"
762
+ role="listitem"
763
+ onmouseenter={() => (highlighted = { kind: "hole", index: i })}
764
+ onmouseleave={() => (highlighted = null)}
765
+ >
766
+ <button
767
+ class="flex cursor-pointer items-center gap-2 border-none bg-transparent p-0 text-left text-(--font-color)"
768
+ title={$T("widget.mission.showOnMap")}
769
+ onclick={() => flyTo(hole)}><span class="size-2.5 rounded-sm bg-(--red-mark)"></span>{$T("widget.mission.hole")} {i + 1}</button
770
+ >
711
771
  <span class="flex items-center gap-2">
712
772
  <span class="font-semibold">{areaText(fieldArea(hole, []))}</span>
713
773
  <button class="cursor-pointer border-none bg-transparent p-0 text-(--red-text)" onclick={() => removeHole(i)} aria-label="Delete"
@@ -11,6 +11,8 @@
11
11
  import * as UI from "../.."
12
12
  import { MapLibre, NavigationControl, ScaleControl, FullScreenControl, CustomControl } from "../mapWrapper"
13
13
  import MissionLayers from "./MissionLayers.svelte"
14
+ import FitIcon from "../FitIcon.svelte"
15
+ import { createAutoFit, fitMapTo } from "./fit"
14
16
  import MissionSim from "./MissionSim.svelte"
15
17
  import { missionProgress } from "./progress"
16
18
  import MapIcon from "../MapIcon.svelte"
@@ -49,7 +51,7 @@
49
51
  onSend,
50
52
  }: Props = $props()
51
53
 
52
- const hints = hintsSetting("WidgetMap")
54
+ const hints = $derived(hintsSetting("WidgetMap", persistKey))
53
55
  let accentStyle = $derived(widgetAccentStyle(componentClass ?? ""))
54
56
 
55
57
  let hasMergedPersisted = false
@@ -234,6 +236,14 @@
234
236
  void isDarkMode
235
237
  mapColors = readMapColors(rootEl)
236
238
  })
239
+ /* «Показать все» в шапке и вписывание при появлении карты */
240
+ const missionPoints = (): LngLat[] => [...(route.start ? [route.start] : []), ...route.points.map((p) => p.point)]
241
+ const fitAll = (animate = true) => fitMapTo(mapInstance, missionPoints(), animate)
242
+ const autoFit = createAutoFit()
243
+ $effect(() => {
244
+ const map = mapInstance
245
+ untrack(() => autoFit(map, () => fitAll(false)))
246
+ })
237
247
 
238
248
  const fmt = (v: number, d = 1) => v.toLocaleString(undefined, { minimumFractionDigits: d, maximumFractionDigits: d })
239
249
  /* Кнопки панели на карте: стили кнопок MapLibre (размер 29px, прозрачный фон, без рамки) перебиваются */
@@ -281,22 +291,31 @@
281
291
  >
282
292
  <WidgetHeader icon={MapIcon} label={label?.name || $T("widget.route.title")} bind:collapsed {hints}>
283
293
  {#snippet right()}
284
- <span class="flex items-center gap-3 text-[12px] font-semibold text-(--muted-color) tabular-nums">
285
- {#if headerProgress !== null}
286
- <span class="flex items-center gap-1.5" title={simulate ? $T("widget.sim.title") : $T("widget.mission.progressHint")}>
287
- <span class="h-1.5 w-14 overflow-hidden rounded-full bg-(--hairline-color)"
288
- ><span class="block h-full rounded-full bg-(--accent-color)" style="width: {Math.round(headerProgress * 100)}%"></span></span
289
- >
290
- {Math.round(headerProgress * 100)}%{#if live}
291
- · {live.finished
292
- ? $T("widget.route.finished")
293
- : live.target > route.points.length
294
- ? $T("widget.route.returning")
295
- : `${$T("widget.route.toPoint")} ${live.target}/${route.points.length}`}{/if}
296
- </span>
297
- {/if}
298
- <span>{stats.points} {$T("widget.route.pts")} · {km(stats.length)}</span>
299
- </span>
294
+ <div class="flex items-center gap-3">
295
+ <UI.Button
296
+ wrapperClass="w-auto"
297
+ componentClass="bg-transparent px-3"
298
+ content={{ icon: FitIcon, name: $T("constructor.props.map.fitAll"), info: { text: $T("widget.route.fitHint"), side: "bottom" } }}
299
+ disabled={collapsed || missionPoints().length === 0}
300
+ onClick={() => fitAll()}
301
+ />
302
+ <span class="flex items-center gap-3 text-[12px] font-semibold text-(--muted-color) tabular-nums">
303
+ {#if headerProgress !== null}
304
+ <span class="flex items-center gap-1.5" title={simulate ? $T("widget.sim.title") : $T("widget.mission.progressHint")}>
305
+ <span class="h-1.5 w-14 overflow-hidden rounded-full bg-(--hairline-color)"
306
+ ><span class="block h-full rounded-full bg-(--accent-color)" style="width: {Math.round(headerProgress * 100)}%"></span></span
307
+ >
308
+ {Math.round(headerProgress * 100)}%{#if live}
309
+ · {live.finished
310
+ ? $T("widget.route.finished")
311
+ : live.target > route.points.length
312
+ ? $T("widget.route.returning")
313
+ : `${$T("widget.route.toPoint")} ${live.target}/${route.points.length}`}{/if}
314
+ </span>
315
+ {/if}
316
+ <span>{stats.points} {$T("widget.route.pts")} · {km(stats.length)}</span>
317
+ </span>
318
+ </div>
300
319
  {/snippet}
301
320
  </WidgetHeader>
302
321
 
@@ -1,4 +1,4 @@
1
- <!-- $lib/WidgetMap/mission/MissionLayers.svelte - слои карты генератора миссии: поле и зоны, траектория, черновик контура и ручки вершин; клики и перетаскивание ручек отдаёт наверх -->
1
+ <!-- $lib/WidgetMap/mission/MissionLayers.svelte - слои карты генератора миссии: поле и зоны, траектория, подсветка выбранного в списке контура, черновик контура и ручки вершин; клики и перетаскивание ручек отдаёт наверх -->
2
2
  <script lang="ts">
3
3
  import { onDestroy, untrack } from "svelte"
4
4
  import { getMapContext } from "../mapWrapper"
@@ -18,9 +18,10 @@
18
18
  onHandleRightClick: (id: string) => void
19
19
  onCursor: (ll: LngLat | null) => void
20
20
  colors: IMapColors /* Маршрут и точки - мастер-цвет, поле, зоны и развороты - цвета по смыслу */
21
+ highlight?: any /* Контур, на который навели в списке «Итога»: поле или зона с kind */
21
22
  }
22
23
 
23
- let { zones, path, draft, handles, drawing, onMapClick, onMapDblClick, onDrag, onDragEnd, onHandleRightClick, onCursor, colors }: Props = $props()
24
+ let { zones, path, draft, handles, drawing, onMapClick, onMapDblClick, onDrag, onDragEnd, onHandleRightClick, onCursor, colors, highlight }: Props = $props()
24
25
 
25
26
  /* Краски слоёв от цветов виджета: одна таблица и для создания слоёв, и для перекраски */
26
27
  const paints = (c: IMapColors): Record<string, Record<string, any>> => {
@@ -33,6 +34,8 @@
33
34
  "msn-path-spray": { "line-color": c.green },
34
35
  "msn-path-route": { "line-color": c.accent },
35
36
  "msn-draft-line": { "line-color": byKind },
37
+ "msn-highlight-fill": { "fill-color": byKind },
38
+ "msn-highlight-line": { "line-color": byKind },
36
39
  "msn-handles": {
37
40
  "circle-color": ["match", ["get", "role"], "mid", "#ffffff", "start", c.accent, "wp", c.accent, byKind],
38
41
  "circle-stroke-color": ["match", ["get", "role"], "mid", ["match", ["get", "kind"], "hole", c.red, "route", c.accent, c.green], "#ffffff"],
@@ -45,7 +48,7 @@
45
48
  const mapCtx = getMapContext()
46
49
  const map = mapCtx.map!
47
50
 
48
- const SOURCES = ["msn-zones", "msn-path", "msn-draft", "msn-handles"]
51
+ const SOURCES = ["msn-zones", "msn-path", "msn-highlight", "msn-draft", "msn-handles"]
49
52
  const empty = { type: "FeatureCollection", features: [] }
50
53
 
51
54
  for (const id of SOURCES) mapCtx.addSource(id, { type: "geojson", data: empty as any })
@@ -97,6 +100,20 @@
97
100
  layout: { "line-join": "round", "line-cap": "round" },
98
101
  paint: { ...initial["msn-path-route"], "line-width": 3 },
99
102
  })
103
+ /* Подсветка контура поверх проходов: плотная заливка и толстая обводка, видна и у маленькой зоны */
104
+ mapCtx.addLayer({
105
+ id: "msn-highlight-fill",
106
+ type: "fill",
107
+ source: "msn-highlight",
108
+ paint: { ...initial["msn-highlight-fill"], "fill-opacity": 0.45 },
109
+ })
110
+ mapCtx.addLayer({
111
+ id: "msn-highlight-line",
112
+ type: "line",
113
+ source: "msn-highlight",
114
+ layout: { "line-join": "round" },
115
+ paint: { ...initial["msn-highlight-line"], "line-width": 5 },
116
+ })
100
117
  mapCtx.addLayer({
101
118
  id: "msn-draft-line",
102
119
  type: "line",
@@ -141,6 +158,7 @@
141
158
  $effect(() => setData("msn-path", $state.snapshot(path)))
142
159
  $effect(() => setData("msn-draft", $state.snapshot(draft)))
143
160
  $effect(() => setData("msn-handles", $state.snapshot(handles)))
161
+ $effect(() => setData("msn-highlight", $state.snapshot(highlight) ?? empty))
144
162
 
145
163
  /* Рисование: двойной клик закрывает контур, а не приближает карту */
146
164
  $effect(() => {
@@ -16,6 +16,7 @@ interface Props {
16
16
  onHandleRightClick: (id: string) => void;
17
17
  onCursor: (ll: LngLat | null) => void;
18
18
  colors: IMapColors;
19
+ highlight?: any;
19
20
  }
20
21
  declare const MissionLayers: import("svelte").Component<Props, {}, "">;
21
22
  type MissionLayers = ReturnType<typeof MissionLayers>;
@@ -1,10 +1,9 @@
1
- <!-- $lib/WidgetMap/mission/SpinnerIcon.svelte - колесо ожидания размером с родителя: подбор угла проходов -->
1
+ <!-- $lib/WidgetMap/mission/SpinnerIcon.svelte - колесо ожидания размером с родителя: подбор угла проходов; крутится CSS-анимацией, браузер ведёт её отдельно от расчёта, поэтому она не замирает -->
2
2
  <script lang="ts"></script>
3
3
 
4
- <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24"
4
+ <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" class="animate-spin [animation-duration:0.8s]"
5
5
  ><path fill="currentColor" d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z" opacity=".35" /><path
6
6
  fill="currentColor"
7
7
  d="M20 12h2A10 10 0 0 0 12 2V4A8 8 0 0 1 20 12Z"
8
- ><animateTransform attributeName="transform" dur="0.8s" from="0 12 12" repeatCount="indefinite" to="360 12 12" type="rotate" /></path
9
- ></svg
8
+ /></svg
10
9
  >