Files
Claude 4bc3e9baa6 docs(hygiene): ARCHITECTURE.md — карта, а не хроника (#680)
Волна 3 эпика #674. ARCHITECTURE.md 2330 → 653 строки: система координат
прототипа 1489×1053, разделы Hidden Isometric Stage 2/4 (текущее — ISOMETRIC.md,
история — ADR 122/570), хроники «Additions v1.28–v1.41» и «Audit follow-ups»
сняты; подсистемы — короткое описание со ссылкой на канонический документ.
Устаревшее исправлено по коду: дерево Layout (store.py, frontend_registration,
весь список модулей), хранилища (config, layout, virtual_lights, trails),
ленивые локали de и fr, таблица WS API (31 + 3 команды, #256-проекция,
space/delete, files/cleanup без keep, контент через /api/houseplan/content),
DevItem без несуществующих полей, отказ help/feedback по support_api.

Всё ещё верное и не записанное в другом месте перенесено в канонические
документы: DEVICE-PRESENTATION (заметки реализации, Action authority, черновик
диалога), RADAR (карта реализации), VACUUM (владение кодом), CANVAS (icon_size,
--hp-cell-visual-scale, барьер записи координат), FILTERING (#44, каталог),
DECOR-EDITOR §7 (инварианты бэкенда ассетов), WALL-THICKNESS (§1 идентичность
и нулевые стены, §2 кэши, §4 hover/туннели/острова, §6 удаление комнаты,
§9 failed-core, §11 завершение цепочки и комната по грани), ISOMETRIC
(isoPlaneMatrix, iso-overlays, створки, служебные атрибуты), LIGHT (Glow над
заливкой #55, формула и screen-смешение), CONFIG-COMPATIBILITY (манифест схемы
#33, Masonry-слоты #561, квадратный холст v1.48, устаревшие URL контента,
layout/set #356), SCOPE (таблица владельцев при сборке файлов), WARM-REMOUNT
§5 (холодная загрузка и визуальная непрерывность), UX-MODES (порядок
стартового пространства), PDF-EXPORT (граница реализации), FURNITURE (adopt,
BOOT_MAX_MS). Раздел TESTING «Backend quality gates (#42)» и строка
DEVELOPMENT о bundle-freshness — из того же разбора, в предыдущем коммите.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:04 +03:00

341 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Isometric (2.5D) View internals
Issue [#89](https://github.com/Matysh/houseplan-card/issues/89) started a
presentation-only volumetric View experiment; the normative Stage 1 contract is
`docs/specs/089-isometric-view-stage1.md`, the fixed rendering decisions are in
`docs/adr/089-isometric-stage1-renderer.md`. Since Stage 6
([#649](https://github.com/Matysh/houseplan-card/issues/649)) the 2.5D View is a
public mode, see [Stage 6](#stage-6-public-mode-tiles-sun-and-materials-649).
This document describes the current View only; how it got here, stage by
stage, is in the ADRs (`docs/adr/089-…`, `122-…`, `160-…`,
`570-isometric-stage4-visual-handoff.md`) and their specs.
## Activation
One installation-wide setting, `settings.volumetric_view: boolean`, switched in
**General settings › Display › Show the plan in 2.5D** (third item after «Show
live presence on the plan»). It is stored only when `true`; missing or `false`
means Flat, which is also the rollback. The rule is one for the card, the
sidebar page and the kiosk: the View is 2.5D when the setting is on and Flat
otherwise. Editors and `houseplan-space-card` are always Flat.
Saving the setting switches the View at once, without a reload, and the view
centre is carried over through the logical plan (#583 §6.3). The lazy
`iso-scene-render` graph is loaded only while the setting is on. The fingerprint
fallback (#89) is unchanged: a failed scene falls back to Flat for that key.
On a cold dashboard load, the first visible successful frame is already 2.5D:
the existing neutral House Plan loading surface remains above the plan until
both the lazy graph and the paper-dependent floor classification are ready.
This is the same in the ordinary card and kiosk mode. A real lazy-load failure
releases that surface and uses the existing safe Flat fallback; it does not
change the saved setting. Flat View and every editor do not wait for the 2.5D
runtime. Paper colour is resolved after the DOM commit (white for a drawn plan,
the theme card background under an image plan) and the resulting light-floor
set is reused until the paper, resolved room fills or room membership changes
([#654](https://github.com/Matysh/houseplan-card/issues/654)).
There is no toggle on the card and no alpha entry: `iso` is gone from
`LABS_FLAGS`, the header `projection-toggle` and the phone-menu item
`projection` (#616) are removed, and the former per-device, per-space choice
`houseplan_card_view_v1` is no longer read (owner decision: not migrated). The
`hp_alpha` switch itself remains as a mechanism without experiments.
## Coordinate systems
- Plan points are the existing 1000-unit logical floor coordinates.
- Scene points are coordinates in the effective SVG viewBox.
- HTML devices, vacuum pucks, room labels/cards and lock markers stay
screen-facing. Their anchors use the same `projectPlanPoint()` snapshot as the
SVG floor.
- `clientToScenePoint()` maps a client point to the current scene; floor hit
testing then uses `unprojectFloorPoint()`.
The presentation uses a fixed orthographic affine camera:
```text
rotDeg=0, tiltDeg=20, xyScale=1, zScale=1, origin=[500,500]
wallHeight=84 scale-aware visual units
```
There is no perspective, free rotation or user tilt. Switching projections
preserves scalar zoom and converts the view centre through logical floor space.
`projectedFrame()` includes both floor corners and wall-top corners so fit/home
cannot clip the volume.
Room fit (#152) projects every final floor vertex at floor and floor-edge depth
and every selected-room boundary-wall vertex at floor and wall-top height before
building the AABB. It never projects a plan-space bounding rectangle, so a
concave room does not gain fictitious corners. The resulting camera still uses
the shared View controller; changing projection cancels the room-focus intent.
## Geometry and composition
`src/iso-projection.ts` owns pure projection math. `src/iso-walls.ts` consumes
the canonical `wallBodiesGeometry()` MultiPolygon after openings and extra
physical bodies have been resolved. It normalizes outer/hole winding, builds one
evenodd top path and at most one visible side per ring edge, then uses a stable
depth/order tie-break. Complexity is O(E) in canonical ring edges.
For connected drafts and partitions those extras already contain computed
bounded junction patches. Isometric wall tops/sides therefore use the same
seamless L/T footprint as flat full/static cards; raw per-record rectangles are
reserved for editor identity and never projected as competing wall faces.
The content fingerprint includes rooms, wall geometry/thickness, open cuts,
openings, partitions, drafts, columns, scale/grid inputs, camera, wall height and
algorithm version. It deliberately excludes `_cfgEpoch`, HA state, hover and
`show_borders`. The per-card LRU is capped at eight scenes. Pan, zoom and HA-only
updates reuse it.
Composition remains SVG-first:
1. the existing floor SVG and all its current live layers;
2. explicit visible wall sides and wall top;
3. screen-facing HTML overlays.
The floor keeps the same nodes and order for paper/backdrop, room fills/hover,
Glow/spill, sun, decor/furniture, flat stair symbols, opening symbols and vacuum path/outline. The
volumetric View adds no second light source/layer; markers and room cards intentionally
remain above walls without geometric occlusion.
## Failure boundary
Projection/topology failures latch flat fallback for the current
`space|fingerprint`. One diagnostic is emitted without config, entity ids or URL
data. The saved iso preference is retained; an explicit iso request retries the
fingerprint, and changed geometry receives a new fingerprint. Flat rendering is
the rollback path and does not depend on the iso cache.
Internal fail-closed evidence, not public API (`STYLING-HOOKS.md` §7.7):
`.stage[data-hp-iso-stage="4"]` with `data-hp-iso-structural-builds`,
`data-hp-iso-overlay-kind|raised|nudged` on low-plane roots, and
`data-hp-iso-material-def` on shared material definitions. The structural LRU is
`_isoGeometryCache`.
## Limits
- No volumetric editor and no volumetric `houseplan-space-card`: editors and
the static card are always Flat (see Activation).
- No perspective, free rotation or user tilt; no marker occlusion by walls —
screen-facing overlays sit on a low plane above the floor and are shifted,
never hidden.
- No per-opening schema field for heights or leaves: every vertical element is
a fixed presentation ratio of `ISO_WALL_HEIGHT`.
- No YAML/config option beyond `settings.volumetric_view`.
Golden references are accepted only from the complete reviewed Linux artifact.
The full `large-house-isometric-v1` performance comparison is also canonical on
the exact Linux CI SHA.
## Structural scene and cache
The per-card LRU is capped at eight entries. A scene contains:
- canonical wall top/sides and the physical-wall contact path;
- a room/exterior floor footprint and its low visible outer faces;
- immutable opening jamb/axis bases, including type, flips and selected wall
face;
- the shared projected frame, including wall/opening tops and the low floor
edge.
The key fingerprints rooms, masonry/opening geometry, flips, scale/camera, wall
and edge heights, the `0°/20°/84` profile, opening policy revision 3 and
structural algorithm 5. It excludes HA state, live opening amount, theme,
hover/selection, day/night, SUN and filter capability. `openingAmount()` is
applied only after an LRU hit by `projectIsoOpening()`, so a contact update
projects O(O) leaves without repeating a wall or floor boolean operation.
Topology, projection or module mismatch enters the fingerprint-latched Flat
fallback (Failure boundary).
`floorFootprintGeometry()` deliberately accepts no independent physical-body
input. The slab is the union of room floors and derived exterior masonry:
internal room boundaries and nested holes make no decorative step, detached
room components keep separate outside edges, while partitions and columns do
not enlarge it.
## Layer order and materials
All geometry roots use one scene `viewBox`. The existing floor/live nodes are
grouped under the affine matrix; HTML anchors still use `projectPlanPoint()`.
```text
stage background
→ shared ambient shadow + low exterior floor edge
→ existing floor SVG (paper/image, room fills/hover, decor, Glow, sun)
→ canonical wall sides/top + inert vertical opening panels
→ existing HTML devices, labels/cards, locks and vacuum overlays
```
Wall top and side use two shared matte gradients. One shared filter supplies
only the soft exterior ambient shadow of the complete building footprint;
internal wall-contact and opening-leaf shadows are deliberately absent.
Definition count is constant per card, never per face or opening. Forced
colours use solid `Canvas`/`CanvasText` faces and omit decoration. A runtime
without the required filter paint keeps solid structure, floor edge and
vertical panels but emits no ambient shadow; this does not enter the structural
fallback latch. The 2.5D View adds no window beam, Glow source, sun renderer,
material config, network request or HA service path.
## Vertical openings and display settings
`src/iso-openings.ts` mirrors the existing opening-symbol transform algebra:
door has one jamb-hinged leaf, gate has two leaves with the established
0–10° exterior-face turn, and window has two light neutral casements. A saved
`passage` keeps the same full-height masonry cut but has zero leaves/panels.
Door/gate leaves are matte prisms `0.04H` thick with state-independent
full-depth reveals.
Heights are fixed presentation ratios of `ISO_WALL_HEIGHT`; there is no schema
field. The saved opening axis and Flat symbol remain on their canonical
centreline. Derived 2.5D door/gate leaves pivot on the selected physical host
face so their prisms do not start inside masonry; windows remain centred across
the reveal. `flip_v` selects/determines the physical face and opening direction
without changing saved coordinates. Jamb/cut depth remains physical and
independent of the Flat symbol. Panels are pointer- and ARIA-inert. Existing
lock badges/cards and HA actions remain the only interactive opening surface.
Door leaves turn by `50° × openingAmount`, paired window leaves by `65° ×
openingAmount`, and gates retain their established 0–10° behaviour. The same
canonical flips/host face that drive the floor symbol determine hinges and
direction; missing, unknown or unavailable state keeps the existing static-plan
fallback.
For wall height `H`, the fixed window frame spans `0.38H..1.00H`, its sash
`0.40H..0.98H`, and clear glass `0.45H..0.93H`; frame/sash rails are `0.05H`.
Frames and sill are neutral, glass side is `#c9e4f3` and its top face is
`#e3f2fa`. Fixed and live faces remain in `buildIsoWallDepthQueue()`. The slots
belonging to one opening are resolved by physical camera depth, so raised glass
covers the rear sill and rotating door/gate prism faces retain their physical
order without reordering unrelated walls or openings. Door/gate faces use fill
differences instead of strokes; window frame/glass borders remain.
- borders visible: vertical panels replace the floor-plane symbols;
- `hide_openings: true`: panels disappear, while masonry cuts, Glow/sun and
contact/lock meaning remain;
- `show_borders: false` is the exact no-volume branch: the volumetric roots are
absent, the floor keeps the real 0°/20° affine matrix, the floor symbols
and the projected frame return (subject to `hide_openings`) and interactive
overlays return to their floor anchors;
- Flat, editors and `houseplan-space-card` retain their old symbols and DOM.
## Camera and overlay placement
The camera is orthographic `rotDeg=0`, `tiltDeg=20`, with the `[500,500]`
pivot and scale-aware 84-unit wall height. Floor SVG, wall/opening projection,
inverse hit mapping, invisible collision footprints and fit bounds share that
one affine authority.
`isoPlaneMatrix()` (`src/iso-projection.ts`) is that authority. The projected
frame also includes the low overlay plane; blur and shadow extents never enter
fit.
Device markers, room labels/cards and opening-lock badges keep their canonical
floor anchors but render on a low plane four visual units above the floor.
Devices and lock badges in the same room whose canonical reference-fit bounds
(expanded by 12 CSS px) connect form one rigid cluster. Every member receives
the same scene-space displacement, so pairwise vectors, rows and intervals are
the affine projection of the Flat layout rather than a per-marker fan toward a
room safe point. Room labels never enter a cluster and stay below interactive
roots.
`src/iso-overlays.ts` is the pure placement boundary. A device accepts its
explicit room only when that room strictly contains its floor anchor, otherwise
the smallest strictly containing room (stable id tie-break); room labels use
their own room; lock badges inherit the physical room side selected by
opening-host geometry. Wall clearance uses a 4 CSS px safety gap
(`ISO_OVERLAY_SAFETY_GAP_CSS_PX`), and every candidate path must stay strictly
inside the owner and outside its island holes. Boundary candidates are evaluated
on the integer CSS-pixel lattice through a bounded spatial grid (#585), so
sub-4 px legal slits are found without an all-pairs or disk scan.
The reference-fit view, not the current live view, converts CSS safety values
into scene units. Wheel/button zoom, pinch and pan therefore transform an
already resolved scene and cannot invalidate placement. Structural changes —
stage/camera, walls, rooms, marker membership or canonical anchors — rebuild it
deterministically; viewport movement and HA-only state do not. One common
vector clears the exact wall silhouettes and already placed clusters within an
absolute 48 CSS-pixel reference-fit budget, using stable size/required-shift/
kind-id order and boundary candidates instead of scanning the displacement
disk. The correction is runtime-only and is never written to configuration.
If no completely legal common vector exists, the nearest deterministic result
keeps the cluster rigid and prioritises room ownership, then wall clearance,
then overlap with an earlier cluster. It never splits or shrinks a cluster;
residual overlap is an explicit degraded diagnostic. The two full isometric
profiles keep the ordinary 150/60/75 ms resize/pan/state noise allowances
(#585, #651). Fit probes reserve the maximum correction but do not execute
live collision search. There is no painted plate, long tether, ground dot or
per-marker shadow. The original screen-facing HTML root remains the only hit,
focus, tooltip and action target, and selection/hover cannot invalidate the
placement cache. Vacuum, Glow/spill, SUN, room fills/hover, arbitrary decor,
furniture/backdrop, stairs and every persisted coordinate remain on `z=0`.
Room names remain screen-facing and lose stroke, text shadow, drop shadow and
halo. Iso uses `#303936` on a light presentation and `#f2f0e8` on a dark one;
contrast comes from colour and the bounded position correction, never an
outline.
## Stage 6: public mode, tiles, sun and materials (#649)
The visual language and the numbers come from the designer lab (sketch 07,
attachments 09–13 of #649); the lab's code, coordinates and ids are not carried
over. Lab units are converted into two bases:
- **D** — the base marker core diameter of the space (`--device-base-size`)
already × `ISO_ICON_SCALE = 1.12`; the lab disc is 80 units.
- **H** — the 2.5D wall height (`gridVisualUnits(ISO_WALL_HEIGHT, cellCm)`); the
lab wall is 218.8 units.
Everything below applies only to `.stage.projection-iso.mode-view`; Flat is
byte-for-byte unchanged. Side-by-side acceptance frames:
`docs/design/649-25d-stage6/ACCEPTANCE.md`.
### Raised tiles (`src/iso-tiles.ts`, `src/styles/iso-tiles.styles.ts`)
- Every marker body (core, value pill, lock core) is a rounded rectangle with
radius `min(0.275 D, 0.3·h)`; the ring is not drawn (virtual markers too).
- A solid edge 0.1 D straight down, the body colour through
`brightness(.7) saturate(.85)` (`brightness(.82) saturate(.8)` on a light
floor); bodies with luma < 70 → `#5b5e5a`, white and dark bodies in the dark
theme → `#4a4a4a`. The colours are evaluated once in TypeScript
(`isoEdgeColor`) and emitted as a generated state table.
- Marker and lock are 1.12 × Flat; layout and collisions
(`iso-scene-render`) use the same factor. The whole marker is lifted 0.075 D.
- **One floor-shadow layer** `.iso-tile-shadows` inside `.devlayer`, rendered
after the markers in DOM order but with `z-index: -1` (`.devlayer` is a
stacking context), so no shadow ever lands on a neighbour's tile. Each marker
and lock has an inert twin (`data-shadow-of`, `aria-hidden`, no pointer
events) painted only as its inset, blurred box-shadow. Offset, blur and
opacity follow the theme × floor table of the ТЗ (`isoTileShadow`).
- Light or dark floor is decided per room: the room fill at its opacity over
the plan paper, luma > 0.55 is light (`isoLightFloorRooms`). A marker belongs
to the room of its overlay owner or, without walls, to the room under it.
- A raised room label keeps its 44 × 44 px touch floor in an invisible
`::before`, like door locks; the label box itself is sized by its text, so
the metrics row keeps the Flat distance from the name at every zoom (#665).
- Hover / focus-visible / selected / alert frames hug tile and edge
(bbox + 0.075 D per side, + edge height) and float with the tile: `#0C82F0`,
`#0C82F0`, `#F0A00C`, `#F0410C`, priority Alert > Focus > Selected > Hover.
The body is not repainted on hover.
- `forced-colors: active` or no `filter` support: no edge and no shadow; size,
tiles and frames stay.
### Soft sun wash (`src/iso-sun.ts`)
In 2.5D the Flat wedges of `sun_rays` are replaced by a soft wash; the gates are
the Flat ones (`sun_rays`, `north_deg`, `sun.sun`, elevation ≥ 3°, fade, no light
in editors or at night) and so are the windows (`windowLit()`). Details:
`docs/SUN.md` § 2.5D.
### Walls, openings and labels (`src/iso-materials.ts`)
- The top face is the user's `fill_colors.wall_fill.c`, opaque (the Flat opacity
does not apply to the prism), gradient to `c × 0.93`; the side face is
`c × 0.77 → × 0.68 @0.58 → × 0.60`. The stage carries them as
`--iso-top-hi/lo` and `--iso-side-hi/mid/lo`.
- The theme never repaints walls, openings, the floor edge, the texture or room
labels: the `theme-dark` and `prefers-color-scheme: dark` rules for `.iso-*`
are gone. Room labels use the Flat label colour. The building ambient shadow
on the card background may follow the theme.
- Furniture keeps its Flat line width: stroke px = width × the plan screen scale
in both views (the former iso constant 1 made it thicker).