mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
fix: seven small honesty fixes from the 29.08 audit (#369)
(a) documented: deleting a vacuum marker erases its server trail at once and a re-added marker starts from scratch (VACUUM.md + both USER-GUIDEs). (b) smoothVacPath reports dropped non-finite segments — one console warn per call with the count — instead of hiding the whole trail silently on a broken calibration matrix. (c) room climate (#317) now reaches legacy markers whose exported config carries an ABSENT area key rather than an explicit null: `== null` where the placement is decided. (d) the armed furniture preview follows Shift without mouse movement — window keydown/keyup listeners live exactly as long as the palette is armed, detached at every palette teardown. (e) only the primary mouse button places decor/furniture: a right or middle click with an armed tool is a no-op, touch/pen untouched. (f) a device whose registry entities were ALL deliberately disabled by the user no longer glows as an alive controller — the #318 entityless-active rule now requires a genuinely empty roster. (g) furniture-pack author corrected to Sergey Matyunin (Сергей Матюнин) per the owner's decision — LICENSE.md, README.md, pack.json, docs/FURNITURE.md, the provenance check in generate-furniture-assets and its unit; the source archive bytes are unchanged and the README notes the romanisation fix. Proofs: units for (b)/(c)/(f) including the #318 regression pair; new smoke_furniture_polish for (d)/(e) with listener add/remove counters and both mouse buttons; five registry mutants, one per code change. Issue: #369 User-Visible: yes
This commit is contained in:
@@ -212,7 +212,14 @@ export function controllerAvailability(hass: any, d: DevItem): DeviceAvailabilit
|
||||
return 'available';
|
||||
}
|
||||
const ownEntities = d.entities || [];
|
||||
// #369(г): "no roster" and "roster fully disabled by the user" are different
|
||||
// evidence. A truly entityless active device stays available (#318); a
|
||||
// device whose registry entities were all deliberately disabled is the user
|
||||
// opting out — it must not look alive.
|
||||
const disabledRoster = d.bindingStatus?.kind === 'ha_disabled'
|
||||
|| (d.bindingStatus?.allEntityIds?.length ?? 0) > 0;
|
||||
const activeEntitylessDevice = ownEntities.length === 0
|
||||
&& !disabledRoster
|
||||
&& (d.bindingKind === 'device' || d.marker?.binding?.startsWith('device:'))
|
||||
&& d.bindingStatus?.kind === 'active';
|
||||
if (activeEntitylessDevice) return 'available';
|
||||
|
||||
+3
-1
@@ -1403,7 +1403,9 @@ function markerClimateTarget(marker: Marker): string | null {
|
||||
// Persisted local-room placement uses an explicit null Area. Do not treat a
|
||||
// stale room_id without its space as a valid target or let it collide with a
|
||||
// room imported into another space.
|
||||
if (marker.area === null && marker.space && marker.room_id) {
|
||||
// #369(в): legacy/import configs carry an ABSENT area key, not an explicit
|
||||
// null — both mean "no HA Area", so both must resolve to the room placement.
|
||||
if (marker.area == null && marker.space && marker.room_id) {
|
||||
return roomClimateKey(marker.space, { id: marker.room_id, area: null });
|
||||
}
|
||||
return marker.area || null;
|
||||
|
||||
@@ -4075,6 +4075,11 @@ public _cancelDecorGesture(): void {
|
||||
|
||||
public _decorPointerDown(ev: PointerEvent): boolean {
|
||||
const t = this.host._decorTool;
|
||||
// #369(е): only the primary mouse button places objects — a right/middle
|
||||
// click with an armed tool must not stamp furniture or start a shape.
|
||||
// Touch/pen keep their own transactions (button is 0/-1 there).
|
||||
if (ev.pointerType === 'mouse' && ev.button !== 0
|
||||
&& t !== 'select' && t !== 'erase') return false;
|
||||
// A DRAWING tool owns the whole canvas. Pressing on top of an existing
|
||||
// shape must start a NEW figure at that very point — otherwise a line can
|
||||
// never begin at the end of another line, because the old line grabs the
|
||||
@@ -4672,9 +4677,37 @@ public _furnFieldToCm(v: number): number {
|
||||
return clampFurnCm(this.host._imperial ? v * 30.48 : v * 100);
|
||||
}
|
||||
|
||||
/** #369(д): the preview must follow Shift even without mouse movement. */
|
||||
private _furnShiftListener = (ev: KeyboardEvent): void => {
|
||||
if (ev.key !== 'Shift') return;
|
||||
const input = this.host._furnPreviewInput;
|
||||
if (!input) return;
|
||||
const free = ev.type === 'keydown';
|
||||
if (input.free === free) return;
|
||||
this.host._furnPreviewInput = { ...input, free };
|
||||
this.host.requestUpdate();
|
||||
};
|
||||
|
||||
private _furnShiftAttached = false;
|
||||
|
||||
private _furnShiftAttach(): void {
|
||||
if (this._furnShiftAttached) return;
|
||||
this._furnShiftAttached = true;
|
||||
window.addEventListener('keydown', this._furnShiftListener);
|
||||
window.addEventListener('keyup', this._furnShiftListener);
|
||||
}
|
||||
|
||||
public _furnShiftDetach(): void {
|
||||
if (!this._furnShiftAttached) return;
|
||||
this._furnShiftAttached = false;
|
||||
window.removeEventListener('keydown', this._furnShiftListener);
|
||||
window.removeEventListener('keyup', this._furnShiftListener);
|
||||
}
|
||||
|
||||
public _furnPick(symbol: string): void {
|
||||
const d = furnitureDefaultCm(symbol);
|
||||
this.host._furnPalette = { symbol, w: d.w, h: d.h };
|
||||
this._furnShiftAttach();
|
||||
}
|
||||
|
||||
public _resolveFurniturePlacement(
|
||||
@@ -4720,7 +4753,9 @@ public _furnPlace(raw: number[], free = false, pointerType = 'mouse'): void {
|
||||
// …and the editor goes back to the tool that can move what was just placed
|
||||
this.host._decorTool = 'select';
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = null;
|
||||
this._furnShiftDetach();
|
||||
this._clearFurniturePreview();
|
||||
this._recordGeometry(this.host._t('history.decor_add'), before);
|
||||
this._saveConfig();
|
||||
@@ -4868,6 +4903,7 @@ public _renderFurnPalette(): TemplateResult {
|
||||
@click=${() => {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = null;
|
||||
this.host._decorTool = 'select';
|
||||
}}>
|
||||
@@ -4879,6 +4915,7 @@ public _renderFurnPalette(): TemplateResult {
|
||||
<button class="btn ghost furnback" @click=${() => {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = null;
|
||||
}}>
|
||||
<ha-icon icon="mdi:arrow-left"></ha-icon>${this.host._t('furn.back_to_categories')}
|
||||
@@ -4903,6 +4940,7 @@ public _renderFurnPalette(): TemplateResult {
|
||||
@click=${() => {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = item.id;
|
||||
}}>
|
||||
${categoryPreview(item)}<span>${this.host._t(`furn.cat_${item.id}` as I18nKey)}</span>
|
||||
@@ -5223,6 +5261,7 @@ public _renderDecorSecondary(): EditorSecondaryModel | null {
|
||||
if (this.host._decorTool !== 'furniture') return;
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = null;
|
||||
this.host._decorTool = 'select';
|
||||
this.host.requestUpdate();
|
||||
@@ -5346,6 +5385,7 @@ public _renderDecorBar(): TemplateResult {
|
||||
if (t === 'furniture' && this.host._decorTool === 'furniture') {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = null;
|
||||
this.host._decorTool = 'select';
|
||||
return;
|
||||
@@ -5358,6 +5398,7 @@ public _renderDecorBar(): TemplateResult {
|
||||
if (t !== 'furniture') {
|
||||
this._clearFurniturePreview();
|
||||
this.host._furnPalette = null;
|
||||
this._furnShiftDetach();
|
||||
this.host._furnCategory = null;
|
||||
} else {
|
||||
this._clearFurniturePreview();
|
||||
|
||||
+11
-2
@@ -247,11 +247,17 @@ const samePoint = (a: Pt, b: Pt): boolean => a[0] === b[0] && a[1] === b[1];
|
||||
* deliberately knows nothing about SVG, Lit, scale conversion or flat/iso
|
||||
* projection; the renderer serialises and projects the typed commands.
|
||||
*/
|
||||
export function smoothVacPath(path: VacPath, maxRadius: number): VacPathCommand[][] {
|
||||
export function smoothVacPath(
|
||||
path: VacPath, maxRadius: number,
|
||||
warn: (message: string) => void = console.warn,
|
||||
): VacPathCommand[][] {
|
||||
if (!Number.isFinite(maxRadius) || maxRadius <= 0) return [];
|
||||
const out: VacPathCommand[][] = [];
|
||||
// #369(б): a NaN calibration matrix used to hide the whole trail silently —
|
||||
// one warn per call (a file may carry hundreds of segments) names the count.
|
||||
let droppedSegments = 0;
|
||||
for (const rawSegment of path) {
|
||||
if (!rawSegment.every(finitePoint)) continue;
|
||||
if (!rawSegment.every(finitePoint)) { droppedSegments++; continue; }
|
||||
const segment = rawSegment.filter((point, index) => index === 0 || !samePoint(point, rawSegment[index - 1]));
|
||||
if (segment.length < 2) continue;
|
||||
const commands: VacPathCommand[] = [{ kind: 'move', point: segment[0] }];
|
||||
@@ -286,6 +292,9 @@ export function smoothVacPath(path: VacPath, maxRadius: number): VacPathCommand[
|
||||
commands.push({ kind: 'line', point: segment[segment.length - 1] });
|
||||
out.push(commands);
|
||||
}
|
||||
if (droppedSegments > 0) {
|
||||
warn(`[houseplan] vacuum trail: ${droppedSegments} segment(s) dropped — non-finite point (check map calibration)`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user