mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
feat: выбор граней окон для солнечных лучей (#577)
Issue: #577 User-Visible: yes
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- General settings now let window sunlight start at either the inner window
|
||||
corners (the existing default) or the outer window corners. The outer mode
|
||||
carries the visible shaft only through the physical window tunnel before it
|
||||
reaches the room; sun direction, reach, colour and shadows are unchanged
|
||||
([#577](https://github.com/Matysh/houseplan-card/issues/577)).
|
||||
|
||||
- Pinch-zooming in the Home Assistant Companion app no longer repeatedly moves
|
||||
the plan between compositor paths while the fingers are still down. Walls,
|
||||
floors, fills, hatching and lighting now remain continuously painted across
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- В общих настройках теперь можно выбрать, от каких углов окна начинаются
|
||||
солнечные лучи: от внутренних (прежнее поведение по умолчанию) или от
|
||||
внешних. Во внешнем режиме видимая часть луча проходит к комнате только
|
||||
через физический оконный тоннель; направление, длина, цвет и тени не меняются
|
||||
([#577](https://github.com/Matysh/houseplan-card/issues/577)).
|
||||
|
||||
- При масштабировании щипком в приложении Home Assistant план больше не
|
||||
переключается между режимами композитинга, пока пальцы остаются на экране.
|
||||
Стены, пол, заливки, штриховка и свет теперь остаются непрерывно видимыми при
|
||||
|
||||
@@ -97,6 +97,20 @@ new frontend restores the disabled behavior after upgrade. Full backup/import
|
||||
preserves the setting and the privacy-safe support projection includes only a
|
||||
validated boolean.
|
||||
|
||||
## Sun-ray window face (#577)
|
||||
|
||||
`settings.sun_ray_origin` is an optional global enum: `inner` or `outer`.
|
||||
Absence and an unknown read-side value resolve to `inner`, preserving the exact
|
||||
pre-#577 source geometry without rewriting an existing configuration. Saving
|
||||
General settings materialises a valid value; new installations start with
|
||||
`inner`. Backend writes reject every other value. There is deliberately no
|
||||
per-space override and no model/store version bump.
|
||||
|
||||
Full backup/import preserves the enum. The privacy-safe support projection
|
||||
includes only a validated `inner`/`outer` scalar. Older frontends ignore a
|
||||
preserved value and temporarily render the historical inner mode; a current
|
||||
frontend restores the selected mode after upgrade.
|
||||
|
||||
## Summary panel namespace (#437)
|
||||
|
||||
`settings.summary_panel` is an optional shared versioned object. Absence means
|
||||
|
||||
+21
-14
@@ -204,17 +204,24 @@ when BOTH hold:
|
||||
perpendicular depth (`len · cos`, see «Dissolving») would be thinner
|
||||
than the wall it came through.
|
||||
|
||||
The wedge is a PARALLELOGRAM: the window's full **room-side span**, from
|
||||
one inner corner of the opening to the other, extruded by the same
|
||||
The wedge is a PARALLELOGRAM. The global `settings.sun_ray_origin` enum selects
|
||||
its full source span: `inner` starts at the two room-side corners (the default
|
||||
and the exact pre-#577 behaviour), while `outer` starts at the two exterior
|
||||
corners. Missing or unknown values resolve to `inner`; this setting has no
|
||||
per-space override. The selected span is extruded by the same
|
||||
length along the direction AWAY from the sun (light falls inward), so
|
||||
its far edge is parallel to the wall, clipped by the receiving room's
|
||||
**inner contour** when wall thickness is set (`inset` of the polygon by
|
||||
half the wall thickness — see `docs/WALL-THICKNESS.md`); otherwise by
|
||||
the room polygon (`polyclip` intersection). With wall depth `d`, the
|
||||
source span is translated from the wall centreline by `d/2` along the
|
||||
inward normal. Thus both crisp side edges begin exactly at the two inner
|
||||
corners of the opening at every incidence angle (`d = 0` keeps the
|
||||
previous centreline/full-span geometry). Its length
|
||||
the room polygon (`polyclip` intersection). In `outer`, the clip also includes
|
||||
only the physical rectangular window tunnel between the exterior span and the
|
||||
clean-floor contour; the surrounding wall body and outside facade remain
|
||||
occluders. With wall depth `d`, `inner` translates the source from the
|
||||
centreline by `+d/2` along the inward normal and `outer` by `-d/2`. Thus both
|
||||
crisp side edges begin exactly at the selected two corners at every incidence
|
||||
angle (`d = 0` makes both modes identical). The nominal length and gradient
|
||||
start at that selected span rather than adding the wall depth to the old reach.
|
||||
Its length
|
||||
is `k(elevation)` in window lengths: ~1.75 at sunrise/sunset tapering to ~0.56 at the zenith
|
||||
(`0.56 + 1.19·(1 − elevation/90)^1.6` — the v1.56 curve
|
||||
`0.8 + 1.7·(1 − elevation/90)^1.6` times `RAY_LENGTH_K` = 0.7, owner
|
||||
@@ -245,10 +252,10 @@ Two rounds with the owner on the same day:
|
||||
wedge, which feathered the SIDES too. Wrong: a shaft of sunlight
|
||||
through a window has crisp sides. Only its reach fades.
|
||||
|
||||
So the falloff is one-dimensional: **along the ray, from the room-side opening
|
||||
So the falloff is one-dimensional: **along the ray, from the selected opening
|
||||
inward, and nothing else.** Three invariants have to hold at once:
|
||||
|
||||
1. the whole room-side opening is at peak alpha — light does not start out
|
||||
1. the whole selected opening span is at peak alpha — light does not start out
|
||||
half-dark at one end of the window;
|
||||
2. every ray fades over the same distance, its own `len`;
|
||||
3. the wedge's far edge lies exactly on an iso-alpha line, so the shaft
|
||||
@@ -257,21 +264,21 @@ inward, and nothing else.** Three invariants have to hold at once:
|
||||
|
||||
**The axis of the fade is the wall's INWARD NORMAL, not the ray.**
|
||||
The light is a bundle of PARALLEL rays, so the distance a point has
|
||||
travelled from the room-side source span is `depth / cos`, where `depth`
|
||||
travelled from the selected source span is `depth / cos`, where `depth`
|
||||
is its perpendicular distance from that span and `cos = dir·normal` is fixed
|
||||
for the whole wedge. That is an affine function of the point, and its
|
||||
level sets are straight lines PARALLEL TO THE WALL. A linear gradient
|
||||
whose axis is the normal therefore describes the travelled distance
|
||||
exactly:
|
||||
|
||||
- `x1,y1` = the middle of the room-side window span (any point of that span —
|
||||
- `x1,y1` = the middle of the selected window span (any point of that span —
|
||||
they all have depth 0);
|
||||
- `x2,y2` = that point plus `normal · len · cos` — `SunRay.depth`, the
|
||||
perpendicular depth a ray reaches after running the full `len`;
|
||||
- a point `source + dir·u` lands on offset `u / len`, whichever ray it
|
||||
rode in on.
|
||||
|
||||
Hence: the inner opening span is all at offset 0 (invariant 1), the alpha at any
|
||||
Hence: the selected opening span is all at offset 0 (invariant 1), the alpha at any
|
||||
point is a function of how far its own ray has run (invariant 2), and
|
||||
the parallelogram's far edge — parallel to the wall — IS the gradient's
|
||||
last iso-alpha line (invariant 3). The «30 % shorter» reach is then a
|
||||
@@ -319,7 +326,7 @@ of its cheap half, verbatim: «тонкая (1px) чёрная граница п
|
||||
The contract:
|
||||
|
||||
- **Two side edges only.** The rim runs along the two edges that leave
|
||||
the inner opening corners and travel inward with the ray — `a → a+dir·len`
|
||||
the selected opening corners and travel inward with the ray — `a → a+dir·len`
|
||||
and `b → b+dir·len`. Never the source edge `a-b` (that is the source,
|
||||
not a boundary) and never the far edge (there is nothing left to
|
||||
outline there — the fill is already at zero, see below).
|
||||
@@ -331,7 +338,7 @@ The contract:
|
||||
`x1,y1,x2,y2`** (the wall's inward normal, `depth` long) and **the same
|
||||
normalised curve** — `rimStops()` returns `rayStops()` by identity, not
|
||||
by copy, so the two can never drift apart. Only the colour and the peak
|
||||
differ: `RIM_MAX_ALPHA` = 0.42 at the inner opening, tuned on the demo rig
|
||||
differ: `RIM_MAX_ALPHA` = 0.42 at the selected opening, tuned on the demo rig
|
||||
against both extremes (below ~0.3 the line vanishes on paper at kiosk
|
||||
scale, above ~0.5 it reads as an ink contour over the dark glow
|
||||
canvas). Zero from `RAY_FADE_END` = 85 % on, like the fill.
|
||||
|
||||
@@ -2848,6 +2848,21 @@ require hands on real hardware — they remain for the human pass.
|
||||
corners of the window opening (the full span translated inward by half
|
||||
the wall depth), including at an oblique sun angle; no edge starts on the
|
||||
wall centreline [auto: unit `sun.test.mjs` + `smoke_wall_thickness`]
|
||||
- [ ] **Window face (#577):** General settings always shows the global
|
||||
`inner`/`outer` selector, even when rays are off; save/reopen preserves it.
|
||||
Missing/invalid read-side values resolve to `inner`, backend writes reject
|
||||
invalid values, full export/import and support projection preserve only a
|
||||
valid enum [auto: `smoke_sun`, `sun.test.mjs`,
|
||||
`config-schema-parity.test.mjs`, `test_validation.py`,
|
||||
`test_ha_import_export.py`, `test_support_package.py`]
|
||||
- [ ] In `outer`, a thick-wall ray starts at both exterior window corners and
|
||||
reaches the clean floor only through the physical opening tunnel; wall
|
||||
body and exterior space never light up. Direction, nominal reach, fade,
|
||||
colour, thresholds and shadows are identical to `inner`; at `d = 0` the
|
||||
two results are byte-identical. Switching the pending dialog value
|
||||
invalidates the geometry memo without waiting for a server revision
|
||||
[auto: `sun.test.mjs`, `smoke_sun`; golden regression: existing inner
|
||||
sun scenarios]
|
||||
- [ ] Brightness + the 3° threshold (2026-08-03): wedges are visibly brighter
|
||||
(peak alpha 0.30, was 0.18) yet still readable over white paper AND the
|
||||
dark glow canvas; there is NO gradual ramp near the horizon — below 3°
|
||||
|
||||
+10
-5
@@ -908,9 +908,9 @@ YAML-сущность без `unique_id` и строки в Entity Registry: п
|
||||
Флаг «Открывается в другую сторону» меняет только направление створок: у двери
|
||||
и окна зеркалит их относительно оси, а у ворот меняет направление 10° поворота.
|
||||
Положение символа не сдвигается к грани стены. Свет проходит через дверь,
|
||||
ворота или открытый проём по ширине
|
||||
внутреннего тоннеля и отсекается откосами. Солнечный луч начинается из
|
||||
внутренних углов оконного тоннеля.
|
||||
ворота или открытый проём по ширине внутреннего тоннеля и отсекается откосами.
|
||||
Солнечный луч начинается из внутренних или внешних углов оконного тоннеля —
|
||||
режим выбирается в общих настройках, по умолчанию используются внутренние.
|
||||
|
||||
В компактной Static-карточке открытый проём также разрывает тело стены и
|
||||
продолжает текущую заливку/Glow-base через тоннель. Отображение уже существующих
|
||||
@@ -1638,7 +1638,10 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
|
||||
компаса. Стрелка N должна буквально указывать туда, где на рисунке находится
|
||||
истинный север; это не обратная поправка на поворот плана.
|
||||
3. При необходимости включите **Солнечный свет через окна**.
|
||||
4. Для отдельного этажа задайте локальный север/режим/включение либо оставьте наследование.
|
||||
4. В поле **Лучи солнца** выберите начало от внутренних углов окна (прежнее
|
||||
поведение по умолчанию) или от внешних углов. Это общая настройка для всех
|
||||
пространств; она доступна и тогда, когда сами лучи временно выключены.
|
||||
5. Для отдельного этажа задайте локальный север/режим/включение либо оставьте наследование.
|
||||
|
||||
Если раньше вы зеркально выставляли компас, чтобы компенсировать ошибочное
|
||||
направление лучей, после обновления верните стрелку N к реальному северу.
|
||||
@@ -1652,7 +1655,9 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
|
||||
| Переход через 3° | Слой плавно появляется/исчезает за 2 секунды |
|
||||
| Окно внутреннее | Луча нет |
|
||||
| Окно не смотрит на солнце | Луча нет |
|
||||
| Толстая внешняя стена | Луч начинается из внутренних углов тоннеля окна |
|
||||
| Толстая внешняя стена, режим «внутренние углы» | Луч начинается с комнатной стороны оконного тоннеля |
|
||||
| Толстая внешняя стена, режим «внешние углы» | Луч начинается с фасадной стороны и проходит к комнате только внутри оконного тоннеля |
|
||||
| Стена нулевой толщины | Оба режима выглядят одинаково |
|
||||
| «Скрыть проёмы» включено | Символ окна скрыт, луч продолжает работать |
|
||||
| Белый план | По бокам луча есть тонкая 1px тёмная кромка для читаемости |
|
||||
| Любая погода, включая дождь и снег | Погода не влияет на лучи |
|
||||
|
||||
+11
-8
@@ -7,7 +7,7 @@ Status: **implemented / evolving** (post beta.4 redesign). Visual reference:
|
||||
Owner intent: thick walls form one continuous hatched body (seamless L and T),
|
||||
grow both ways from the room centreline, fills/light stay inside the inner
|
||||
contour, displayed area is the clean floor, and sun wedges start at the two
|
||||
room-side corners of an opening.
|
||||
corners of the globally selected inner or outer opening face.
|
||||
|
||||
Code: `src/wall-thickness.ts`, render in `src/houseplan-card.ts` /
|
||||
`src/space-render.ts`. Sun: `src/sun.ts`. Tests: `test/wall-thickness.test.mjs`,
|
||||
@@ -369,11 +369,14 @@ invoke it.
|
||||
|
||||
## 5. Sun
|
||||
|
||||
Wedges do not draw through wall bodies. Their full source span is translated
|
||||
from the centreline by half the wall depth along the receiving room's inward
|
||||
normal, so both side rays begin at the two room-side corners of the window
|
||||
opening. Clip to the receiving room's inner contour. `d = 0` keeps the previous
|
||||
centreline/full-span wedge. `hide_openings` hides the symbol only.
|
||||
Wedges do not draw through wall bodies. The global `settings.sun_ray_origin`
|
||||
selects their source face. `inner` (default and legacy fallback) translates the
|
||||
full span from the centreline by `+d/2` along the receiving room's inward
|
||||
normal, so both sides begin at the room-side corners. `outer` translates it by
|
||||
`-d/2` and includes only the rectangular physical window tunnel before the
|
||||
receiving room's inner contour; adjacent wall and exterior space stay clipped.
|
||||
The nominal length and gradient begin at the selected span. `d = 0` makes both
|
||||
modes identical. `hide_openings` hides the symbol only.
|
||||
|
||||
## 6. Tool / hooks / i18n
|
||||
|
||||
@@ -446,8 +449,8 @@ handle and cannot split or re-key their atomic records
|
||||
`demo/smoke_resize_wall_thickness.mjs`); an eligible uniformly thick exact
|
||||
wall moves through the fixed-topology safe pipeline and Undo restores its
|
||||
source (`demo/smoke_room_resize.mjs`);
|
||||
the zero-wall preview paints above the real body; sun starts at the room-side
|
||||
opening corners; nav mode restores after `can_write`; a 1 cm body uses
|
||||
the zero-wall preview paints above the real body; sun starts at the selected
|
||||
inner/outer opening corners; nav mode restores after `can_write`; a 1 cm body uses
|
||||
solid-only in both full and static cards while a 20 cm body keeps its hatch;
|
||||
door/window/gate tunnels repeat outer/shared room fills without an axis seam
|
||||
(`demo/smoke_opening_tunnel_fill.mjs`); corner Split keeps the same facade in
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": "oxipng 10.2.0",
|
||||
"sourceFingerprint": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceFingerprint": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"captureScriptSha256": "9e4b0ae533407fe003a129078aec020f2c83d67cdb6fc6da696ce3d829beaeb5",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "76c35a87810e3dc6890bda7b43da133f57d8860031204a62cda7115c15950090"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "795585acb09488236ebb8028a7fcbd918404c5fe01560bb9b4559de13d441625"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "9f0970de09772674c3156b9fc5b2d49de0ceb22be11ecf8b136047d37d100a84"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "0c354a79f9c3660788b6c5e889a27b6bd78b1f209dc0eef6bb43e2c1344028c4"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "64169c543291bc4df806dcf8ff484b42c8ea55eb343323d945344e0e28f2ce97"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "3c8703175605fcf0387d654cd30783ecfd33e21743e70b45190a5ae1a8d355c2"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "5d6193202102f5e6d85567f1111fa105895b933979fbf369c6ba0b1029deb823"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "375900a29aa62632883ede6727b7dbb8b48cabe9d7d0e16425f9779c837f3dd9"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "3d85424676c823ddca3fa7dab3cce089b6030e48c05d16d2d48d922c66c0cbd2"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "ac962820f8e1c5cb5b042d39b18dc95905fa840cb2d4749060d991be4f6977c2"
|
||||
},
|
||||
"pdf-export": {
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"theme": "light",
|
||||
"language": "en",
|
||||
"sourceSha256": "58a0c82061aeaa744df96d8f089d2048469e69743920a6d9d67d4c0b84029d87",
|
||||
"sourceSha256": "1104398c6aa8885020c8f8b714582b66f586c9cb845846d52813990d87f94454",
|
||||
"imageSha256": "4537692fd57a3ed95528eddd748eee5cb6e456ffab7cc49169853d62459252cd"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user