mirror of
https://github.com/Matysh/houseplan-card
synced 2026-08-01 00:48:29 +00:00
Trail modes + the tip grows glued to the icon
«Показывать путь робота» is a three-way choice now: never / while cleaning (the default — the line hides the moment the run ends) / always (the only mode that also shows the faded previous run). trail_mode rides next to the legacy bool, which still maps in. The last segment no longer pops in when the next telemetry point arrives: a rAF sampler drags a tip line's endpoint to the puck's animated centre every frame, so the path visually pours out strictly from under the icon. The sampler runs only while pucks exist and stops itself.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -282,6 +282,9 @@ MARKER_SCHEMA = vol.Schema(
|
|||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Optional("live"): vol.Any(bool, None),
|
vol.Optional("live"): vol.Any(bool, None),
|
||||||
vol.Optional("trail"): vol.Any(bool, None),
|
vol.Optional("trail"): vol.Any(bool, None),
|
||||||
|
vol.Optional("trail_mode"): vol.Any(
|
||||||
|
None, vol.In(["never", "cleaning", "always"])
|
||||||
|
),
|
||||||
vol.Optional("room_highlight"): vol.Any(bool, None),
|
vol.Optional("room_highlight"): vol.Any(bool, None),
|
||||||
vol.Optional("source"): vol.Any(str, None),
|
vol.Optional("source"): vol.Any(str, None),
|
||||||
# one 6-number affine per robot map; numbers must be finite
|
# one 6-number affine per robot map; numbers must be finite
|
||||||
|
|||||||
+20
-2
@@ -87,7 +87,8 @@ const out = await page.evaluate(async () => {
|
|||||||
'vacuum.robo': { state: 'docked', attributes: { friendly_name: 'Робот' } } } };
|
'vacuum.robo': { state: 'docked', attributes: { friendly_name: 'Робот' } } } };
|
||||||
c._regSignature = ''; c.requestUpdate(); await c.updateComplete; await new Promise((r) => setTimeout(r, 30));
|
c._regSignature = ''; c.requestUpdate(); await c.updateComplete; await new Promise((r) => setTimeout(r, 30));
|
||||||
o.puckGoneWhenDocked = !sr().querySelector('.vacpuck');
|
o.puckGoneWhenDocked = !sr().querySelector('.vacpuck');
|
||||||
o.trailLingers = !!sr().querySelector('.vactrail polyline');
|
// default mode 'cleaning': the trail HIDES the moment the run ends (owner)
|
||||||
|
o.trailHiddenAfterDock = !sr().querySelector('.vactrail polyline');
|
||||||
|
|
||||||
// hidden marker renders neither puck nor trail
|
// hidden marker renders neither puck nor trail
|
||||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||||
@@ -104,6 +105,7 @@ const out = await page.evaluate(async () => {
|
|||||||
o.unknownMapNoPuck = !sr().querySelector('.vacpuck');
|
o.unknownMapNoPuck = !sr().querySelector('.vacpuck');
|
||||||
|
|
||||||
// ---- server-side runs: current + one previous (owner 2026-07-31) ----
|
// ---- server-side runs: current + one previous (owner 2026-07-31) ----
|
||||||
|
cfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum.trail_mode = 'always';
|
||||||
c._vacSrvTrails = { e_vacuum_robo: {
|
c._vacSrvTrails = { e_vacuum_robo: {
|
||||||
current: { map_id: 'm1', started: 1, ended: null,
|
current: { map_id: 'm1', started: 1, ended: null,
|
||||||
points: [[700, 500], [900, 500], [1100, 640], [950, 1150]] },
|
points: [[700, 500], [900, 500], [1100, 640], [950, 1150]] },
|
||||||
@@ -123,6 +125,21 @@ const out = await page.evaluate(async () => {
|
|||||||
// 4 server points, the live tail trimmed while moving -> 3 rendered
|
// 4 server points, the live tail trimmed while moving -> 3 rendered
|
||||||
o.srvCurTrimmed = !!cur && cur.getAttribute('points').split(' ').length === 3;
|
o.srvCurTrimmed = !!cur && cur.getAttribute('points').split(' ').length === 3;
|
||||||
o.srvPrevFaded = !!prevG && getComputedStyle(prevG).opacity === '0.4';
|
o.srvPrevFaded = !!prevG && getComputedStyle(prevG).opacity === '0.4';
|
||||||
|
// the growing tip is glued to the puck by the rAF sampler
|
||||||
|
const tip = sr().querySelector('line.tip');
|
||||||
|
o.tipExists = !!tip;
|
||||||
|
await new Promise((r) => setTimeout(r, 250));
|
||||||
|
const puckNow = sr().querySelector('.vacpuck').getBoundingClientRect();
|
||||||
|
const svgT = sr().querySelector('.vactrail');
|
||||||
|
const vbT = svgT.viewBox.baseVal; const sbT = svgT.getBoundingClientRect();
|
||||||
|
const tx = sbT.left + ((parseFloat(tip.getAttribute('x2')) - vbT.x) / vbT.width) * sbT.width;
|
||||||
|
const ty = sbT.top + ((parseFloat(tip.getAttribute('y2')) - vbT.y) / vbT.height) * sbT.height;
|
||||||
|
o.tipGluedToPuck = Math.hypot(tx - (puckNow.left + puckNow.width / 2), ty - (puckNow.top + puckNow.height / 2)) < 12;
|
||||||
|
// 'never' kills every line including the previous run
|
||||||
|
cfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum.trail_mode = 'never';
|
||||||
|
c._regSignature = ''; c.requestUpdate(); await c.updateComplete;
|
||||||
|
o.neverHidesAll = !sr().querySelector('.vactrail');
|
||||||
|
cfg.markers.find((m) => m.id === 'e_vacuum_robo').vacuum.trail_mode = null;
|
||||||
c._vacSrvTrails = {};
|
c._vacSrvTrails = {};
|
||||||
|
|
||||||
// ---- the fit panel (drag + corner-stretch) end to end ----
|
// ---- the fit panel (drag + corner-stretch) end to end ----
|
||||||
@@ -202,9 +219,10 @@ checkAll(out, {
|
|||||||
noWedge: true, iconCentred: true, baseStaysDuringCleaning: true, trailDrawn: true,
|
noWedge: true, iconCentred: true, baseStaysDuringCleaning: true, trailDrawn: true,
|
||||||
trailScaledByMatrix: true, trailBehindPuck: true, zoomTeleports: true,
|
trailScaledByMatrix: true, trailBehindPuck: true, zoomTeleports: true,
|
||||||
trailCasing: true, trailToggleOff: true,
|
trailCasing: true, trailToggleOff: true,
|
||||||
puckGoneWhenDocked: true, trailLingers: true, hiddenNoPuck: true,
|
puckGoneWhenDocked: true, trailHiddenAfterDock: true, hiddenNoPuck: true,
|
||||||
unknownMapNoPuck: true,
|
unknownMapNoPuck: true,
|
||||||
srvPrevRunShown: true, srvCurTrimmed: true, srvPrevFaded: true,
|
srvPrevRunShown: true, srvCurTrimmed: true, srvPrevFaded: true,
|
||||||
|
tipExists: true, tipGluedToPuck: true, neverHidesAll: true,
|
||||||
fitDevFound: true, fitOverlay: true, fitGhostRooms: true, fitLabels: true,
|
fitDevFound: true, fitOverlay: true, fitGhostRooms: true, fitLabels: true,
|
||||||
fitHandles: true, fitHandleHittable: true, fitGhostHittable: true, fitBar: true, fitMirrorDefault: true, fitDragMoves: true,
|
fitHandles: true, fitHandleHittable: true, fitGhostHittable: true, fitBar: true, fitMirrorDefault: true, fitDragMoves: true,
|
||||||
fitRotateKeepsCentre: true, fitCornerScales: true, fitSavedMatrix: true,
|
fitRotateKeepsCentre: true, fitCornerScales: true, fitSavedMatrix: true,
|
||||||
|
|||||||
+140
-138
File diff suppressed because one or more lines are too long
Vendored
+140
-138
File diff suppressed because one or more lines are too long
+50
-10
@@ -27,7 +27,7 @@ import {
|
|||||||
import { ContentSigner } from './signing';
|
import { ContentSigner } from './signing';
|
||||||
import {
|
import {
|
||||||
Affine, applyAffine, solveAffine, affineResidual, readVacTelemetry, isVacSourceState,
|
Affine, applyAffine, solveAffine, affineResidual, readVacTelemetry, isVacSourceState,
|
||||||
autoCalibrate, pushTrailPoint, isVacMoving, VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
|
autoCalibrate, pushTrailPoint, isVacMoving, vacTrailMode, VAC_TELEPORT_GAP_MS, VAC_STALE_MS,
|
||||||
FitParams, fitMatrix, fitFromMatrix, initialFit, reanchorFit, VacRoom,
|
FitParams, fitMatrix, fitFromMatrix, initialFit, reanchorFit, VacRoom,
|
||||||
VAC_TRAIL_LINGER_MS, Pt as VacPt,
|
VAC_TRAIL_LINGER_MS, Pt as VacPt,
|
||||||
} from './vacuum';
|
} from './vacuum';
|
||||||
@@ -353,6 +353,8 @@ class HouseplanCard extends LitElement {
|
|||||||
space switch) or a tab return must TELEPORT the puck — animating left/top
|
space switch) or a tab return must TELEPORT the puck — animating left/top
|
||||||
through a viewport change reads as «едет через весь план» (owner). */
|
through a viewport change reads as «едет через весь план» (owner). */
|
||||||
private _vacViewKey = '';
|
private _vacViewKey = '';
|
||||||
|
private _vacLastView: { x: number; y: number; w: number; h: number } | null = null;
|
||||||
|
private _vacRaf = 0;
|
||||||
/** server-recorded runs per marker: {current, previous} in raw robot coords */
|
/** server-recorded runs per marker: {current, previous} in raw robot coords */
|
||||||
private _vacSrvTrails: Record<string, any> = {};
|
private _vacSrvTrails: Record<string, any> = {};
|
||||||
private _unsubTrail?: () => void;
|
private _unsubTrail?: () => void;
|
||||||
@@ -458,6 +460,7 @@ class HouseplanCard extends LitElement {
|
|||||||
|
|
||||||
public disconnectedCallback(): void {
|
public disconnectedCallback(): void {
|
||||||
document.removeEventListener('visibilitychange', this._vacVisHandler);
|
document.removeEventListener('visibilitychange', this._vacVisHandler);
|
||||||
|
if (this._vacRaf) { cancelAnimationFrame(this._vacRaf); this._vacRaf = 0; }
|
||||||
window.removeEventListener('keydown', this._keyHandler);
|
window.removeEventListener('keydown', this._keyHandler);
|
||||||
clearInterval(this._cycleTimer);
|
clearInterval(this._cycleTimer);
|
||||||
clearTimeout(this._kioskDotsTimer);
|
clearTimeout(this._kioskDotsTimer);
|
||||||
@@ -4391,7 +4394,7 @@ class HouseplanCard extends LitElement {
|
|||||||
this._vacRt.set(d.id, rt);
|
this._vacRt.set(d.id, rt);
|
||||||
}
|
}
|
||||||
if (moving && !rt.moving) { rt.trail = []; rt.lastPos = null; } // a fresh run
|
if (moving && !rt.moving) { rt.trail = []; rt.lastPos = null; } // a fresh run
|
||||||
const wantTrail = d.marker?.vacuum?.trail !== false && !tele?.path;
|
const wantTrail = vacTrailMode(d.marker?.vacuum) !== 'never' && !tele?.path;
|
||||||
if (!moving && rt.moving) {
|
if (!moving && rt.moving) {
|
||||||
rt.endedTs = Date.now();
|
rt.endedTs = Date.now();
|
||||||
// the run is over: the puck has arrived everywhere it was going
|
// the run is over: the puck has arrived everywhere it was going
|
||||||
@@ -4455,11 +4458,12 @@ class HouseplanCard extends LitElement {
|
|||||||
@change=${(e: Event) => setVac({ live: (e.target as HTMLInputElement).checked ? null : false })} />
|
@change=${(e: Event) => setVac({ live: (e.target as HTMLInputElement).checked ? null : false })} />
|
||||||
<span>${this._t('vac.live')}</span>
|
<span>${this._t('vac.live')}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="srcrow">
|
<label>${this._t('vac.trail')}</label>
|
||||||
<input type="checkbox" .checked=${v.trail !== false}
|
<select class="areasel"
|
||||||
@change=${(e: Event) => setVac({ trail: (e.target as HTMLInputElement).checked ? null : false })} />
|
@change=${(e: Event) => setVac({ trail_mode: (e.target as HTMLSelectElement).value, trail: null })}>
|
||||||
<span>${this._t('vac.trail')}</span>
|
${(['never', 'cleaning', 'always'] as const).map((mv) => html`
|
||||||
</label>
|
<option value=${mv} ?selected=${vacTrailMode(v) === mv}>${this._t(('vac.trail_' + mv) as any)}</option>`)}
|
||||||
|
</select>
|
||||||
${cals.length ? html`<div class="rhint">${subst(this._t('vac.cal_maps'), { maps: cals.join(', ') })}</div>` : nothing}
|
${cals.length ? html`<div class="rhint">${subst(this._t('vac.cal_maps'), { maps: cals.join(', ') })}</div>` : nothing}
|
||||||
` : nothing}
|
` : nothing}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -4669,6 +4673,29 @@ class HouseplanCard extends LitElement {
|
|||||||
@pointercancel=${(e: PointerEvent) => this._vacFitPointer(e, view)}>${rects}${dot}${handles}</svg>`;
|
@pointercancel=${(e: PointerEvent) => this._vacFitPointer(e, view)}>${rects}${dot}${handles}</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Every frame: glue the growing tip segment to the animated puck centre. */
|
||||||
|
private _vacRafLoop(): void {
|
||||||
|
this._vacRaf = requestAnimationFrame(() => {
|
||||||
|
const sr = this.renderRoot as ShadowRoot;
|
||||||
|
const stage = this._stageEl;
|
||||||
|
const view = this._vacLastView;
|
||||||
|
const pucks = sr?.querySelectorAll?.('.vacpuck') || [];
|
||||||
|
if (!stage || !view || !pucks.length) { this._vacRaf = 0; return; }
|
||||||
|
const sb = stage.getBoundingClientRect();
|
||||||
|
for (const puck of pucks as any) {
|
||||||
|
const mid = puck.getAttribute('data-mid');
|
||||||
|
const pb = puck.getBoundingClientRect();
|
||||||
|
const cx = view.x + ((pb.left + pb.width / 2 - sb.left) / sb.width) * view.w;
|
||||||
|
const cy = view.y + ((pb.top + pb.height / 2 - sb.top) / sb.height) * view.h;
|
||||||
|
for (const line of sr.querySelectorAll(`line.tip[data-mid="${mid}"]`)) {
|
||||||
|
line.setAttribute('x2', cx.toFixed(1));
|
||||||
|
line.setAttribute('y2', cy.toFixed(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._vacRafLoop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Puck + trail for every live vacuum of the space. */
|
/** Puck + trail for every live vacuum of the space. */
|
||||||
private _renderVacuums(devs: DevItem[], view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing {
|
private _renderVacuums(devs: DevItem[], view: { x: number; y: number; w: number; h: number }): TemplateResult | typeof nothing {
|
||||||
if (this._markup || this._mode === 'decor') return nothing;
|
if (this._markup || this._mode === 'decor') return nothing;
|
||||||
@@ -4688,14 +4715,17 @@ class HouseplanCard extends LitElement {
|
|||||||
if (!matrix || matrix.length !== 6) continue;
|
if (!matrix || matrix.length !== 6) continue;
|
||||||
const rt = this._vacRt.get(d.id);
|
const rt = this._vacRt.get(d.id);
|
||||||
const moving = rt?.moving ?? false;
|
const moving = rt?.moving ?? false;
|
||||||
const lingering = !moving && rt?.endedTs && Date.now() - rt.endedTs < VAC_TRAIL_LINGER_MS;
|
const tmode = vacTrailMode(d.marker?.vacuum);
|
||||||
|
// owner 2026-07-31: hide when the cleanup is over (default), unless the
|
||||||
|
// mode says always; the previous run only ever shows in 'always'
|
||||||
|
const showCur = tmode === 'always' || (tmode === 'cleaning' && moving);
|
||||||
const srv = this._vacSrvTrails[d.id];
|
const srv = this._vacSrvTrails[d.id];
|
||||||
const mapNow = this._vacMapId(d, tele);
|
const mapNow = this._vacMapId(d, tele);
|
||||||
const srvCur = srv?.current?.map_id === mapNow && Array.isArray(srv.current.points) ? srv.current : null;
|
const srvCur = srv?.current?.map_id === mapNow && Array.isArray(srv.current.points) ? srv.current : null;
|
||||||
const srvPrev = srv?.previous?.map_id === mapNow && Array.isArray(srv.previous.points) ? srv.previous : null;
|
const srvPrev = srv?.previous?.map_id === mapNow && Array.isArray(srv.previous.points) ? srv.previous : null;
|
||||||
// the PREVIOUS run stays visible even at rest: users compare where the
|
// the PREVIOUS run stays visible even at rest: users compare where the
|
||||||
// robot has been against where it has not (owner call 2026-07-31)
|
// robot has been against where it has not (owner call 2026-07-31)
|
||||||
if (d.marker?.vacuum?.trail !== false && srvPrev && srvPrev.points.length > 1) {
|
if (tmode === 'always' && srvPrev && srvPrev.points.length > 1) {
|
||||||
const pts = srvPrev.points.map(([x, y]: number[]) => {
|
const pts = srvPrev.points.map(([x, y]: number[]) => {
|
||||||
const [cx2, cy2] = applyAffine(matrix, x, y);
|
const [cx2, cy2] = applyAffine(matrix, x, y);
|
||||||
return cx2.toFixed(1) + ',' + cy2.toFixed(1);
|
return cx2.toFixed(1) + ',' + cy2.toFixed(1);
|
||||||
@@ -4704,7 +4734,7 @@ class HouseplanCard extends LitElement {
|
|||||||
}
|
}
|
||||||
// trail source order: server current run (survives reloads, shared by
|
// trail source order: server current run (survives reloads, shared by
|
||||||
// every screen) → integration path → the local live buffer
|
// every screen) → integration path → the local live buffer
|
||||||
if (d.marker?.vacuum?.trail !== false && (moving || lingering || srvCur)) {
|
if (showCur && (moving || srvCur)) {
|
||||||
// any server/integration path ends at the CURRENT target — trim the
|
// any server/integration path ends at the CURRENT target — trim the
|
||||||
// live tail while moving so the line never runs ahead of the puck
|
// live tail while moving so the line never runs ahead of the puck
|
||||||
const full: VacPt[] = (srvCur?.points as VacPt[]) || tele.path || rt?.trail || [];
|
const full: VacPt[] = (srvCur?.points as VacPt[]) || tele.path || rt?.trail || [];
|
||||||
@@ -4719,6 +4749,13 @@ class HouseplanCard extends LitElement {
|
|||||||
// visible over ANY room fill — blend modes all have a blind
|
// visible over ANY room fill — blend modes all have a blind
|
||||||
// luminance where the line vanishes (owner request 2026-07-31).
|
// luminance where the line vanishes (owner request 2026-07-31).
|
||||||
trails.push(svg`<polyline class="case" points="${ptsStr}"></polyline><polyline class="core" points="${ptsStr}"></polyline>`);
|
trails.push(svg`<polyline class="case" points="${ptsStr}"></polyline><polyline class="core" points="${ptsStr}"></polyline>`);
|
||||||
|
// the LAST segment grows glued to the icon (owner: «след появлялся
|
||||||
|
// строго за иконкой»): a rAF sampler drags x2/y2 to the puck centre
|
||||||
|
if (moving) {
|
||||||
|
const [ax, ay] = applyAffine(matrix, raw[raw.length - 1][0], raw[raw.length - 1][1]);
|
||||||
|
const a1 = ax.toFixed(1), a2 = ay.toFixed(1);
|
||||||
|
trails.push(svg`<line class="case tip" data-mid="${d.id}" x1="${a1}" y1="${a2}" x2="${a1}" y2="${a2}"></line><line class="core tip" data-mid="${d.id}" x1="${a1}" y1="${a2}" x2="${a1}" y2="${a2}"></line>`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!moving || !tele.pos) continue;
|
if (!moving || !tele.pos) continue;
|
||||||
@@ -4728,6 +4765,7 @@ class HouseplanCard extends LitElement {
|
|||||||
const stale = rt && rt.lastTs > 0 && Date.now() - rt.lastTs > VAC_STALE_MS;
|
const stale = rt && rt.lastTs > 0 && Date.now() - rt.lastTs > VAC_STALE_MS;
|
||||||
const icon = d.marker?.icon || d.icon || 'mdi:robot-vacuum';
|
const icon = d.marker?.icon || d.icon || 'mdi:robot-vacuum';
|
||||||
pucks.push(html`<div
|
pucks.push(html`<div
|
||||||
|
data-mid="${d.id}"
|
||||||
class="vacpuck ${rt?.jump || jumpAll ? 'jump' : ''} ${stale ? 'stale' : ''}"
|
class="vacpuck ${rt?.jump || jumpAll ? 'jump' : ''} ${stale ? 'stale' : ''}"
|
||||||
style="left:${left}%;top:${top}%"
|
style="left:${left}%;top:${top}%"
|
||||||
title=${d.name}
|
title=${d.name}
|
||||||
@@ -4735,6 +4773,8 @@ class HouseplanCard extends LitElement {
|
|||||||
<ha-icon .icon=${icon}></ha-icon>
|
<ha-icon .icon=${icon}></ha-icon>
|
||||||
</div>`);
|
</div>`);
|
||||||
}
|
}
|
||||||
|
this._vacLastView = view;
|
||||||
|
if (pucks.length && !this._vacRaf) this._vacRafLoop();
|
||||||
if (!pucks.length && !trails.length) return nothing;
|
if (!pucks.length && !trails.length) return nothing;
|
||||||
return html`
|
return html`
|
||||||
${trails.length ? svg`<svg class="vactrail" viewBox="${view.x} ${view.y} ${view.w} ${view.h}" preserveAspectRatio="none">${trails}</svg>` : nothing}
|
${trails.length ? svg`<svg class="vactrail" viewBox="${view.x} ${view.y} ${view.w} ${view.h}" preserveAspectRatio="none">${trails}</svg>` : nothing}
|
||||||
|
|||||||
+5
-2
@@ -361,7 +361,7 @@
|
|||||||
"vac.status_none": "The integration reports no coordinates — the robot will only be shown at its base",
|
"vac.status_none": "The integration reports no coordinates — the robot will only be shown at its base",
|
||||||
"vac.autocal": "Set up automatically",
|
"vac.autocal": "Set up automatically",
|
||||||
"vac.live": "Live position on the plan",
|
"vac.live": "Live position on the plan",
|
||||||
"vac.trail": "Show the cleaning trail",
|
"vac.trail": "Show the robot's path",
|
||||||
"vac.cal_maps": "Calibrated maps: {maps}",
|
"vac.cal_maps": "Calibrated maps: {maps}",
|
||||||
"vac.autocal_no_rooms": "The integration reports no room list — use point calibration",
|
"vac.autocal_no_rooms": "The integration reports no room list — use point calibration",
|
||||||
"vac.autocal_no_match": "Room names did not match (need ≥3 in common) — use point calibration",
|
"vac.autocal_no_match": "Room names did not match (need ≥3 in common) — use point calibration",
|
||||||
@@ -373,5 +373,8 @@
|
|||||||
"vac.fit": "Fit manually",
|
"vac.fit": "Fit manually",
|
||||||
"vac.fit_hint": "Drag the robot map into place, stretch by the corners",
|
"vac.fit_hint": "Drag the robot map into place, stretch by the corners",
|
||||||
"vac.fit_rotate": "Rotate 90°",
|
"vac.fit_rotate": "Rotate 90°",
|
||||||
"vac.fit_mirror": "Mirror"
|
"vac.fit_mirror": "Mirror",
|
||||||
|
"vac.trail_never": "Never",
|
||||||
|
"vac.trail_cleaning": "While cleaning",
|
||||||
|
"vac.trail_always": "Always"
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -361,7 +361,7 @@
|
|||||||
"vac.status_none": "Интеграция не отдаёт координаты — робот будет показан только на базе",
|
"vac.status_none": "Интеграция не отдаёт координаты — робот будет показан только на базе",
|
||||||
"vac.autocal": "Настроить автоматически",
|
"vac.autocal": "Настроить автоматически",
|
||||||
"vac.live": "Живая позиция на плане",
|
"vac.live": "Живая позиция на плане",
|
||||||
"vac.trail": "Показывать след уборки",
|
"vac.trail": "Показывать путь робота",
|
||||||
"vac.cal_maps": "Откалиброваны карты: {maps}",
|
"vac.cal_maps": "Откалиброваны карты: {maps}",
|
||||||
"vac.autocal_no_rooms": "Интеграция не отдаёт список комнат — используйте калибровку по точкам",
|
"vac.autocal_no_rooms": "Интеграция не отдаёт список комнат — используйте калибровку по точкам",
|
||||||
"vac.autocal_no_match": "Не совпали имена комнат (нужно ≥3 общих) — используйте калибровку по точкам",
|
"vac.autocal_no_match": "Не совпали имена комнат (нужно ≥3 общих) — используйте калибровку по точкам",
|
||||||
@@ -373,5 +373,8 @@
|
|||||||
"vac.fit": "Подогнать вручную",
|
"vac.fit": "Подогнать вручную",
|
||||||
"vac.fit_hint": "Перетащите карту робота на место, растяните за уголки",
|
"vac.fit_hint": "Перетащите карту робота на место, растяните за уголки",
|
||||||
"vac.fit_rotate": "Повернуть 90°",
|
"vac.fit_rotate": "Повернуть 90°",
|
||||||
"vac.fit_mirror": "Отразить"
|
"vac.fit_mirror": "Отразить",
|
||||||
|
"vac.trail_never": "Не показывать никогда",
|
||||||
|
"vac.trail_cleaning": "Во время уборки",
|
||||||
|
"vac.trail_always": "Показывать всегда"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -58,7 +58,8 @@ export interface Marker {
|
|||||||
/** live robot vacuum (docs/VACUUM.md); absent on non-vacuum markers */
|
/** live robot vacuum (docs/VACUUM.md); absent on non-vacuum markers */
|
||||||
vacuum?: {
|
vacuum?: {
|
||||||
live?: boolean | null;
|
live?: boolean | null;
|
||||||
trail?: boolean | null;
|
trail?: boolean | null; // legacy bool; trail_mode wins
|
||||||
|
trail_mode?: 'never' | 'cleaning' | 'always' | null;
|
||||||
room_highlight?: boolean | null;
|
room_highlight?: boolean | null;
|
||||||
source?: string | null;
|
source?: string | null;
|
||||||
calibration?: Record<string, number[]>;
|
calibration?: Record<string, number[]>;
|
||||||
|
|||||||
@@ -290,3 +290,13 @@ export function reanchorFit(p: FitParams, prev: FitParams, sx: number, sy: numbe
|
|||||||
const [qx, qy] = applyAffine(trial, sx, sy);
|
const [qx, qy] = applyAffine(trial, sx, sy);
|
||||||
return { ...p, ox: px - qx, oy: py - qy };
|
return { ...p, ox: px - qx, oy: py - qy };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VacTrailMode = 'never' | 'cleaning' | 'always';
|
||||||
|
|
||||||
|
/** marker.vacuum → display mode; legacy bool maps in (false = never). */
|
||||||
|
export function vacTrailMode(v: { trail?: boolean | null; trail_mode?: string | null } | null | undefined): VacTrailMode {
|
||||||
|
const m = v?.trail_mode;
|
||||||
|
if (m === 'never' || m === 'cleaning' || m === 'always') return m;
|
||||||
|
if (v?.trail === false) return 'never';
|
||||||
|
return 'cleaning';
|
||||||
|
}
|
||||||
|
|||||||
@@ -956,3 +956,10 @@ class TestVacuum:
|
|||||||
import pytest
|
import pytest
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
v.MARKER_SCHEMA(_marker(vacuum={"teleport": True}))
|
v.MARKER_SCHEMA(_marker(vacuum={"teleport": True}))
|
||||||
|
|
||||||
|
def test_trail_mode_bounded(self):
|
||||||
|
import pytest
|
||||||
|
for ok in ("never", "cleaning", "always", None):
|
||||||
|
v.MARKER_SCHEMA(_marker(vacuum={"trail_mode": ok}))
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
v.MARKER_SCHEMA(_marker(vacuum={"trail_mode": "sometimes"}))
|
||||||
|
|||||||
Reference in New Issue
Block a user