Release v1.61.0-beta.6 candidate

This commit is contained in:
Matysh
2026-08-11 01:14:59 +03:00
parent b0c29fb57f
commit d2bc908280
59 changed files with 4417 additions and 1629 deletions
+126
View File
@@ -0,0 +1,126 @@
# Light on the plan — the model (source of truth)
Status: accepted by the owner after manual testing, 2026-08-11 (#71). Replaces
the layered model of v1.61.0-beta.5 and earlier.
## Principle
**A lamp lights the floor it can see.** That is the whole model. Everything the
plan shows — a beam through a doorway, a shadow behind a column, a wall corner
cutting that beam two rooms away, light flowing across a virtual boundary —
is one computation, so those things cannot disagree with each other.
The previous model computed them separately: a clip for the source's "open
zone", a blurred sector pasted at each doorway, a mask for obstacle shadows.
Every fix to one layer broke another; a doorway could be an unlit bar between
two lit rooms, a beam could float detached from its aperture, a shadow could
dissolve into a smear, and walls belonging to a farther room cast nothing at
all. None of those states is expressible now.
## What stops light, and what does not
`_lightBarriers()` (`src/houseplan-card.ts`) builds the barrier set once per
plan geometry and shares it between every lamp in the space.
**Opaque**
- the wall bodies exactly as the plan draws them (`wallBodiesGeometry`), with
their real thickness and mitred junctions;
- independent bodies: partitions, columns, room drafts;
- the bare outline of any room edge that carries no thickness — a wall is still
a wall when it is drawn as a line.
**Transparent**
- doorways, gates and arches — cut through the masonry, so an opening is a real
gap between two jamb faces and a thick wall's returns narrow the beam;
- virtual (open) boundaries, which are not walls to begin with.
**Deliberately opaque, although the plan draws an opening there**
- windows: an indoor lamp must not wash the street;
- a door with no floor behind it. An opening is transparent only where BOTH
sides are floor; otherwise a front door glows halfway — up to the centreline,
where the room polygon ends — and the plan shows a lit doorway to nowhere.
So the light's masonry is cut by passages only and differs on purpose from the
drawn one.
## From barriers to a lit region
1. **Split at crossings** (`splitAtIntersections`). The sweep casts a ray at
every barrier ENDPOINT. Two faces that cross in their middles — normal where
wall bodies meet at a junction — would leave that corner unsampled, and the
fan would close it with a chord: a sliver of floor next to a corner the lamp
plainly sees goes dark. After the split every crossing is an endpoint and the
sweep is exact, whatever shape the geometry arrived in.
2. **Sweep** (`visibilityPolygon`, `src/light-visibility.ts`): a ray at every
corner and just to either side of it, the nearest hit wins, and the fan is
closed with an arc at the lamp's own radius (`GLOW_ARC_STEPS` = 96, chord
error 0.05% of the radius).
3. **Intersect with the floor** (`intersectionPaths`): light lands on rooms, not
on the space around the house.
The result is ONE `clipPath` for ONE `<circle>` filled with the source's radial
gradient. A shadow is simply floor that is not in that region.
## Brightness
`glowAlpha` remains the only intensity formula (docs/specs/067) and gives the
alpha **at the centre** of the pool. `GLOW_FALLOFF` then spends that alpha over
the whole radius (100/88/62/32/0 %). The old flat plateau out to 70% turned
every clipped shape into a slab of solid colour with a rim — which is exactly
how a doorway sector read in the next room. The gradient is `userSpaceOnUse`
and centred on the lamp, so attenuation depends on distance from the lamp and
on nothing else: the floor behind a door is faint because it is far.
A shadow currently keeps no light at all. This follows from "objects do not let
light through" and is the owner's standing decision; a residual term would be
one constant if a pitch-black corner next to a bright area ever needs softening.
## Penumbra
One SVG `feGaussianBlur` over the whole light layer, sized in SCREEN pixels
(`GLOW_EDGE_FEATHER_PX` = 2, so σ = 1 px) and converted to plan units with the
current zoom — an edge stays a hairline when the plan is enlarged instead of
turning into a smear. Measured: the lit→unlit border crosses 20–80% in 1.5 px
and is fully done in 3.5 px.
Do NOT reach for CSS `filter: blur()` here: Chromium applies it to an SVG group
in name only (`getComputedStyle` reports the blur; the picture changes by a
couple of hundred pixels on a whole plan). And do not blur per source — one
pass over the layer costs a fifth of one blurred mask per source.
## Caching
Barriers are keyed by a fingerprint of their own geometry, never by
`_cfgEpoch`: the epoch lags behind geometry edited in place, and a stale barrier
set is invisible — the plan simply keeps lighting through a wall that now
exists. The same fingerprint, plus position and radius, keys the per-source
region cache (`_glowClipCache`).
## What the tests hold
- `test/light-visibility.test.mjs` — the sweep itself: a wall stops light, a
doorway lets a beam through and only through, a column's shadow has the
angular width its size dictates, an occluder out of range changes nothing, an
edge through the lamp is ignored, and a corner made by two crossing barriers
is lit right up to the corner.
- `demo/smoke_glow.mjs` — pixels on a rendered plan: the aperture itself is lit,
the floor behind a door is lit, the visible beam is no wider than twice the
opening, there is no light behind a column while the floor beside it is lit,
the lit→unlit border is at most 4 px wide, a spot has exactly one painted
child, the light layer contains exactly one blur and no mask, and an outside
door does not change the lit region at all.
- `demo/smoke_openwall.mjs` — a lamp lights across one virtual boundary, and
across two in sequence, only where it can see through both.
- `test/golden-matrix.test.mjs` — the source contract: one region per source, no
second layer of light, no barrier cache keyed by the epoch.
- Golden: `lighting-opaque-glow-two-doorways-dark` and the other `lighting-*`
scenes, re-shot and approved 2026-08-11.
## Performance
One clipped circle per source and one blur for the layer: 60 sources in the
`large-light-blend-v1` profile render in ~75 ms against ~259 ms for the layered
model on the same runner (CPU throttled 4×).