feat: validate vacuum map routes and clean them up with their space

User-Visible: no
Issue: #162
This commit is contained in:
Codex
2026-09-03 19:18:13 +03:00
parent 51b4cc7e2c
commit 9809ec7a04
35 changed files with 534 additions and 268 deletions
+3 -2
View File
@@ -226,7 +226,7 @@ import {
} from './zero-walls';
import { snapNearAxisEndpoint } from './near-axis';
import type { SpaceReferenceRepairContext } from './space-reference-repair';
import { collectSpaceMarkerDependencies } from './space-deletion';
import { collectSpaceMarkerDependencies, spaceDeletionMessage } from './space-deletion';
import {
checkSpacePhysicalGeometry,
checkOptimizeGeometry,
@@ -8863,7 +8863,8 @@ public async _deleteSpace(): Promise<void> {
key: 'delete-space',
kind: 'destructive',
title: this.host._t('confirm.delete_space_title'),
message: this.host._t('confirm.delete_space_body'),
message: spaceDeletionMessage(this.host._t('confirm.delete_space_body'),
this.host._t('confirm.delete_space_vac_routes'), dependencies.routeCount),
objectName: sp.title,
confirmLabel: this.host._t('btn.delete'),
cancelLabel: this.host._t('btn.cancel'),
+2
View File
@@ -292,6 +292,7 @@
"confirm.erase_decor": "Das Objekt „{kind}“ löschen? Diese Aktion kann im Editor rückgängig gemacht werden.",
"confirm.delete_space_title": "Bereich löschen?",
"confirm.delete_space_body": "Der Bereich, alle seine Räume und die gesamte Planmarkierung werden gelöscht.",
"confirm.delete_space_vac_routes": "Zuordnungen von Roboterkarten werden ebenfalls entfernt: {count}.",
"space.delete_blocked": "Dieser Bereich wird noch von {n} Gerät(en) verwendet. Die Geräte zuerst in einen anderen Bereich verschieben oder löschen.",
"toast.pos_save_failed": "Position nicht gespeichert: {err}",
"toast.no_entity": "Das Gerät hat keine geeignete Entität",
@@ -1153,6 +1154,7 @@
"backup.error.invalid_value_badge_attribute": "Ungültiges Wert-Badge-Attribut",
"backup.error.invalid_value_badge_position": "Ungültige Wert-Badge-Position",
"backup.error.invalid_value_badge_source": "Ungültige Wert-Badge-Quelle",
"backup.error.invalid_vacuum_map_route": "Prüfen Sie die Zuordnung der Roboterkarten zu Räumen.",
"backup.error.invalid_value_source": "Ungültige Wertquelle",
"backup.error.invalid_value_source_attribute": "Ungültiges Wertquellen-Attribut",
"backup.error.io_error": "Eine Dateioperation auf dem Server ist fehlgeschlagen",
+2
View File
@@ -292,6 +292,7 @@
"confirm.erase_decor": "Erase the {kind} object? You can undo this action from the editor history.",
"confirm.delete_space_title": "Delete space?",
"confirm.delete_space_body": "The space, all its rooms and all its plan markup will be deleted.",
"confirm.delete_space_vac_routes": "Robot map assignments will also be removed: {count}.",
"space.delete_blocked": "This space is still used by {n} device(s). Move them to another space or delete them first.",
"toast.pos_save_failed": "Failed to save position: {err}",
"toast.no_entity": "The device has no suitable entity",
@@ -1153,6 +1154,7 @@
"backup.error.invalid_value_badge_attribute": "Invalid value badge attribute",
"backup.error.invalid_value_badge_position": "Invalid value badge position",
"backup.error.invalid_value_badge_source": "Invalid value badge source",
"backup.error.invalid_vacuum_map_route": "Check how the robot's maps are assigned to spaces.",
"backup.error.invalid_value_source": "Invalid value source",
"backup.error.invalid_value_source_attribute": "Invalid value source attribute",
"backup.error.io_error": "A file operation failed on the server",
+2
View File
@@ -292,6 +292,7 @@
"confirm.erase_decor": "Effacer l’objet {kind} ? Vous pouvez annuler cette action depuis l’historique de l’éditeur.",
"confirm.delete_space_title": "Supprimer l’espace ?",
"confirm.delete_space_body": "L’espace, toutes ses pièces et tout le tracé du plan seront supprimés.",
"confirm.delete_space_vac_routes": "Les affectations de cartes de robots seront aussi supprimées : {count}.",
"space.delete_blocked": "Cet espace est encore utilisé par {n} appareil(s). Déplacez-les vers un autre espace ou supprimez-les d’abord.",
"toast.pos_save_failed": "Échec de l’enregistrement de la position : {err}",
"toast.no_entity": "L’appareil n’a aucune entité adaptée",
@@ -1153,6 +1154,7 @@
"backup.error.invalid_value_badge_attribute": "Attribut de badge de valeur invalide",
"backup.error.invalid_value_badge_position": "Position de badge de valeur invalide",
"backup.error.invalid_value_badge_source": "Source de badge de valeur invalide",
"backup.error.invalid_vacuum_map_route": "Vérifiez l'affectation des cartes du robot aux espaces.",
"backup.error.invalid_value_source": "Source de valeur invalide",
"backup.error.invalid_value_source_attribute": "Attribut de source de valeur invalide",
"backup.error.io_error": "Une opération de fichier a échoué sur le serveur",
+2
View File
@@ -292,6 +292,7 @@
"confirm.erase_decor": "Стереть объект «{kind}»? Действие можно отменить из истории редактора.",
"confirm.delete_space_title": "Удалить пространство?",
"confirm.delete_space_body": "Пространство, все его комнаты и вся разметка плана будут удалены.",
"confirm.delete_space_vac_routes": "Также будет снято сопоставление карт роботов: {count}.",
"space.delete_blocked": "Это пространство всё ещё используется устройствами: {n}. Сначала перенесите их в другое пространство или удалите.",
"toast.pos_save_failed": "Не удалось сохранить позицию: {err}",
"toast.no_entity": "У устройства нет подходящей сущности",
@@ -1153,6 +1154,7 @@
"backup.error.invalid_value_badge_attribute": "Некорректный атрибут бейджа значения",
"backup.error.invalid_value_badge_position": "Некорректная позиция бейджа значения",
"backup.error.invalid_value_badge_source": "Некорректный источник бейджа значения",
"backup.error.invalid_vacuum_map_route": "Проверьте сопоставление карт робота пространствам.",
"backup.error.invalid_value_source": "Некорректный источник значения",
"backup.error.invalid_value_source_attribute": "Некорректный атрибут источника значения",
"backup.error.io_error": "Файловая операция на сервере не удалась",
+42 -1
View File
@@ -3,8 +3,21 @@
export interface SpaceDeletionDependencyReport {
markerIds: string[];
count: number;
/**
* Markers that live on ANOTHER floor but route one of their robot maps here
* (#162). They do not block the deletion — the dock is not in this space —
* but the user has to be told how many map assignments disappear with it.
*/
routeMarkerIds: string[];
routeCount: number;
}
/** Ids of routes this marker points at the space being deleted. */
const routesIntoSpace = (marker: any, spaceId: string): string[] =>
(marker?.vacuum?.map_routes || [])
.filter((route: any) => route && route.space === spaceId && typeof route.id === 'string')
.map((route: any) => route.id);
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
export function collectSpaceMarkerDependencies(
@@ -23,7 +36,27 @@ export function collectSpaceMarkerDependencies(
|| layout?.[marker.id]?.s === spaceId)
.map((marker: any) => marker.id))]
.sort((a: string, b: string) => a.localeCompare(b));
return { markerIds, count: markerIds.length };
const routeMarkerIds = (config?.markers || [])
.filter((marker: any) => marker?.removed !== true && typeof marker?.id === 'string')
.filter((marker: any) => !markerIds.includes(marker.id))
.filter((marker: any) => routesIntoSpace(marker, spaceId).length > 0)
.map((marker: any) => marker.id)
.sort((a: string, b: string) => a.localeCompare(b));
return {
markerIds, count: markerIds.length,
routeMarkerIds, routeCount: routeMarkerIds.length,
};
}
/**
* Confirm text for a space delete: the base warning, plus how many robot map
* assignments go with it (#162). Composed here so the count cannot quietly
* fall out of the dialog when the wording changes.
*/
export function spaceDeletionMessage(
base: string, routesTemplate: string, routeCount: number,
): string {
return routeCount ? `${base} ${routesTemplate.replace('{count}', String(routeCount))}` : base;
}
export function createSpaceDeletionCandidate(
@@ -61,5 +94,13 @@ export function createSpaceDeletionCandidate(
for (const [key, position] of Object.entries(layout)) {
if ((position as any)?.s === spaceId) delete layout[key];
}
// #162: a robot docked elsewhere keeps its dock and its other maps; only the
// routes that pointed here go, in the same logical operation as the space.
for (const marker of config.markers || []) {
const routes = marker?.vacuum?.map_routes;
if (!Array.isArray(routes)) continue;
const kept = routes.filter((route: any) => route?.space !== spaceId);
if (kept.length !== routes.length) marker.vacuum.map_routes = kept;
}
return { config, layout, dependencies };
}