mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 14:19:04 +00:00
feat(view): the moon in its phase over the "Follow the Sun" background (#661)
At dawn, dusk and night the environment shows the moon in the top-left corner of the scene, behind the plan: computed in the card from the home coordinates and the browser clock (short Meeus series + topocentric parallax, within 1.4° / 1.8 pp of JPL Horizons), one designer image under a continuous phase mask with the lit side always on the left (owner 2026-09-29), a feathered terminator, 3°/3 % thresholds and the 2 s fade of the window rays. Everything but a small gate lives in the lazy moon-runtime chunk, with its own 30 s ticker. General settings: "Sun" becomes "Sun and Moon" with one switch, on for new installations (DEFAULT_CONFIG), off for existing ones. The initial View graph sat 728 B under its budget: the gate is paid for by moving fifteen dialog-only strings of General settings into the lazy settings dictionary (#459) and by one build fingerprint literal instead of three, so the graph ends 4 B above dev. Golden: two new moon scenes, and the two General settings help frames show «Sun and Moon»; the WSL artifact test fixture now models scenes whose first capture awaits acceptance. Issue: #661 User-Visible: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e515bdaab8
commit
c8c470ed57
@@ -17,10 +17,15 @@ export function dayCycleStageVars(state: DayCycleState | null): string {
|
||||
].join(';');
|
||||
}
|
||||
|
||||
/** Four constant layers make gradient changes genuinely cross-fade in CSS. */
|
||||
/**
|
||||
* Four constant layers make gradient changes genuinely cross-fade in CSS. The
|
||||
* moon (#661) comes last: above the gradients and the sun glow, still inside
|
||||
* the environment and therefore below the plan paper and everything else.
|
||||
*/
|
||||
export function renderDayCycleEnvironment(
|
||||
state: DayCycleState | null,
|
||||
viewWeight = 1,
|
||||
moon: TemplateResult | typeof nothing = nothing,
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!state) return nothing;
|
||||
const position = [
|
||||
@@ -39,6 +44,6 @@ export function renderDayCycleEnvironment(
|
||||
data-day-cycle-layer=${phase} style=${background}>
|
||||
<div class="hp-day-cycle-sun" style="background:radial-gradient(circle, ${palette.sun} 0%, transparent 67%)"></div>
|
||||
</div>`;
|
||||
})}
|
||||
})}${moon}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export type EditorRuntimeLoaderState = 'idle' | 'loading' | 'ready' | 'failed';
|
||||
|
||||
/** This build's id, embedded at build time; every lazy runtime must carry the same. */
|
||||
export const ENTRY_BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
|
||||
|
||||
export interface EditorRuntimeModule<Runtime> {
|
||||
readonly fingerprint: string;
|
||||
create(): Runtime;
|
||||
|
||||
@@ -43,3 +43,14 @@ export function generalProblems(d: GeneralSettingsDraft): GeneralProblem[] {
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
/** #661: the moon switch reads only an explicit `true` (absent, `false` or garbage — off). */
|
||||
export function moonDraftOf(settings: { moon?: unknown } | null | undefined): boolean {
|
||||
return settings?.moon === true;
|
||||
}
|
||||
|
||||
/** #661: saving writes `true` or removes the key — `false` is never stored (as volumetric_view). */
|
||||
export function writeMoonSetting(settings: { moon?: unknown }, on: boolean): void {
|
||||
if (on) settings.moon = true;
|
||||
else delete settings.moon;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
: nothing;
|
||||
|
||||
return html`<hp-dialog .hass=${host.hass} data-kind="settings" form-shell wide
|
||||
.title=${t('gs.title')} icon="mdi:cog-outline" @hp-close=${requestClose}>
|
||||
.title=${st('gs.title')} icon="mdi:cog-outline" @hp-close=${requestClose}>
|
||||
<div class="body hpf-form">
|
||||
${formCard({
|
||||
id: 'display',
|
||||
@@ -94,7 +94,7 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
})}
|
||||
${toggleRow({
|
||||
id: 'gs-radar-live', icon: 'mdi:radar',
|
||||
title: t('gs.radar_show_live'), caption: st('gs.radar_show_live_hint'),
|
||||
title: st('gs.radar_show_live'), caption: st('gs.radar_show_live_hint'),
|
||||
checked: d.radarShowLive, onChange: (v) => set({ radarShowLive: v }),
|
||||
})}
|
||||
${toggleRow({
|
||||
@@ -116,11 +116,11 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
title: t('gs.card_fills'),
|
||||
help: this._help('gs.card_fills.help'),
|
||||
body: html`
|
||||
${subsection({ title: t('gs.light_group') })}
|
||||
${subsection({ title: st('gs.light_group') })}
|
||||
${colorTiles([tile('light_on', 'gs.light_on'), tile('light_off', 'gs.light_off'), tile('light_none', 'gs.light_none')])}
|
||||
${subsection({ title: t('gs.temp_group') })}
|
||||
${subsection({ title: st('gs.temp_group') })}
|
||||
${colorTiles([tile('temp_cold', 'gs.temp_cold'), tile('temp_ok', 'gs.temp_ok'), tile('temp_hot', 'gs.temp_hot')])}
|
||||
${subsection({ title: t('gs.lqi_group') })}
|
||||
${subsection({ title: st('gs.lqi_group') })}
|
||||
${colorTiles([tile('lqi_low', 'gs.lqi_low'), tile('lqi_high', 'gs.lqi_high')])}`,
|
||||
})}
|
||||
${formCard({
|
||||
@@ -129,7 +129,7 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
body: html`
|
||||
${colorTiles([tile('glow_base', 'gs.glow_base'), tile('glow_light', 'gs.glow_light')])}
|
||||
${field({
|
||||
label: t('gs.glow_radius'), htmlFor: 'gs-glow-radius', help: this._help('gs.glow_radius.help'),
|
||||
label: st('gs.glow_radius'), htmlFor: 'gs-glow-radius', help: this._help('gs.glow_radius.help'),
|
||||
error: glowProblem ? st(glowProblem.message) : undefined,
|
||||
control: unitInput({
|
||||
id: 'gs-glow-radius', value: d.glowRadiusInput, min: 0.5, step: 0.5, invalid: !!glowProblem,
|
||||
@@ -146,13 +146,13 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
title: t('gs.card_plan'),
|
||||
body: html`
|
||||
${colorRow({
|
||||
label: t('gs.wall_fill'),
|
||||
label: st('gs.wall_fill'),
|
||||
picker: colorField({
|
||||
hex: d.colors.wall_fill.c, opacity: d.colors.wall_fill.a, opacityLabel: t('space.opacity'),
|
||||
onOpacity: (a) => this._setFillColor('wall_fill', { c: d.colors.wall_fill.c, a }),
|
||||
resetLabel: t('btn.reset'),
|
||||
onReset: () => this._setFillColor('wall_fill', { ...DEFAULT_FILL_COLORS.wall_fill }),
|
||||
picker: html`<hp-color-opacity .label=${t('gs.wall_fill')} hide-label flat-swatch
|
||||
picker: html`<hp-color-opacity .label=${st('gs.wall_fill')} hide-label flat-swatch
|
||||
.opacityLabel=${t('space.opacity')} .pickerLabels=${host._colorPickerLabels}
|
||||
.color=${d.colors.wall_fill.c} .opacity=${d.colors.wall_fill.a} .showOpacity=${true}
|
||||
@hp-color-opacity-change=${(e: CustomEvent<{ color: string; opacity: number }>) => {
|
||||
@@ -161,11 +161,11 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
}),
|
||||
})}
|
||||
${field({
|
||||
label: t('gs.bg_mode'), help: this._help('gs.bg_mode.help'),
|
||||
label: st('gs.bg_mode'), help: this._help('gs.bg_mode.help'),
|
||||
control: segmented({
|
||||
name: 'gs-bg-mode',
|
||||
value: d.bgMode === 'daynight' ? 'daynight' : 'static',
|
||||
ariaLabel: t('gs.bg_mode'),
|
||||
ariaLabel: st('gs.bg_mode'),
|
||||
options: [
|
||||
{ value: 'static', label: t('gs.bg_static') },
|
||||
{ value: 'daynight', label: t('gs.bg_daynight') },
|
||||
@@ -201,7 +201,7 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
<div class="hpf-inline hpf-wrap">
|
||||
${unitInput({
|
||||
id: 'gs-north', value: d.northDegInput, unit: '°',
|
||||
min: 0, max: 359, step: 1, placeholder: t('gs.north_ph'), invalid: !!northProblem,
|
||||
min: 0, max: 359, step: 1, placeholder: st('gs.north_ph'), invalid: !!northProblem,
|
||||
onInput: (raw) => {
|
||||
const trimmed = raw.trim();
|
||||
const n = trimmed === '' ? null : strictNumber(trimmed);
|
||||
@@ -221,10 +221,15 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
${northProblem ? html`<p class="hpf-error" role="alert">${st(northProblem.message)}</p>` : nothing}
|
||||
${toggleRow({
|
||||
id: 'gs-sun-rays', icon: 'mdi:weather-sunny',
|
||||
title: t('gs.sun_rays'), caption: st('gs.sun_rays_hint'),
|
||||
title: st('gs.sun_rays'), caption: st('gs.sun_rays_hint'),
|
||||
checked: d.sunRays, onChange: (v) => set({ sunRays: v }),
|
||||
})}
|
||||
${renderSunRayOriginSegment(d.sunRayOrigin, (key) => t(key), (sunRayOrigin) => set({ sunRayOrigin }))}`,
|
||||
${renderSunRayOriginSegment(d.sunRayOrigin, (key) => t(key), (sunRayOrigin) => set({ sunRayOrigin }))}
|
||||
${toggleRow({
|
||||
id: 'gs-moon', icon: 'mdi:moon-waning-crescent',
|
||||
title: st('gs.moon'), caption: st('gs.moon_hint'),
|
||||
checked: d.moon, onChange: (v) => set({ moon: v }),
|
||||
})}`,
|
||||
})}
|
||||
${formCard({
|
||||
id: 'data',
|
||||
@@ -234,28 +239,28 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
${subsection({ title: t('gs.backup_group'), help: this._help('gs.backup_group.help') })}
|
||||
<div class="hpf-actions">
|
||||
<button class="btn" @click=${() => this._openBackupExport()}>
|
||||
<ha-icon icon="mdi:download"></ha-icon>${t('backup.export_open')}
|
||||
<ha-icon icon="mdi:download"></ha-icon>${st('backup.export_open')}
|
||||
</button>
|
||||
<span class="backupupload">
|
||||
<button class="btn" type="button" @click=${(e: Event) =>
|
||||
((e.currentTarget as HTMLElement).nextElementSibling as HTMLInputElement | null)?.click()}>
|
||||
<ha-icon icon="mdi:upload"></ha-icon>${t('backup.import_open')}
|
||||
<ha-icon icon="mdi:upload"></ha-icon>${st('backup.import_open')}
|
||||
</button>
|
||||
<input type="file" accept="application/json,.json" @change=${(event: Event) => this._pickBackupImport(event)} />
|
||||
</span>
|
||||
${host._canOptimizeUndo && host._undoKind === 'import' ? html`
|
||||
<button class="btn" @click=${() => this._undoPlanOptimization()} ?disabled=${host._optimizeUndoBusy}>
|
||||
<ha-icon icon="mdi:undo-variant"></ha-icon>${t('backup.undo_import')}
|
||||
<ha-icon icon="mdi:undo-variant"></ha-icon>${st('backup.undo_import')}
|
||||
</button>` : nothing}
|
||||
</div>` : nothing}
|
||||
${subsection({ title: t('gs.grid_group'), help: this._help('gs.grid_group.help') })}
|
||||
${subsection({ title: st('gs.grid_group'), help: this._help('gs.grid_group.help') })}
|
||||
<div class="hpf-actions">
|
||||
<button class="btn" @click=${() => this.optimizePlans.open()}>
|
||||
<ha-icon icon="mdi:broom"></ha-icon>${t('gs.align_all')}
|
||||
</button>
|
||||
${host._canOptimizeUndo && host._undoKind !== 'import' ? html`
|
||||
<button class="btn" @click=${() => this._undoPlanOptimization()} ?disabled=${host._optimizeUndoBusy}>
|
||||
<ha-icon icon="mdi:undo-variant"></ha-icon>${t('gs.optimize_undo')}
|
||||
<ha-icon icon="mdi:undo-variant"></ha-icon>${st('gs.optimize_undo')}
|
||||
</button>` : nothing}
|
||||
</div>`,
|
||||
})}
|
||||
@@ -266,7 +271,7 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
colors: JSON.parse(JSON.stringify(DEFAULT_FILL_COLORS)), glowRadius: host._imperial ? 9.8 : 3,
|
||||
glowRadiusInput: String(host._imperial ? 9.8 : 3),
|
||||
bgColor: null, northDeg: null, northDegInput: '', bgMode: 'daynight', sunRays: false, sunRayOrigin: 'inner',
|
||||
showRoomTooltip: true, radarShowLive: true, volumetricView: false, zigbeeTopology: { enabled: false, z2mBaseTopics: [] },
|
||||
showRoomTooltip: true, radarShowLive: true, volumetricView: false, moon: true, zigbeeTopology: { enabled: false, z2mBaseTopics: [] },
|
||||
})}>${t('gs.reset')}</button>
|
||||
</div>
|
||||
${footerStatus(problems.length
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
* get one `requestUpdate()` when the runtime settles.
|
||||
*/
|
||||
import type { FurnitureGraphic } from './furniture';
|
||||
import { ENTRY_BUILD_FINGERPRINT } from './editor-runtime-loader';
|
||||
|
||||
export type FurnitureArtState = 'ready' | 'pending' | 'fallback';
|
||||
|
||||
@@ -211,7 +212,6 @@ export function ensureFurnitureArtFor(
|
||||
|
||||
/* Page-scoped runtime wired to the real chunk. The retry token is replaced
|
||||
* at build time with the exact content-hashed asset (bundle-manifest.mjs). */
|
||||
const ENTRY_BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
|
||||
const FURNITURE_ART_RETRY_ASSET = '__HOUSEPLAN_FURNITURE_ART_RETRY_ASSET__';
|
||||
let retrySeq = 0;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
rayRimEdges, rimStops, rimPeakAlpha, RIM_COLOR, type SunRayOrigin,
|
||||
} from './sun';
|
||||
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
|
||||
import { moonLayer } from './moon-gate';
|
||||
import { renderPaperShapes, type PaperShape } from './render/paper-scene';
|
||||
import {
|
||||
furnitureGraphic, furnitureArtIsLazy,
|
||||
@@ -231,7 +232,7 @@ import {
|
||||
roomFitGeometryBounds, roomFitOwnerFromPath, STAGE_TAP_DISTANCE_PX,
|
||||
type RoomFitGestureCandidate,
|
||||
} from './room-fit';
|
||||
import { EditorRuntimeLoader, lazyLoadFailureMessage, safeRuntimeDiagnostic, type EditorRuntimeLoaderState } from './editor-runtime-loader';
|
||||
import { ENTRY_BUILD_FINGERPRINT, EditorRuntimeLoader, lazyLoadFailureMessage, safeRuntimeDiagnostic, type EditorRuntimeLoaderState } from './editor-runtime-loader';
|
||||
import { FURNITURE_ART_RUNTIME, composeUnsub, ensureFurnitureArtFor, furnitureArtBootPending, subscribeFurnitureArtLoadFailures } from './furniture-art-runtime';
|
||||
import type { BackdropGuardState } from './backdrop-pick';
|
||||
import {
|
||||
@@ -302,7 +303,6 @@ import { isoWallMaterialVars, parseCssColor } from './iso-materials'; import { I
|
||||
import { renderIsoTileShadow } from './iso-tiles';
|
||||
import { displayVersion } from './card-version';
|
||||
const CARD_VERSION = '1.78.0';
|
||||
const ENTRY_BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
|
||||
const EDITOR_RETRY_ASSET = '__HOUSEPLAN_EDITOR_RETRY_ASSET__';
|
||||
const ISO_RETRY_ASSET = '__HOUSEPLAN_ISO_RETRY_ASSET__';
|
||||
const PDF_RETRY_ASSET = '__HOUSEPLAN_PDF_RETRY_ASSET__';
|
||||
@@ -2050,7 +2050,7 @@ export class HouseplanCard extends LitElement {
|
||||
colors: FillColors; glowRadius: number; glowRadiusInput: string; bgColor: string | null;
|
||||
/** sun on the plan (docs/SUN.md) */
|
||||
northDeg: number | null; northDegInput: string; bgMode: 'static' | 'daynight'; sunRays: boolean; sunRayOrigin: SunRayOrigin;
|
||||
showRoomTooltip: boolean; zigbeeTopology: import('./zigbee-topology-settings').ZigbeeTopologySettings; radarShowLive: boolean; volumetricView: boolean; busy: boolean;
|
||||
showRoomTooltip: boolean; zigbeeTopology: import('./zigbee-topology-settings').ZigbeeTopologySettings; radarShowLive: boolean; volumetricView: boolean; moon: boolean; busy: boolean;
|
||||
} | null = null;
|
||||
private _pdfDialog = false;
|
||||
private _supportDialog: SupportDialogState | null = null;
|
||||
@@ -9772,6 +9772,7 @@ export class HouseplanCard extends LitElement {
|
||||
bg_mode: gd.bgMode,
|
||||
sun_rays: gd.sunRays,
|
||||
sun_ray_origin: gd.sunRayOrigin,
|
||||
moon: gd.moon,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10873,7 +10874,7 @@ export class HouseplanCard extends LitElement {
|
||||
@pointerleave=${(e: PointerEvent) => this._stagePointerLeave(e)}
|
||||
@pointerup=${(e: PointerEvent) => this._stagePointerUp(e)}
|
||||
@pointercancel=${(e: PointerEvent) => this._stagePointerCancel(e)}>
|
||||
${renderDayCycleEnvironment(dayCycle, dayCycleWeight)}
|
||||
${renderDayCycleEnvironment(dayCycle, dayCycleWeight, moonLayer(this, this._sunGlobal(), dayCycle))}
|
||||
${this._editorRuntime ? this._renderEditorSecondary() : nothing}
|
||||
<div class="zoomwrap ${this._slide ? 'slide-' + this._slide : ''}"
|
||||
?inert=${this._continuity.overlayBlocksInteraction || this._modeTransitionBusy}
|
||||
|
||||
@@ -110,7 +110,7 @@ import {
|
||||
roomTempThresholdDraft, roomTempThresholdInputValues, strictNumber, type SpaceDialogState,
|
||||
} from './space-dialog';
|
||||
import { rememberSpaceDialogBaseline, spaceDialogProblems } from './editors/space-form-state';
|
||||
import { generalProblems, rememberGeneralBaseline } from './editors/general-form-state';
|
||||
import { generalProblems, moonDraftOf, rememberGeneralBaseline, writeMoonSetting } from './editors/general-form-state';
|
||||
import { rememberRoomBaseline } from './editors/room-form-state';
|
||||
import { forgetMarkerBaseline, rememberMarkerBaseline } from './editors/marker-form-state';
|
||||
import { commitPlanOptimization } from './plan-optimize-write';
|
||||
@@ -713,9 +713,9 @@ export interface HouseplanEditorHostPort {
|
||||
_sentPos: Map<string, DeviceLayout[string] | null>;
|
||||
_serverCfg: ServerConfig | null;
|
||||
_serverStorage: boolean;
|
||||
_settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; show_room_tooltip?: boolean; sun_ray_origin?: SunRayOrigin; zigbee_topology?: { enabled?: boolean; z2m_base_topics?: string[] }; radar?: { version?: 1; show_live?: boolean; [key: string]: unknown }; };
|
||||
_settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; show_room_tooltip?: boolean; moon?: boolean; sun_ray_origin?: SunRayOrigin; zigbee_topology?: { enabled?: boolean; z2m_base_topics?: string[] }; radar?: { version?: 1; show_live?: boolean; [key: string]: unknown }; };
|
||||
_terminalFrame: 0 | 1 | 2;
|
||||
_settingsDialog: { colors: FillColors; glowRadius: number; glowRadiusInput: string; bgColor: string | null; northDeg: number | null; northDegInput: string; bgMode: "static" | "daynight"; sunRays: boolean; sunRayOrigin: SunRayOrigin; showRoomTooltip: boolean; zigbeeTopology: ZigbeeTopologySettings; radarShowLive: boolean; volumetricView: boolean; busy: boolean; } | null;
|
||||
_settingsDialog: { colors: FillColors; glowRadius: number; glowRadiusInput: string; bgColor: string | null; northDeg: number | null; northDegInput: string; bgMode: "static" | "daynight"; sunRays: boolean; sunRayOrigin: SunRayOrigin; showRoomTooltip: boolean; zigbeeTopology: ZigbeeTopologySettings; radarShowLive: boolean; volumetricView: boolean; moon: boolean; busy: boolean; } | null;
|
||||
_supportDialog: SupportDialogState | null;
|
||||
_showAll: boolean;
|
||||
_showHidden: boolean;
|
||||
@@ -8454,7 +8454,7 @@ public _openSettingsDialog = (): void => {
|
||||
bgMode: bgModeOf(this.host._settings, {}),
|
||||
sunRays: sunRaysOn(this.host._settings, {}),
|
||||
sunRayOrigin: sunRayOriginOf(this.host._settings),
|
||||
radarShowLive: this.host._settings.radar?.show_live !== false, volumetricView: volumetricViewOf(this.host._settings),
|
||||
radarShowLive: this.host._settings.radar?.show_live !== false, volumetricView: volumetricViewOf(this.host._settings), moon: moonDraftOf(this.host._settings),
|
||||
showRoomTooltip: showRoomTooltipOf(this.host._settings), zigbeeTopology: zigbeeTopologySettingsOf(this.host._settings), busy: false,
|
||||
};
|
||||
// #600 К10: снимок на момент открытия — от него считается «есть изменения».
|
||||
@@ -9512,6 +9512,7 @@ public _updateDecorStyle(next: DecorStyle): void {
|
||||
else settings.show_room_tooltip = false;
|
||||
if (d.volumetricView) settings.volumetric_view = true;
|
||||
else delete settings.volumetric_view;
|
||||
writeMoonSetting(settings, d.moon);
|
||||
const radarSettings = settings.radar;
|
||||
if (!radarSettings || radarSettings.version == null || radarSettings.version === 1) {
|
||||
settings.radar = { ...(radarSettings || {}), version: 1 };
|
||||
|
||||
+1
-16
@@ -481,20 +481,15 @@
|
||||
"pdf.asset_failed": "Ein Hintergrund- oder Dekorbild konnte nicht geladen werden.",
|
||||
"pdf.scale": "Maßstab 1:{n}",
|
||||
"pdf.north": "Nord",
|
||||
"gs.title": "Allgemeine Einstellungen",
|
||||
"gs.light_group": "Beleuchtung",
|
||||
"gs.light_on": "Lichter an",
|
||||
"gs.light_off": "Alle Lichter aus",
|
||||
"gs.temp_group": "Temperatur",
|
||||
"gs.temp_cold": "Kalt",
|
||||
"gs.temp_ok": "Angenehm",
|
||||
"gs.temp_hot": "Heiß",
|
||||
"gs.lqi_group": "Zigbee-Signal",
|
||||
"gs.lqi_low": "Schwaches Signal",
|
||||
"gs.lqi_high": "Starkes Signal",
|
||||
"gs.reset": "Auf Standard zurücksetzen",
|
||||
"gs.saved": "Allgemeine Einstellungen gespeichert",
|
||||
"gs.radar_show_live": "Live-Präsenz auf dem Plan anzeigen",
|
||||
"gs.volumetric_view": "Plan in 2.5D anzeigen",
|
||||
"radar.title": "Präsenz auf dem Plan",
|
||||
"radar.help": "Verbinde diese Markierung mit exakten Home-Assistant-Radarentitäten und wähle Position sowie Koordinatenprofil. Live-Positionen werden auf den ausgewählten Raum begrenzt.",
|
||||
@@ -730,8 +725,6 @@
|
||||
"gs.glow_group": "Glow von Lichtquellen",
|
||||
"gs.glow_base": "Grunddunkelheit des Hauses",
|
||||
"gs.glow_light": "Standardlichtfarbe / Intensität",
|
||||
"gs.wall_fill": "Wandfüllung",
|
||||
"gs.glow_radius": "Glow-Radius",
|
||||
"gs.glow_radius.help": "Legt den allgemeinen Radius des Lichtscheins in Metern oder Fuß fest; ein gerätespezifischer Radius überschreibt diesen Wert.",
|
||||
"gs.glow_radius.help.aria": "Hilfe: allgemeiner Glow-Radius",
|
||||
"gs.unit_m": "m",
|
||||
@@ -1014,20 +1007,17 @@
|
||||
"vac.trail_always": "Immer",
|
||||
"gs.bg_color": "Hintergrund um den Plan",
|
||||
"gs.bg_default": "Standardthema",
|
||||
"gs.bg_mode": "Planhintergrund",
|
||||
"gs.bg_mode.help": "„Folgt der Sonne“ verwendet sun.sun und ersatzweise die lokale Browserzeit; „Statische Farbe“ zeigt immer die ausgewählte Farbe.",
|
||||
"gs.bg_mode.help.aria": "Hilfe: allgemeiner Planhintergrund",
|
||||
"gs.bg_static": "Statische Farbe",
|
||||
"gs.bg_daynight": "Folgt der Sonne",
|
||||
"gs.sun_group": "Sonne",
|
||||
"gs.sun_group": "Sonne und Mond",
|
||||
"gs.sun_missing": "Die sun.sun-Entität wurde nicht gefunden — der Hintergrund folgt Ihrer lokalen Uhr; Fensterstrahlen sind nicht verfügbar.",
|
||||
"gs.north": "Norden auf dem Plan",
|
||||
"gs.north.help": "Der Winkel wird im Uhrzeigersinn von der oberen Vertikalen des Plans gemessen; Norden wird für Fensterstrahlen benötigt, nicht für den Hintergrund „Folgt der Sonne“.",
|
||||
"gs.north.help.aria": "Hilfe: allgemeine Nordrichtung",
|
||||
"gs.north_ph": "nicht gesetzt",
|
||||
"gs.north_clear": "Löschen",
|
||||
"gs.north_letter": "N",
|
||||
"gs.sun_rays": "Sonnenlicht durch Fenster",
|
||||
"gs.sun_ray_origin": "Sonnenstrahlen",
|
||||
"gs.sun_ray_origin.inner": "Von den inneren Fensterecken",
|
||||
"gs.sun_ray_origin.outer": "Von den äußeren Fensterecken",
|
||||
@@ -1050,7 +1040,6 @@
|
||||
"canvas.far_objects": "{n} Objekt(e) weit vom Plan entfernt",
|
||||
"canvas.show_far": "Anzeigen",
|
||||
"canvas.home_tip": "Der Plan ist dort drüben — klicken, um ihn anzupassen",
|
||||
"gs.grid_group": "Planwartung",
|
||||
"gs.align_all": "Pläne optimieren",
|
||||
"gs.align_title": "Pläne optimieren",
|
||||
"gs.align_none": "Alle Pläne verwenden bereits das aktuelle optimierte Datenmodell.",
|
||||
@@ -1109,7 +1098,6 @@
|
||||
"gs.align_warn": "Absichtlich zwischen Rasterknoten platzierte Elemente werden sich bewegen. Nach der Operation ist nur eine Rückgängig-Funktion verfügbar, und zwar nur bis zur nächsten Planbearbeitung.",
|
||||
"gs.align_run": "Optimieren",
|
||||
"gs.align_done": "Pläne optimiert: {n} Elemente bewegt, {m} Datensätze beibehalten, {r} Verweise repariert",
|
||||
"gs.optimize_undo": "Letzte Optimierung rückgängig machen",
|
||||
"gs.optimize_undone": "Die letzte Optimierung wurde rückgängig gemacht",
|
||||
"decor.furniture": "Möbel",
|
||||
"decor.image": "Bild",
|
||||
@@ -1273,8 +1261,6 @@
|
||||
"vac.residual_message": "Die zugeordneten Räume weichen um bis zu {error} voneinander ab. Diese ungefähre Kalibrierung anwenden, manuell verfeinern oder ohne Änderung der gespeicherten Einrichtung abbrechen.",
|
||||
"vac.apply_proposal": "Anwenden",
|
||||
"gs.backup_group": "Sichern und übertragen",
|
||||
"backup.export_open": "Exportieren",
|
||||
"backup.import_open": "Importieren",
|
||||
"backup.export_title": "House Plan exportieren",
|
||||
"backup.import_title": "House Plan importieren",
|
||||
"backup.export_hint": "Wählen Sie, ob das Backup die gesamte House Plan-Konfiguration oder nur den aktuellen Bereich enthalten soll.",
|
||||
@@ -1391,7 +1377,6 @@
|
||||
"backup.add": "Bereich hinzufügen",
|
||||
"backup.space_done": "Bereich importiert: {rooms} Räume, {markers} Geräte, {refs} bestehende Referenzen wiederhergestellt",
|
||||
"backup.full_done": "Backup wiederhergestellt: {spaces} Bereiche, {rooms} Räume, {markers} Geräte",
|
||||
"backup.undo_import": "Letzten vollständigen Import rückgängig machen",
|
||||
"backup.import_undone": "Vollständiger Import rückgängig gemacht",
|
||||
"backdrop.large_title": "Großes Bild",
|
||||
"backdrop.large_body": "Dieses Bild ist {w}×{h} ({fileMb} MB Datei) und benötigt etwa {decodedMb} MB Speicher zur Anzeige. Auf Tablets kann das die Seite zum Absturz bringen.",
|
||||
|
||||
+1
-16
@@ -481,20 +481,15 @@
|
||||
"pdf.asset_failed": "A backdrop or decor image could not be loaded.",
|
||||
"pdf.scale": "Scale 1:{n}",
|
||||
"pdf.north": "North",
|
||||
"gs.title": "General settings",
|
||||
"gs.light_group": "Lights",
|
||||
"gs.light_on": "Lights on",
|
||||
"gs.light_off": "All lights off",
|
||||
"gs.temp_group": "Temperature",
|
||||
"gs.temp_cold": "Cold",
|
||||
"gs.temp_ok": "Comfortable",
|
||||
"gs.temp_hot": "Hot",
|
||||
"gs.lqi_group": "Zigbee signal",
|
||||
"gs.lqi_low": "Weak signal",
|
||||
"gs.lqi_high": "Strong signal",
|
||||
"gs.reset": "Reset to defaults",
|
||||
"gs.saved": "General settings saved",
|
||||
"gs.radar_show_live": "Show live presence on the plan",
|
||||
"gs.volumetric_view": "Show the plan in 2.5D",
|
||||
"radar.title": "Presence on the plan",
|
||||
"radar.help": "Connect this marker to exact Home Assistant radar entities, place the sensor and choose its coordinate profile. Live positions are clipped to the selected room.",
|
||||
@@ -730,8 +725,6 @@
|
||||
"gs.glow_group": "Light-source glow",
|
||||
"gs.glow_base": "House darkness",
|
||||
"gs.glow_light": "Default light color / intensity",
|
||||
"gs.wall_fill": "Wall fill",
|
||||
"gs.glow_radius": "Glow radius",
|
||||
"gs.glow_radius.help": "Sets the default glow radius in metres or feet; a device-specific radius overrides it.",
|
||||
"gs.glow_radius.help.aria": "Help: default glow radius",
|
||||
"gs.unit_m": "m",
|
||||
@@ -1014,20 +1007,17 @@
|
||||
"vac.trail_always": "Always",
|
||||
"gs.bg_color": "Background around the plan",
|
||||
"gs.bg_default": "Theme default",
|
||||
"gs.bg_mode": "Plan background",
|
||||
"gs.bg_mode.help": "“Follows the Sun” uses sun.sun, falling back to the browser's local clock; Static always shows the selected colour.",
|
||||
"gs.bg_mode.help.aria": "Help: general plan background",
|
||||
"gs.bg_static": "Static color",
|
||||
"gs.bg_daynight": "Follows the Sun",
|
||||
"gs.sun_group": "Sun",
|
||||
"gs.sun_group": "Sun and Moon",
|
||||
"gs.sun_missing": "The sun.sun entity was not found — the background follows your local clock; window rays are unavailable.",
|
||||
"gs.north": "North on the plan",
|
||||
"gs.north.help": "The angle is measured clockwise from the plan's upward vertical; north is required for window rays, not for the “Follows the Sun” background.",
|
||||
"gs.north.help.aria": "Help: general north direction",
|
||||
"gs.north_ph": "not set",
|
||||
"gs.north_clear": "Clear",
|
||||
"gs.north_letter": "N",
|
||||
"gs.sun_rays": "Sunlight through windows",
|
||||
"gs.sun_ray_origin": "Sun rays",
|
||||
"gs.sun_ray_origin.inner": "From the inner window corners",
|
||||
"gs.sun_ray_origin.outer": "From the outer window corners",
|
||||
@@ -1050,7 +1040,6 @@
|
||||
"canvas.far_objects": "{n} object(s) far from the plan",
|
||||
"canvas.show_far": "Show",
|
||||
"canvas.home_tip": "The plan is over there — click to fit it",
|
||||
"gs.grid_group": "Plan maintenance",
|
||||
"gs.align_all": "Optimize plans",
|
||||
"gs.align_title": "Optimize plans",
|
||||
"gs.align_none": "All plans already use the current optimized data model.",
|
||||
@@ -1109,7 +1098,6 @@
|
||||
"gs.align_warn": "Elements deliberately placed between grid nodes will move. One undo is available after the operation, only until the next plan edit.",
|
||||
"gs.align_run": "Optimize",
|
||||
"gs.align_done": "Plans optimized: {n} elements moved, {m} records maintained, {r} references repaired",
|
||||
"gs.optimize_undo": "Undo last optimization",
|
||||
"gs.optimize_undone": "The last optimization was undone",
|
||||
"decor.furniture": "Furniture",
|
||||
"decor.image": "Image",
|
||||
@@ -1273,8 +1261,6 @@
|
||||
"vac.residual_message": "The matched rooms disagree by up to {error}. Apply this approximate calibration, refine it manually, or cancel without changing the saved setup.",
|
||||
"vac.apply_proposal": "Apply",
|
||||
"gs.backup_group": "Backup and transfer",
|
||||
"backup.export_open": "Export",
|
||||
"backup.import_open": "Import",
|
||||
"backup.export_title": "Export House Plan",
|
||||
"backup.import_title": "Import House Plan",
|
||||
"backup.export_hint": "Choose whether the backup should contain the whole House Plan configuration or only the current space.",
|
||||
@@ -1391,7 +1377,6 @@
|
||||
"backup.add": "Add space",
|
||||
"backup.space_done": "Space imported: {rooms} rooms, {markers} devices, {refs} existing references restored",
|
||||
"backup.full_done": "Backup restored: {spaces} spaces, {rooms} rooms, {markers} devices",
|
||||
"backup.undo_import": "Undo last full import",
|
||||
"backup.import_undone": "Full import undone",
|
||||
"backdrop.large_title": "Large image",
|
||||
"backdrop.large_body": "This image is {w}×{h} ({fileMb} MB file) and needs about {decodedMb} MB of memory to display. On tablets this can crash the page.",
|
||||
|
||||
+1
-16
@@ -481,20 +481,15 @@
|
||||
"pdf.asset_failed": "Impossible de charger une image de fond ou de décoration.",
|
||||
"pdf.scale": "Échelle 1:{n}",
|
||||
"pdf.north": "Nord",
|
||||
"gs.title": "Paramètres généraux",
|
||||
"gs.light_group": "Éclairage",
|
||||
"gs.light_on": "Lumières allumées",
|
||||
"gs.light_off": "Toutes les lumières éteintes",
|
||||
"gs.temp_group": "Température",
|
||||
"gs.temp_cold": "Froid",
|
||||
"gs.temp_ok": "Confortable",
|
||||
"gs.temp_hot": "Chaud",
|
||||
"gs.lqi_group": "Signal Zigbee",
|
||||
"gs.lqi_low": "Signal faible",
|
||||
"gs.lqi_high": "Signal fort",
|
||||
"gs.reset": "Réinitialiser par défaut",
|
||||
"gs.saved": "Paramètres généraux enregistrés",
|
||||
"gs.radar_show_live": "Afficher la présence en direct sur le plan",
|
||||
"gs.volumetric_view": "Afficher le plan en 2.5D",
|
||||
"radar.title": "Présence sur le plan",
|
||||
"radar.help": "Reliez ce marqueur aux entités radar exactes de Home Assistant, placez le capteur et choisissez son profil de coordonnées. Les positions en direct sont limitées à la pièce sélectionnée.",
|
||||
@@ -730,8 +725,6 @@
|
||||
"gs.glow_group": "Halo des sources lumineuses",
|
||||
"gs.glow_base": "Obscurité de la maison",
|
||||
"gs.glow_light": "Couleur / intensité de lumière par défaut",
|
||||
"gs.wall_fill": "Remplissage des murs",
|
||||
"gs.glow_radius": "Rayon du halo",
|
||||
"gs.glow_radius.help": "Définit le rayon général du halo en mètres ou en pieds ; le rayon propre à un appareil remplace cette valeur.",
|
||||
"gs.glow_radius.help.aria": "Aide : rayon général du halo",
|
||||
"gs.unit_m": "m",
|
||||
@@ -1014,20 +1007,17 @@
|
||||
"vac.trail_always": "Toujours",
|
||||
"gs.bg_color": "Arrière-plan autour du plan",
|
||||
"gs.bg_default": "Valeur par défaut du thème",
|
||||
"gs.bg_mode": "Arrière-plan du plan",
|
||||
"gs.bg_mode.help": "« Suit le soleil » utilise sun.sun ou, à défaut, l’heure locale du navigateur ; « Couleur statique » affiche toujours la couleur choisie.",
|
||||
"gs.bg_mode.help.aria": "Aide : arrière-plan général du plan",
|
||||
"gs.bg_static": "Couleur statique",
|
||||
"gs.bg_daynight": "Suit le soleil",
|
||||
"gs.sun_group": "Soleil",
|
||||
"gs.sun_group": "Soleil et Lune",
|
||||
"gs.sun_missing": "L’entité sun.sun est introuvable — l’arrière-plan suit votre heure locale ; les rayons de soleil par les fenêtres sont indisponibles.",
|
||||
"gs.north": "Nord sur le plan",
|
||||
"gs.north.help": "L’angle se mesure dans le sens horaire depuis la verticale supérieure du plan ; le nord sert aux rayons des fenêtres, pas à l’arrière-plan « Suit le soleil ».",
|
||||
"gs.north.help.aria": "Aide : direction générale du nord",
|
||||
"gs.north_ph": "non défini",
|
||||
"gs.north_clear": "Effacer",
|
||||
"gs.north_letter": "N",
|
||||
"gs.sun_rays": "Lumière du soleil à travers les fenêtres",
|
||||
"gs.sun_ray_origin": "Rayons du soleil",
|
||||
"gs.sun_ray_origin.inner": "Depuis les angles intérieurs des fenêtres",
|
||||
"gs.sun_ray_origin.outer": "Depuis les angles extérieurs des fenêtres",
|
||||
@@ -1050,7 +1040,6 @@
|
||||
"canvas.far_objects": "{n} objet(s) éloigné(s) du plan",
|
||||
"canvas.show_far": "Afficher",
|
||||
"canvas.home_tip": "Le plan est là-bas — cliquez pour l’ajuster à l’écran",
|
||||
"gs.grid_group": "Maintenance du plan",
|
||||
"gs.align_all": "Optimiser les plans",
|
||||
"gs.align_title": "Optimiser les plans",
|
||||
"gs.align_none": "Tous les plans utilisent déjà le modèle de données optimisé actuel.",
|
||||
@@ -1109,7 +1098,6 @@
|
||||
"gs.align_warn": "Les éléments volontairement placés entre les nœuds de la grille seront déplacés. Une seule annulation est disponible après l’opération, uniquement jusqu’à la prochaine modification du plan.",
|
||||
"gs.align_run": "Optimiser",
|
||||
"gs.align_done": "Plans optimisés : {n} élément(s) déplacé(s), {m} enregistrement(s) maintenu(s), {r} référence(s) réparée(s)",
|
||||
"gs.optimize_undo": "Annuler la dernière optimisation",
|
||||
"gs.optimize_undone": "La dernière optimisation a été annulée",
|
||||
"decor.furniture": "Mobilier",
|
||||
"decor.image": "Image",
|
||||
@@ -1273,8 +1261,6 @@
|
||||
"vac.residual_message": "Les pièces correspondantes présentent un écart maximal de {error}. Appliquez cette calibration approximative, affinez-la manuellement ou annulez sans modifier la configuration enregistrée.",
|
||||
"vac.apply_proposal": "Appliquer",
|
||||
"gs.backup_group": "Sauvegarde et transfert",
|
||||
"backup.export_open": "Exporter",
|
||||
"backup.import_open": "Importer",
|
||||
"backup.export_title": "Exporter House Plan",
|
||||
"backup.import_title": "Importer House Plan",
|
||||
"backup.export_hint": "Choisissez si la sauvegarde doit contenir toute la configuration House Plan ou uniquement l’espace actuel.",
|
||||
@@ -1391,7 +1377,6 @@
|
||||
"backup.add": "Ajouter un espace",
|
||||
"backup.space_done": "Espace importé : {rooms} pièces, {markers} appareils, {refs} références existantes restaurées",
|
||||
"backup.full_done": "Sauvegarde restaurée : {spaces} espaces, {rooms} pièces, {markers} appareils",
|
||||
"backup.undo_import": "Annuler la dernière importation complète",
|
||||
"backup.import_undone": "Importation complète annulée",
|
||||
"backdrop.large_title": "Grande image",
|
||||
"backdrop.large_body": "Cette image fait {w}×{h} (fichier de {fileMb} Mo) et nécessite environ {decodedMb} Mo de mémoire pour s'afficher. Sur tablette, cela peut faire planter la page.",
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
type LazyLanguageModule,
|
||||
type LocaleDictionary,
|
||||
} from './language-runtime';
|
||||
import { ENTRY_BUILD_FINGERPRINT as BUILD_FINGERPRINT } from '../editor-runtime-loader';
|
||||
|
||||
const BUILD_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
|
||||
const GERMAN_RETRY_ASSET = '__HOUSEPLAN_DE_RETRY_ASSET__';
|
||||
const FRENCH_RETRY_ASSET = '__HOUSEPLAN_FR_RETRY_ASSET__';
|
||||
|
||||
|
||||
+1
-16
@@ -481,20 +481,15 @@
|
||||
"pdf.asset_failed": "Не удалось загрузить подложку или изображение декора.",
|
||||
"pdf.scale": "Масштаб 1:{n}",
|
||||
"pdf.north": "Север",
|
||||
"gs.title": "Общие настройки",
|
||||
"gs.light_group": "Освещение",
|
||||
"gs.light_on": "Свет включён",
|
||||
"gs.light_off": "Весь свет выключен",
|
||||
"gs.temp_group": "Температура",
|
||||
"gs.temp_cold": "Холодно",
|
||||
"gs.temp_ok": "Комфорт",
|
||||
"gs.temp_hot": "Жарко",
|
||||
"gs.lqi_group": "Сигнал Zigbee",
|
||||
"gs.lqi_low": "Слабый сигнал",
|
||||
"gs.lqi_high": "Сильный сигнал",
|
||||
"gs.reset": "Сбросить к умолчаниям",
|
||||
"gs.saved": "Общие настройки сохранены",
|
||||
"gs.radar_show_live": "Показывать текущее присутствие на плане",
|
||||
"gs.volumetric_view": "Объёмный вид плана (2.5D)",
|
||||
"radar.title": "Присутствие на плане",
|
||||
"radar.help": "Свяжите маркер с точными сущностями радара Home Assistant, укажите положение датчика и профиль координат. Живые позиции обрезаются по выбранной комнате.",
|
||||
@@ -730,8 +725,6 @@
|
||||
"gs.glow_group": "Свечение источников света",
|
||||
"gs.glow_base": "Темнота дома",
|
||||
"gs.glow_light": "Цвет света по умолчанию / интенсивность",
|
||||
"gs.wall_fill": "Цвет заливки стен",
|
||||
"gs.glow_radius": "Радиус свечения",
|
||||
"gs.glow_radius.help": "Задаёт общий радиус светового пятна в метрах или футах; персональный радиус устройства заменяет это значение.",
|
||||
"gs.glow_radius.help.aria": "Подсказка: общий радиус свечения",
|
||||
"gs.unit_m": "м",
|
||||
@@ -1014,20 +1007,17 @@
|
||||
"vac.trail_always": "Показывать всегда",
|
||||
"gs.bg_color": "Цвет фона вокруг плана",
|
||||
"gs.bg_default": "Как в теме",
|
||||
"gs.bg_mode": "Фон плана",
|
||||
"gs.bg_mode.help": "«Следует за Солнцем» использует sun.sun, а при недоступности — локальные часы браузера; статичный режим всегда показывает выбранный цвет.",
|
||||
"gs.bg_mode.help.aria": "Подсказка: общий фон плана",
|
||||
"gs.bg_static": "Статичный цвет",
|
||||
"gs.bg_daynight": "Следует за Солнцем",
|
||||
"gs.sun_group": "Солнце",
|
||||
"gs.sun_group": "Солнце и Луна",
|
||||
"gs.sun_missing": "Сущность sun.sun не найдена — фон следует локальным часам, а оконные лучи недоступны.",
|
||||
"gs.north": "Север на плане",
|
||||
"gs.north.help": "Угол отсчитывается по часовой стрелке от верхней вертикали плана; север нужен для оконных лучей, а не для фона «Следует за Солнцем».",
|
||||
"gs.north.help.aria": "Подсказка: общее направление севера",
|
||||
"gs.north_ph": "не задан",
|
||||
"gs.north_clear": "Сбросить",
|
||||
"gs.north_letter": "С",
|
||||
"gs.sun_rays": "Солнце в окнах",
|
||||
"gs.sun_ray_origin": "Лучи солнца",
|
||||
"gs.sun_ray_origin.inner": "От внутренних углов окон",
|
||||
"gs.sun_ray_origin.outer": "От внешних углов окон",
|
||||
@@ -1050,7 +1040,6 @@
|
||||
"canvas.far_objects": "Объектов далеко от плана: {n}",
|
||||
"canvas.show_far": "Показать",
|
||||
"canvas.home_tip": "План там — нажмите, чтобы вписать",
|
||||
"gs.grid_group": "Обслуживание планов",
|
||||
"gs.align_all": "Оптимизировать планы",
|
||||
"gs.align_title": "Оптимизировать планы",
|
||||
"gs.align_none": "Все планы уже используют актуальную и оптимальную модель данных.",
|
||||
@@ -1109,7 +1098,6 @@
|
||||
"gs.align_warn": "Элементы, намеренно поставленные между узлами, будут сдвинуты. После операции доступна одна отмена — только до следующего изменения плана.",
|
||||
"gs.align_run": "Оптимизировать",
|
||||
"gs.align_done": "Планы оптимизированы: сдвинуто элементов — {n}, обслужено записей — {m}, исправлено ссылок — {r}",
|
||||
"gs.optimize_undo": "Отменить последнюю оптимизацию",
|
||||
"gs.optimize_undone": "Последняя оптимизация отменена",
|
||||
"decor.furniture": "Мебель",
|
||||
"decor.image": "Изображение",
|
||||
@@ -1273,8 +1261,6 @@
|
||||
"vac.residual_message": "Совпавшие комнаты расходятся максимум на {error}. Примените приблизительную калибровку, подгоните её вручную либо отмените без изменения сохранённых настроек.",
|
||||
"vac.apply_proposal": "Применить",
|
||||
"gs.backup_group": "Резервная копия и перенос",
|
||||
"backup.export_open": "Экспорт",
|
||||
"backup.import_open": "Импорт",
|
||||
"backup.export_title": "Экспорт House Plan",
|
||||
"backup.import_title": "Импорт House Plan",
|
||||
"backup.export_hint": "Выберите, должна ли копия содержать всю конфигурацию House Plan или только текущее пространство.",
|
||||
@@ -1391,7 +1377,6 @@
|
||||
"backup.add": "Добавить пространство",
|
||||
"backup.space_done": "Пространство импортировано: комнат — {rooms}, устройств — {markers}, существующих ссылок восстановлено — {refs}",
|
||||
"backup.full_done": "Копия восстановлена: пространств — {spaces}, комнат — {rooms}, устройств — {markers}",
|
||||
"backup.undo_import": "Отменить последний полный импорт",
|
||||
"backup.import_undone": "Полный импорт отменён",
|
||||
"backdrop.large_title": "Большое изображение",
|
||||
"backdrop.large_body": "Изображение {w}×{h} ({fileMb} МБ), для показа потребуется около {decodedMb} МБ памяти. На планшетах это может привести к падению страницы.",
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"gs.bg_custom_hint": "Eigene Hintergrundfarbe.",
|
||||
"gs.bg_theme_hint": "Hintergrund des Themes wird verwendet.",
|
||||
"gs.sun_rays_hint": "Sonnenstrahlen durch Fensteröffnungen zeichnen.",
|
||||
"gs.moon": "Mond über dem Plan in Dämmerung und Nacht",
|
||||
"gs.moon_hint": "Erscheint im Hintergrund hinter dem Plan ab 3° über dem Horizont, außer um Neumond; berechnet aus den Koordinaten des Zuhauses in Home Assistant.",
|
||||
"gs.error_glow_radius": "Geben Sie einen Radius größer als null ein.",
|
||||
"gs.error_north": "Geben Sie eine ganze Zahl zwischen 0° und 359° ein.",
|
||||
"gs.opacity_of": "Deckkraft: {name}",
|
||||
@@ -91,5 +93,20 @@
|
||||
"fill.seg_lqi": "Zigbee",
|
||||
"fill.seg_light": "Licht",
|
||||
"fill.seg_temp": "Temperatur",
|
||||
"fill.seg_custom": "Eigene"
|
||||
"fill.seg_custom": "Eigene",
|
||||
"backup.export_open": "Exportieren",
|
||||
"backup.import_open": "Importieren",
|
||||
"backup.undo_import": "Letzten vollständigen Import rückgängig machen",
|
||||
"gs.bg_mode": "Planhintergrund",
|
||||
"gs.glow_radius": "Glow-Radius",
|
||||
"gs.grid_group": "Planwartung",
|
||||
"gs.light_group": "Beleuchtung",
|
||||
"gs.lqi_group": "Zigbee-Signal",
|
||||
"gs.north_ph": "nicht gesetzt",
|
||||
"gs.optimize_undo": "Letzte Optimierung rückgängig machen",
|
||||
"gs.radar_show_live": "Live-Präsenz auf dem Plan anzeigen",
|
||||
"gs.sun_rays": "Sonnenlicht durch Fenster",
|
||||
"gs.temp_group": "Temperatur",
|
||||
"gs.title": "Allgemeine Einstellungen",
|
||||
"gs.wall_fill": "Wandfüllung"
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"gs.bg_custom_hint": "Custom background color.",
|
||||
"gs.bg_theme_hint": "Using the theme background.",
|
||||
"gs.sun_rays_hint": "Draw sun rays through window openings.",
|
||||
"gs.moon": "Moon over the plan at dusk and night",
|
||||
"gs.moon_hint": "Shown on the background behind the plan from 3° above the horizon, except around new moon; computed from the home coordinates in Home Assistant.",
|
||||
"gs.error_glow_radius": "Enter a radius greater than zero.",
|
||||
"gs.error_north": "Enter a whole number between 0° and 359°.",
|
||||
"gs.opacity_of": "{name} opacity",
|
||||
@@ -91,5 +93,20 @@
|
||||
"fill.seg_lqi": "Zigbee",
|
||||
"fill.seg_light": "Lights",
|
||||
"fill.seg_temp": "Temperature",
|
||||
"fill.seg_custom": "Custom"
|
||||
"fill.seg_custom": "Custom",
|
||||
"backup.export_open": "Export",
|
||||
"backup.import_open": "Import",
|
||||
"backup.undo_import": "Undo last full import",
|
||||
"gs.bg_mode": "Plan background",
|
||||
"gs.glow_radius": "Glow radius",
|
||||
"gs.grid_group": "Plan maintenance",
|
||||
"gs.light_group": "Lights",
|
||||
"gs.lqi_group": "Zigbee signal",
|
||||
"gs.north_ph": "not set",
|
||||
"gs.optimize_undo": "Undo last optimization",
|
||||
"gs.radar_show_live": "Show live presence on the plan",
|
||||
"gs.sun_rays": "Sunlight through windows",
|
||||
"gs.temp_group": "Temperature",
|
||||
"gs.title": "General settings",
|
||||
"gs.wall_fill": "Wall fill"
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"gs.bg_custom_hint": "Couleur de fond personnalisée.",
|
||||
"gs.bg_theme_hint": "Fond du thème utilisé.",
|
||||
"gs.sun_rays_hint": "Tracer les rayons du soleil à travers les ouvertures des fenêtres.",
|
||||
"gs.moon": "Lune sur le plan au crépuscule et la nuit",
|
||||
"gs.moon_hint": "Affichée sur le fond derrière le plan à partir de 3° au-dessus de l’horizon, sauf autour de la nouvelle lune ; calculée à partir des coordonnées du domicile dans Home Assistant.",
|
||||
"gs.error_glow_radius": "Saisissez un rayon supérieur à zéro.",
|
||||
"gs.error_north": "Saisissez un nombre entier entre 0° et 359°.",
|
||||
"gs.opacity_of": "Opacité : {name}",
|
||||
@@ -91,5 +93,20 @@
|
||||
"fill.seg_lqi": "Zigbee",
|
||||
"fill.seg_light": "Lumières",
|
||||
"fill.seg_temp": "Température",
|
||||
"fill.seg_custom": "Perso"
|
||||
"fill.seg_custom": "Perso",
|
||||
"backup.export_open": "Exporter",
|
||||
"backup.import_open": "Importer",
|
||||
"backup.undo_import": "Annuler la dernière importation complète",
|
||||
"gs.bg_mode": "Arrière-plan du plan",
|
||||
"gs.glow_radius": "Rayon du halo",
|
||||
"gs.grid_group": "Maintenance du plan",
|
||||
"gs.light_group": "Éclairage",
|
||||
"gs.lqi_group": "Signal Zigbee",
|
||||
"gs.north_ph": "non défini",
|
||||
"gs.optimize_undo": "Annuler la dernière optimisation",
|
||||
"gs.radar_show_live": "Afficher la présence en direct sur le plan",
|
||||
"gs.sun_rays": "Lumière du soleil à travers les fenêtres",
|
||||
"gs.temp_group": "Température",
|
||||
"gs.title": "Paramètres généraux",
|
||||
"gs.wall_fill": "Remplissage des murs"
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"gs.bg_custom_hint": "Свой цвет фона.",
|
||||
"gs.bg_theme_hint": "Используется фон темы.",
|
||||
"gs.sun_rays_hint": "Рисовать солнечные лучи через оконные проёмы.",
|
||||
"gs.moon": "Луна на плане в сумерках и ночью",
|
||||
"gs.moon_hint": "Видна на фоне за планом при высоте от 3° над горизонтом, кроме новолуния; положение считается по координатам дома из Home Assistant.",
|
||||
"gs.error_glow_radius": "Введите радиус больше нуля.",
|
||||
"gs.error_north": "Введите целое число от 0° до 359°.",
|
||||
"gs.opacity_of": "Прозрачность: {name}",
|
||||
@@ -91,5 +93,20 @@
|
||||
"fill.seg_lqi": "Zigbee",
|
||||
"fill.seg_light": "Свет",
|
||||
"fill.seg_temp": "Температура",
|
||||
"fill.seg_custom": "Свой цвет"
|
||||
"fill.seg_custom": "Свой цвет",
|
||||
"backup.export_open": "Экспорт",
|
||||
"backup.import_open": "Импорт",
|
||||
"backup.undo_import": "Отменить последний полный импорт",
|
||||
"gs.bg_mode": "Фон плана",
|
||||
"gs.glow_radius": "Радиус свечения",
|
||||
"gs.grid_group": "Обслуживание планов",
|
||||
"gs.light_group": "Освещение",
|
||||
"gs.lqi_group": "Сигнал Zigbee",
|
||||
"gs.north_ph": "не задан",
|
||||
"gs.optimize_undo": "Отменить последнюю оптимизацию",
|
||||
"gs.radar_show_live": "Показывать текущее присутствие на плане",
|
||||
"gs.sun_rays": "Солнце в окнах",
|
||||
"gs.temp_group": "Температура",
|
||||
"gs.title": "Общие настройки",
|
||||
"gs.wall_fill": "Цвет заливки стен"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* The moon's only foothold in the initial View graph (#661, C7): whether the
|
||||
* lazy `moon-runtime` chunk is wanted, and one load per page for every card on
|
||||
* it. It is wanted when the moon is switched on, the environment exists (the
|
||||
* effective background follows the sun, on a View surface) and it is not
|
||||
* daytime. Everything else — astronomy, art, element, its 30 s ticker — lives
|
||||
* in the chunk.
|
||||
*
|
||||
* The contract of the other lazy runtimes, in the fewest bytes (the initial
|
||||
* graph sits at its budget): a chunk from another build is never installed and
|
||||
* never asked for again; a failed load leaves no moon and is retried at most
|
||||
* every 30 s, on the next render, from the content-hashed URL with a fresh
|
||||
* query so the browser's cached failure is not replayed.
|
||||
*/
|
||||
import { nothing, type TemplateResult } from 'lit';
|
||||
import { ENTRY_BUILD_FINGERPRINT } from './editor-runtime-loader';
|
||||
import type { DayCycleState } from './sun';
|
||||
|
||||
type MoonRuntime = typeof import('./moon-runtime');
|
||||
export type MoonHost = import('./moon-runtime').MoonHost;
|
||||
|
||||
const MOON_RETRY_ASSET = '__HOUSEPLAN_MOON_RETRY_ASSET__';
|
||||
let runtime: MoonRuntime | null = null;
|
||||
/** Earliest next attempt; 0 before the first, Infinity after a foreign build. */
|
||||
let nextAttempt = 0;
|
||||
|
||||
/**
|
||||
* The moon element for `renderDayCycleEnvironment`. `state` is the
|
||||
* environment (null: no environment, so no moon); `settings` the global
|
||||
* settings, where only an explicit `moon: true` counts (C1). Nothing until the
|
||||
* chunk is here; the card that asked re-renders when it arrives.
|
||||
*/
|
||||
export function moonLayer(
|
||||
host: MoonHost | undefined, settings: unknown, state: DayCycleState | null,
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!host || !state || (settings as { moon?: unknown } | null | undefined)?.moon !== true) return nothing;
|
||||
if (!runtime && state.phase !== 'day' && Date.now() >= nextAttempt) {
|
||||
const retry = nextAttempt > 0;
|
||||
nextAttempt = Date.now() + 30_000;
|
||||
(retry
|
||||
? import(/* @vite-ignore */ new URL(`${MOON_RETRY_ASSET}?${nextAttempt}`, import.meta.url).href) as Promise<MoonRuntime>
|
||||
: import('./moon-runtime')
|
||||
).then((module) => {
|
||||
if (module.MOON_RUNTIME_FINGERPRINT !== ENTRY_BUILD_FINGERPRINT) { nextAttempt = Infinity; return; }
|
||||
runtime = module;
|
||||
host.requestUpdate();
|
||||
}, () => undefined);
|
||||
}
|
||||
return runtime?.renderMoon(host, settings, state.phase) ?? nothing;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* The lazy moon chunk (#661, C7): astronomy, the designer's art, the element
|
||||
* template and the moon's own 30 s ticker. `moon-gate.ts` imports it only
|
||||
* while the moon is switched on, the background follows the sun and it is not
|
||||
* daytime; until then the initial View graph carries none of this.
|
||||
*
|
||||
* One element inside `.hp-day-cycle-env`, after the four phase layers: above
|
||||
* the gradients and the sun glow, below the plan paper and everything else
|
||||
* (owner decision 7). Its styles travel with it, so the View card, kiosk and
|
||||
* `houseplan-space-card` get the same moon without growing their own CSS.
|
||||
*/
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { MOON_ART } from './moon-art.generated';
|
||||
import {
|
||||
MOON_SHARP_FROM, moonFingerprint, moonPhasePath, moonView, type MoonView,
|
||||
} from './moon';
|
||||
import type { DayCyclePhase } from './sun';
|
||||
|
||||
export const MOON_RUNTIME_FINGERPRINT = '__HOUSEPLAN_SOURCE_FINGERPRINT__';
|
||||
|
||||
/** The card that shows the moon: the View card or `houseplan-space-card`. */
|
||||
export interface MoonHost {
|
||||
readonly isConnected: boolean;
|
||||
readonly updateComplete: Promise<unknown>;
|
||||
readonly hass?: { config?: unknown };
|
||||
requestUpdate(): void;
|
||||
}
|
||||
|
||||
/** C3: the environment ticker period; the moon rises at most 0.25°/min. */
|
||||
export const MOON_TICK_MS = 30_000;
|
||||
|
||||
/**
|
||||
* C6: box `min(200px, 25cqmin)` measured on the scene (the environment is the
|
||||
* size container), 5 % inset, never moved by pan or zoom, never hit-tested.
|
||||
* C4: opacity only, 2 s on the background curve; reduced motion — instant.
|
||||
* C8: no CSS filter and no will-change on the element.
|
||||
*/
|
||||
const MOON_CSS = '.hp-day-cycle-env{container-type:size}'
|
||||
+ '.hp-moon{--hp-moon-box:min(200px,25cqmin);position:absolute;'
|
||||
+ 'left:calc(var(--hp-moon-box)*.05);top:calc(var(--hp-moon-box)*.05);'
|
||||
+ 'width:var(--hp-moon-box);height:var(--hp-moon-box);opacity:0;pointer-events:none;'
|
||||
+ 'transition:opacity 2000ms cubic-bezier(.22,.61,.36,1)}' // RAY_FADE_MS, pinned by the test
|
||||
+ '.hp-moon.on{opacity:1}'
|
||||
+ '@media (prefers-reduced-motion:reduce){.hp-moon{transition:none}}';
|
||||
|
||||
interface MoonWatch {
|
||||
settings: unknown;
|
||||
phase: DayCyclePhase;
|
||||
/** Fingerprint of what is on screen (or was last asked for). */
|
||||
key: string;
|
||||
/** Set by every render that includes the moon; cleared when a tick asks for one. */
|
||||
seen: boolean;
|
||||
timer: ReturnType<typeof setInterval>;
|
||||
}
|
||||
|
||||
const watches = new WeakMap<MoonHost, MoonWatch>();
|
||||
|
||||
function forget(host: MoonHost, watch: MoonWatch): void {
|
||||
clearInterval(watch.timer);
|
||||
if (watches.get(host) === watch) watches.delete(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* C3: between renders only the clock moves the moon. Every 30 s the tick
|
||||
* recomputes it from the last rendered inputs; an equal fingerprint costs no
|
||||
* render. A host that re-rendered without a moon (switched off, editor mode,
|
||||
* static background) or left the page drops its ticker; a hidden page skips.
|
||||
*/
|
||||
export function moonTick(host: MoonHost, now: Date = new Date()): void {
|
||||
const watch = watches.get(host);
|
||||
if (!watch) return;
|
||||
if (!host.isConnected) { forget(host, watch); return; }
|
||||
if (globalThis.document?.visibilityState === 'hidden') return;
|
||||
const key = moonFingerprint(moonView(watch.settings, watch.phase, host.hass?.config, now));
|
||||
if (key === watch.key) return;
|
||||
watch.key = key;
|
||||
watch.seen = false;
|
||||
host.requestUpdate();
|
||||
void host.updateComplete.then(() => { if (!watch.seen) forget(host, watch); });
|
||||
}
|
||||
|
||||
function remember(host: MoonHost, settings: unknown, phase: DayCyclePhase, view: MoonView): void {
|
||||
const key = moonFingerprint(view);
|
||||
const watch = watches.get(host);
|
||||
if (watch) {
|
||||
Object.assign(watch, { settings, phase, key, seen: true });
|
||||
return;
|
||||
}
|
||||
watches.set(host, {
|
||||
settings, phase, key, seen: true,
|
||||
timer: setInterval(() => moonTick(host), MOON_TICK_MS),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The element stays in the environment while the moon is switched on, at
|
||||
* opacity 0 when hidden, so rising, setting, the new-moon threshold and the
|
||||
* day phase all fade (C4). A freshly created element takes its final state at
|
||||
* once: the first appearance after a page or chunk load is not animated.
|
||||
*/
|
||||
export function renderMoon(
|
||||
host: MoonHost, settings: unknown, phase: DayCyclePhase, now: Date = new Date(),
|
||||
): TemplateResult {
|
||||
const view = moonView(settings, phase, host.hass?.config, now);
|
||||
remember(host, settings, phase, view);
|
||||
return html`<style>${MOON_CSS}</style><svg class="hp-moon${view.visible ? ' on' : ''}"
|
||||
viewBox="0 0 512 512" aria-hidden="true" focusable="false"
|
||||
data-moon-k=${view.k.toFixed(2)} data-moon-visible=${view.visible ? 'true' : 'false'}>
|
||||
<defs>
|
||||
<g id="hp-moon-art">${MOON_ART}</g>
|
||||
<filter id="hp-moon-soft" x="-5%" y="-5%" width="110%" height="110%">
|
||||
<feGaussianBlur stdDeviation="5"></feGaussianBlur>
|
||||
</filter>
|
||||
<mask id="hp-moon-phase" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
||||
<path d=${moonPhasePath(view.k)} fill="#fff"
|
||||
filter=${view.k >= MOON_SHARP_FROM ? nothing : 'url(#hp-moon-soft)'}></path>
|
||||
</mask>
|
||||
</defs>
|
||||
<use href="#hp-moon-art" opacity="0.08"></use>
|
||||
<use href="#hp-moon-art" mask="url(#hp-moon-phase)"></use>
|
||||
</svg>`;
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* The moon over the "Follow the Sun" background (#661): pure astronomy,
|
||||
* visibility rules and the phase mask. No DOM, no Lit — the lazy
|
||||
* `moon-runtime` chunk renders from these, unit tests call them directly.
|
||||
*
|
||||
* Home Assistant publishes no moon altitude, so the card computes it from
|
||||
* `hass.config.latitude/longitude` and the browser clock: the short Meeus
|
||||
* series (as in SunCalc) plus the topocentric parallax, without refraction.
|
||||
* Checked against JPL Horizons (airless) on the twelve points of the issue:
|
||||
* altitude within 1.5°, illumination within 2.5 percentage points.
|
||||
*/
|
||||
import type { DayCyclePhase } from './sun';
|
||||
|
||||
/**
|
||||
* C1: the same 3° as the window rays (`RAY_ELEVATION_MIN`), so both lights
|
||||
* obey one horizon. A literal, not an import: every name the lazy chunk takes
|
||||
* from the initial graph is one more export there (test/moon.test.mjs pins it).
|
||||
*/
|
||||
export const MOON_ELEVATION_MIN = 3;
|
||||
/** C1: a thinner crescent reads as a scratch on a wall tablet. */
|
||||
export const MOON_MIN_ILLUMINATION = 0.03;
|
||||
|
||||
const RAD = Math.PI / 180;
|
||||
const DAY_MS = 86_400_000;
|
||||
const J1970 = 2_440_588;
|
||||
const J2000 = 2_451_545;
|
||||
const OBLIQUITY = RAD * 23.4397;
|
||||
const EARTH_RADIUS_KM = 6378.14;
|
||||
const SUN_DISTANCE_KM = 149_598_000;
|
||||
|
||||
const daysSinceJ2000 = (date: Date): number => date.valueOf() / DAY_MS - 0.5 + J1970 - J2000;
|
||||
|
||||
const rightAscension = (l: number, b: number): number =>
|
||||
Math.atan2(Math.sin(l) * Math.cos(OBLIQUITY) - Math.tan(b) * Math.sin(OBLIQUITY), Math.cos(l));
|
||||
|
||||
const declination = (l: number, b: number): number =>
|
||||
Math.asin(Math.sin(b) * Math.cos(OBLIQUITY) + Math.cos(b) * Math.sin(OBLIQUITY) * Math.sin(l));
|
||||
|
||||
interface Equatorial { ra: number; dec: number; dist: number }
|
||||
|
||||
/** Geocentric moon: the short Meeus series, degrees per day as in SunCalc. */
|
||||
function moonCoords(d: number): Equatorial {
|
||||
const L = RAD * (218.316 + 13.176396 * d);
|
||||
const M = RAD * (134.963 + 13.064993 * d);
|
||||
const F = RAD * (93.272 + 13.22935 * d);
|
||||
const l = L + RAD * 6.289 * Math.sin(M);
|
||||
const b = RAD * 5.128 * Math.sin(F);
|
||||
return { ra: rightAscension(l, b), dec: declination(l, b), dist: 385_001 - 20_905 * Math.cos(M) };
|
||||
}
|
||||
|
||||
function sunCoords(d: number): Equatorial {
|
||||
const M = RAD * (357.5291 + 0.98560028 * d);
|
||||
const C = RAD * (1.9148 * Math.sin(M) + 0.02 * Math.sin(2 * M) + 0.0003 * Math.sin(3 * M));
|
||||
const L = M + C + RAD * 102.9372 + Math.PI;
|
||||
return { ra: rightAscension(L, 0), dec: declination(L, 0), dist: SUN_DISTANCE_KM };
|
||||
}
|
||||
|
||||
export interface MoonPosition {
|
||||
/** Topocentric altitude, degrees, airless (no refraction). */
|
||||
altitude: number;
|
||||
/** Degrees clockwise from north. */
|
||||
azimuth: number;
|
||||
}
|
||||
|
||||
/** C2: topocentric altitude/azimuth; the parallax `h − π·cos h` matters by the horizon. */
|
||||
export function moonPosition(date: Date, latitude: number, longitude: number): MoonPosition {
|
||||
const d = daysSinceJ2000(date);
|
||||
const phi = RAD * latitude;
|
||||
const c = moonCoords(d);
|
||||
const H = RAD * (280.16 + 360.9856235 * d) + RAD * longitude - c.ra;
|
||||
const geocentric = Math.asin(Math.sin(phi) * Math.sin(c.dec) + Math.cos(phi) * Math.cos(c.dec) * Math.cos(H));
|
||||
const parallax = Math.asin(EARTH_RADIUS_KM / c.dist);
|
||||
const altitude = geocentric - parallax * Math.cos(geocentric);
|
||||
const south = Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(phi) - Math.tan(c.dec) * Math.cos(phi));
|
||||
return {
|
||||
altitude: altitude / RAD,
|
||||
azimuth: (((south / RAD + 180) % 360) + 360) % 360,
|
||||
};
|
||||
}
|
||||
|
||||
export interface MoonIllumination {
|
||||
/** Illuminated fraction k, 0…1, from the Sun–Moon elongation. */
|
||||
fraction: number;
|
||||
/** Growing towards full; by the sign of the bright limb's position angle. */
|
||||
waxing: boolean;
|
||||
}
|
||||
|
||||
/** C2: `k = (1 + cos i) / 2`; independent of where on Earth the plan is. */
|
||||
export function moonIllumination(date: Date): MoonIllumination {
|
||||
const d = daysSinceJ2000(date);
|
||||
const s = sunCoords(d);
|
||||
const m = moonCoords(d);
|
||||
const elongation = Math.acos(Math.sin(s.dec) * Math.sin(m.dec)
|
||||
+ Math.cos(s.dec) * Math.cos(m.dec) * Math.cos(s.ra - m.ra));
|
||||
const inc = Math.atan2(s.dist * Math.sin(elongation), m.dist - s.dist * Math.cos(elongation));
|
||||
const angle = Math.atan2(Math.cos(s.dec) * Math.sin(s.ra - m.ra),
|
||||
Math.sin(s.dec) * Math.cos(m.dec) - Math.cos(s.dec) * Math.sin(m.dec) * Math.cos(s.ra - m.ra));
|
||||
return { fraction: (1 + Math.cos(inc)) / 2, waxing: angle < 0 };
|
||||
}
|
||||
|
||||
/** C1 thresholds on an already computed sky: the environment phase, 3° and 3 %. */
|
||||
export function moonShownAt(phase: DayCyclePhase, altitude: number, fraction: number): boolean {
|
||||
return phase !== 'day' && altitude >= MOON_ELEVATION_MIN && fraction >= MOON_MIN_ILLUMINATION;
|
||||
}
|
||||
|
||||
export interface MoonView {
|
||||
visible: boolean;
|
||||
/** Illuminated fraction quantised to 0.01: the render changes only with the fingerprint (C3). */
|
||||
k: number;
|
||||
}
|
||||
|
||||
const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
|
||||
|
||||
/**
|
||||
* C1 for an existing environment. The environment itself is the gate for the
|
||||
* rest: it exists only on a View surface whose effective background follows
|
||||
* the sun (`bg_mode` per space, then global). `settings` are the global
|
||||
* settings, `config` is `hass.config`.
|
||||
*/
|
||||
export function moonView(settings: unknown, phase: DayCyclePhase, config: unknown, now: Date): MoonView {
|
||||
const { fraction } = moonIllumination(now);
|
||||
const k = Math.round(fraction * 100) / 100;
|
||||
const { latitude, longitude } = (config ?? {}) as { latitude?: unknown; longitude?: unknown };
|
||||
if ((settings as { moon?: unknown } | null | undefined)?.moon !== true
|
||||
|| !finite(latitude) || !finite(longitude)) return { visible: false, k };
|
||||
return { visible: moonShownAt(phase, moonPosition(now, latitude, longitude).altitude, fraction), k };
|
||||
}
|
||||
|
||||
/** C3: equal fingerprint → no re-render. The lit side is fixed, so k is all the shape. */
|
||||
export function moonFingerprint(view: MoonView): string {
|
||||
return `${view.visible ? 1 : 0}|${view.k.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** Disc of the designer's pack: `viewBox 0 0 512 512`, centre 256, radius 240. */
|
||||
export const MOON_BOX = 512;
|
||||
export const MOON_R = 240;
|
||||
/** Below this the terminator is feathered in the mask; a full disc keeps the art's own edge. */
|
||||
export const MOON_SHARP_FROM = 0.995;
|
||||
|
||||
/**
|
||||
* C5, owner 2026-09-29: the lit side is always the LEFT one, in both
|
||||
* hemispheres; waning runs the waxing states backwards. The lit region is the
|
||||
* left half of the box (its outer arc runs along the box, r 256, never along
|
||||
* the limb, so the art keeps its anti-aliased edge) plus or minus the
|
||||
* terminator half-ellipse `R × R·|2k−1|`: bulging right when k > 0.5.
|
||||
*/
|
||||
export function moonPhasePath(k: number): string {
|
||||
const c = MOON_BOX / 2;
|
||||
const top = c - MOON_R;
|
||||
const bottom = c + MOON_R;
|
||||
const rx = MOON_R * Math.abs(2 * Math.min(1, Math.max(0, k)) - 1);
|
||||
const terminator = rx < 0.5
|
||||
? `L${c} ${top}`
|
||||
: `A${rx.toFixed(2)} ${MOON_R} 0 0 ${k > 0.5 ? 0 : 1} ${c} ${top}`;
|
||||
return `M${c} 0A${c} ${c} 0 0 0 ${c} ${MOON_BOX}L${c} ${bottom}${terminator}L${c} 0Z`;
|
||||
}
|
||||
@@ -892,6 +892,7 @@ class HouseplanSpaceCard extends LitElement {
|
||||
showTemperature: this._config.show_temperature !== false,
|
||||
showSignal: this._config.show_signal !== false,
|
||||
inert: this._continuity.overlayBlocksInteraction,
|
||||
moonHost: this,
|
||||
});
|
||||
if (!stage) {
|
||||
return this._errorCard(t(this._lang, 'space_card.not_found', { id: spaceId }));
|
||||
|
||||
+4
-1
@@ -25,6 +25,7 @@ import {
|
||||
import { DEFAULT_ICON_RULES, compileIconRules } from './rules';
|
||||
import { t, type Lang } from './i18n';
|
||||
import { bgModeOf, resolveDayCycle } from './sun';
|
||||
import { moonLayer, type MoonHost } from './moon-gate';
|
||||
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
|
||||
import type { DevItem, OpeningCfg, ServerConfig } from './types';
|
||||
import { floorMinusBodies, physicalBodyParts, polyclipPathD } from './physical-geometry';
|
||||
@@ -219,6 +220,8 @@ export interface StaticRenderOpts {
|
||||
glowRuntime?: StaticGlowRuntime;
|
||||
/** Deterministic clock injection for unit/smoke fixtures; production omits it. */
|
||||
dayCycleNow?: Date | number;
|
||||
/** #661: the card that shows the moon (re-rendered when its runtime arrives or it moves). */
|
||||
moonHost?: MoonHost;
|
||||
virtualLights?: VirtualLightSnapshot | null;
|
||||
/**
|
||||
* Resolve a stored content url to what the DOM may actually request — the
|
||||
@@ -927,7 +930,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
<div class="hp-static-stage${dayCycle ? ` daycycle phase-${dayCycle.phase}` : ''}"
|
||||
?inert=${!!o.inert}
|
||||
style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''};--hp-cell-visual-scale:${gridVisualScale(cellCm)};--wall-fill:${colors.wall_fill.c};--wall-fill-op:${colors.wall_fill.a}${dayCycle ? `;${dayCycleStageVars(dayCycle)}` : ''}">
|
||||
${renderDayCycleEnvironment(dayCycle)}
|
||||
${renderDayCycleEnvironment(dayCycle, 1, moonLayer(o.moonHost, o.cfg?.settings, dayCycle))}
|
||||
${dayCycle && paperShapes.length ? svg`<svg class="hp-paper-outline-svg"
|
||||
viewBox="${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}"
|
||||
preserveAspectRatio="xMidYMid meet" aria-hidden="true" pointer-events="none">
|
||||
|
||||
@@ -305,6 +305,8 @@ export interface ServerConfig {
|
||||
show_room_tooltip?: boolean;
|
||||
/** #649: the whole installation views plans in 2.5D; absent or false keeps Flat. */
|
||||
volumetric_view?: boolean;
|
||||
/** #661: the moon on the "Follow the Sun" background; only `true` switches it on. */
|
||||
moon?: boolean;
|
||||
north_deg?: number;
|
||||
bg_mode?: 'static' | 'daynight';
|
||||
sun_rays?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user