Trail casing: dark halo + light core

The accent line vanished on same-hue fills. Blend modes (difference/
exclusion) all keep a blind luminance where the stroke disappears and
composite expensively on old kiosk WebViews, so the trail now uses the
cartographers' trick instead: a neutral dark halo under a light core —
one of the two always contrasts with whatever is underneath. Verified
over glow fill (dark rooms + light pools) in one screenshot.
This commit is contained in:
Matysh
2026-07-31 09:59:27 +03:00
parent 893ccff94f
commit 692bb91e57
7 changed files with 58 additions and 18 deletions
@@ -1294,13 +1294,20 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
}
.vactrail polyline {
fill: none;
stroke: var(--hp-accent);
stroke-opacity: 0.35;
stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
/* dark halo + light core: neutral, and one of the two always contrasts
with whatever fill is underneath */
.vactrail .case {
stroke: rgba(0, 0, 0, 0.4);
stroke-width: 4.5;
}
.vactrail .core {
stroke: rgba(255, 255, 255, 0.82);
stroke-width: 1.8;
}
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;
@@ -2130,7 +2137,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</label>
${a.length?B`<div class="rhint">${ti(this._t("vac.cal_maps"),{maps:a.join(", ")})}</div>`:G}
`:G}
</div>`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`<polyline points="${e}"></polyline>`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`<div
</div>`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`<polyline class="case" points="${e}"></polyline><polyline class="core" points="${e}"></polyline>`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`<div
class="vacpuck ${a?.jump?"jump":""} ${_?"stale":""}"
style="left:${p}%;top:${u}%"
title=${o.name}
+10 -1
View File
@@ -58,6 +58,15 @@ const out = await page.evaluate(async () => {
const trail = sr().querySelector('.vactrail polyline');
o.trailDrawn = !!trail && trail.getAttribute('points').split(' ').length >= 3;
o.trailScaledByMatrix = !!trail && trail.getAttribute('points').startsWith('300.0,400.0');
// casing pair: dark halo + light core with identical geometry — the trail
// must survive any room fill underneath (owner 2026-07-31)
const tcase = sr().querySelector('.vactrail .case');
const tcore = sr().querySelector('.vactrail .core');
o.trailCasing = !!tcase && !!tcore
&& tcase.getAttribute('points') === tcore.getAttribute('points')
&& parseFloat(getComputedStyle(tcase).strokeWidth) > parseFloat(getComputedStyle(tcore).strokeWidth)
&& getComputedStyle(tcase).stroke.includes('0, 0, 0')
&& getComputedStyle(tcore).stroke.includes('255, 255, 255');
// trail off via marker option
@@ -128,7 +137,7 @@ checkAll(out, {
dockedNoPuck: true, baseMarkerThere: true, puckAppears: true, puckRound: true,
puckPlateMatchesDev: true, puck80pct: true,
noWedge: true, iconCentred: true, baseStaysDuringCleaning: true, trailDrawn: true,
trailScaledByMatrix: true, trailToggleOff: true,
trailScaledByMatrix: true, trailCasing: true, trailToggleOff: true,
puckGoneWhenDocked: true, trailLingers: true, hiddenNoPuck: true,
unknownMapNoPuck: true,
wizardDevFound: true, wizardBanner: true, wizardSavedMatrix: true,
+11 -4
View File
@@ -1294,13 +1294,20 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
}
.vactrail polyline {
fill: none;
stroke: var(--hp-accent);
stroke-opacity: 0.35;
stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
/* dark halo + light core: neutral, and one of the two always contrasts
with whatever fill is underneath */
.vactrail .case {
stroke: rgba(0, 0, 0, 0.4);
stroke-width: 4.5;
}
.vactrail .core {
stroke: rgba(255, 255, 255, 0.82);
stroke-width: 1.8;
}
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;
@@ -2130,7 +2137,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</label>
${a.length?B`<div class="rhint">${ti(this._t("vac.cal_maps"),{maps:a.join(", ")})}</div>`:G}
`:G}
</div>`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`<polyline points="${e}"></polyline>`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`<div
</div>`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`<polyline class="case" points="${e}"></polyline><polyline class="core" points="${e}"></polyline>`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`<div
class="vacpuck ${a?.jump?"jump":""} ${_?"stale":""}"
style="left:${p}%;top:${u}%"
title=${o.name}
+11 -4
View File
@@ -1294,13 +1294,20 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
}
.vactrail polyline {
fill: none;
stroke: var(--hp-accent);
stroke-opacity: 0.35;
stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
/* dark halo + light core: neutral, and one of the two always contrasts
with whatever fill is underneath */
.vactrail .case {
stroke: rgba(0, 0, 0, 0.4);
stroke-width: 4.5;
}
.vactrail .core {
stroke: rgba(255, 255, 255, 0.82);
stroke-width: 1.8;
}
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;
@@ -2130,7 +2137,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</label>
${a.length?B`<div class="rhint">${ti(this._t("vac.cal_maps"),{maps:a.join(", ")})}</div>`:G}
`:G}
</div>`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`<polyline points="${e}"></polyline>`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`<div
</div>`}_vacMapId(t,e){if("default"!==e.mapId)return e.mapId;const i=this._vacEntity(t),s=i?this.hass?.states[i]?.attributes?.selected_map:null;return s?String(s):"default"}_vacSaveMatrix(t,e,i,s){const o=this._serverCfg,n=o?.markers?.find(e=>e.id===t);if(!n)return;const r={...n.vacuum||{}};r.source=e,r.calibration={...r.calibration||{},[i]:s.map(t=>Number(t.toFixed(6)))},n.vacuum=r,this._regSignature="",this._saveConfig(),this.requestUpdate()}_vacAutoCalibrate(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;if(!e||!i||i.rooms.length<3)return void this._showToast(this._t("vac.autocal_no_rooms"));const s=this._spaceModel(t.space),o=(s?.rooms||[]).filter(t=>t.name&&t.poly?.length>=3).map(t=>{const e=Ae(t.poly);return{name:t.name,cx:e[0],cy:e[1]}}),n=function(t,e){const i=new Map(e.map(t=>[Ai(t.name),t])),s=[],o=[];for(const e of t){const t=i.get(Ai(e.name));t&&(s.push([[e.cx,e.cy],[t.cx,t.cy]]),o.push(e.name))}if(s.length<3)return null;const n=Di(s);return n?{matrix:n,matched:o,residual:Ti(n,s)}:null}(i.rooms,o);n?(n.residual>50&&this._showToast(ti(this._t("vac.autocal_res_warn"),{rooms:String(n.matched.length)})),this._vacSaveMatrix(t.id,e,this._vacMapId(t,i),n.matrix),this._showToast(ti(this._t("vac.autocal_done"),{rooms:String(n.matched.length)}))):this._showToast(this._t("vac.autocal_no_match"))}_vacStartWizard(t){const e=this._vacSource(t),i=e?Pi(this.hass?.states[e]?.attributes):null;e&&i?.pos?(this._markerDialog=null,this._vacCal={markerId:t.id,source:e,mapId:this._vacMapId(t,i),pairs:[]}):this._showToast(this._t("vac.cal_need_pos"))}_vacCalClick(t){const e=this._vacCal;if(!e)return;const i=Pi(this.hass?.states[e.source]?.attributes);if(!i?.pos)return void this._showToast(this._t("vac.cal_need_pos"));const s=this._svgPoint(t),o=[...e.pairs,[[i.pos.x,i.pos.y],[s[0],s[1]]]];if(o.length<3)return void(this._vacCal={...e,pairs:o});const n=Di(o);return n?Ti(n,o)>50&&o.length<4?(this._vacCal={...e,pairs:o},void this._showToast(this._t("vac.cal_res_warn"))):(this._vacSaveMatrix(e.markerId,e.source,e.mapId,n),this._vacCal=null,void this._showToast(this._t("vac.cal_done"))):(this._vacCal={...e,pairs:o.slice(0,-1)},void this._showToast(this._t("vac.cal_degenerate")))}_renderVacuums(t,e){if(this._markup||"decor"===this._mode)return G;const i=[],s=[];for(const o of t){if(o.hidden||!this._isVacDev(o))continue;const t=this._vacSource(o);if(!t)continue;const n=Pi(this.hass?.states[t]?.attributes);if(!n)continue;const r=o.marker?.vacuum?.calibration?.[this._vacMapId(o,n)];if(!r||6!==r.length)continue;const a=this._vacRt.get(o.id),l=a?.moving??!1,c=!l&&a?.endedTs&&Date.now()-a.endedTs<6e5;if(!1!==o.marker?.vacuum?.trail&&(l||c)){const t=n.path||a?.trail||[];if(t.length>1){const e=t.map(([t,e])=>{const[i,s]=Ci(r,t,e);return i.toFixed(1)+","+s.toFixed(1)}).join(" ");s.push(j`<polyline class="case" points="${e}"></polyline><polyline class="core" points="${e}"></polyline>`)}}if(!l||!n.pos)continue;const[h,d]=Ci(r,n.pos.x,n.pos.y),p=(h-e.x)/e.w*100,u=(d-e.y)/e.h*100,_=a&&a.lastTs>0&&Date.now()-a.lastTs>6e4,m=o.marker?.icon||o.icon||"mdi:robot-vacuum";i.push(B`<div
class="vacpuck ${a?.jump?"jump":""} ${_?"stale":""}"
style="left:${p}%;top:${u}%"
title=${o.name}
+1 -1
View File
@@ -75,7 +75,7 @@ marker is edited as usual.
draw as-is, includes history from before the card was opened.
- Otherwise self-recorded: client-side ring buffer, ~600 points with
Douglas-Peucker thinning, one SVG polyline.
- Style fixed (translucent accent line), no styling options. Lifecycle:
- Style fixed, no options: cartography casing — a dark translucent halo under a light core. Neutral (pure black/white alphas) and readable over any room fill; blend modes were rejected: each has a blind luminance where the line vanishes, and mix-blend-mode is costly on old kiosk WebViews. Lifecycle:
appears on cleaning start, lives until dock + 10 min, dissolves;
`docked → cleaning` clears the old trail. Ephemeral, never stored.
- Marker option «Показывать след уборки», on by default where data
+4 -1
View File
@@ -4541,7 +4541,10 @@ class HouseplanCard extends LitElement {
const [cx, cy] = applyAffine(matrix, x, y);
return cx.toFixed(1) + ',' + cy.toFixed(1);
}).join(' ');
trails.push(svg`<polyline points="${ptsStr}"></polyline>`);
// cartography casing: a dark halo under a light core. Neutral and
// visible over ANY room fill — blend modes all have a blind
// 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>`);
}
}
if (!moving || !tele.pos) continue;
+10 -3
View File
@@ -1291,13 +1291,20 @@ export const cardStyles = css`
}
.vactrail polyline {
fill: none;
stroke: var(--hp-accent);
stroke-opacity: 0.35;
stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
/* dark halo + light core: neutral, and one of the two always contrasts
with whatever fill is underneath */
.vactrail .case {
stroke: rgba(0, 0, 0, 0.4);
stroke-width: 4.5;
}
.vactrail .core {
stroke: rgba(255, 255, 255, 0.82);
stroke-width: 1.8;
}
.vacbox .vacbtns { display: flex; gap: 8px; margin: 6px 0; flex-wrap: wrap; }
.vaccalbar {
position: fixed;