mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
269 lines
14 KiB
Markdown
269 lines
14 KiB
Markdown
# Hidden Isometric View internals
|
||
|
||
Issue [#89](https://github.com/Matysh/houseplan-card/issues/89) implements a
|
||
hidden, presentation-only volumetric View experiment. The normative contract is
|
||
`docs/specs/089-isometric-view-stage1.md`; the fixed rendering decisions are in
|
||
`docs/adr/089-isometric-stage1-renderer.md`.
|
||
|
||
## Activation
|
||
|
||
The feature belongs to the single hidden alpha set in `src/labs.ts`. Enable all
|
||
experiments in the current build with either `?hp_alpha=1` or
|
||
`#hp_alpha=1&space=<id>`; disable them with `hp_alpha=0`. Query operations are
|
||
applied first, hash operations second, and the last exact `1`/`0` wins. A known
|
||
operation is persisted as that exact string in `houseplan_card_alpha_v1`; the
|
||
URL itself is not rewritten. Unknown values fail closed for the current
|
||
resolution and do not overwrite storage.
|
||
|
||
The selected presentation is stored per space in `houseplan_card_view_v1`. Flat
|
||
is always the initial default. Kiosk has no toggle but reads the saved preference.
|
||
Editors and `houseplan-space-card` are always flat.
|
||
|
||
The switch has no version expiry and enables the complete capability set known
|
||
to the installed build. The legacy `hp-labs` URL and
|
||
`houseplan_card_labs_v1` storage are not read or migrated, so former testers
|
||
must enable `hp_alpha` once. Malformed registry entries and duplicate ids fail
|
||
closed. Alpha never gates schemas, migrations, plan stores, service calls or
|
||
network requests.
|
||
|
||
## 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 current Stage 4 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, opening symbols and vacuum path/outline. Stage
|
||
1 does not add a 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.
|
||
|
||
## Deliberate Stage 1 limits
|
||
|
||
- door, window and gate keep their current floor-plane symbol and live state;
|
||
- no vertical leaf/window panels, sill model or new window light;
|
||
- no floor-edge extrusion, shadows, photorealistic materials or marker
|
||
occlusion;
|
||
- no volumetric editor and no volumetric `houseplan-space-card`;
|
||
- no YAML/config option or public settings surface.
|
||
|
||
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.
|
||
|
||
## Stage 2 composition (#122)
|
||
|
||
Stage 2 evolves the same hidden `iso` experiment; it does not add a flag,
|
||
setting or public activation path. The accepted implementation contract is
|
||
`docs/specs/122-isometric-stage2.md` and the fixed composition decisions are in
|
||
`docs/adr/122-isometric-stage2-composition.md`. Its historical per-feature
|
||
lifetime was superseded by the single indefinite `hp_alpha` gate in #448; the
|
||
Stage 2 rendering contract itself is unchanged.
|
||
|
||
### One structural scene, live opening leaves
|
||
|
||
The per-card LRU remains capped at eight entries. Its Stage 2 value 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 includes rooms, masonry/opening geometry, flips, scale/camera, wall and
|
||
edge heights and algorithm revision. It excludes HA state, theme, hover,
|
||
day/night 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.
|
||
|
||
`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 Stage 1 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.
|
||
|
||
### 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.
|
||
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.
|
||
|
||
- 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`: Stage 2 roots are absent and the established floor
|
||
symbols and Stage 1 projected frame return (subject to `hide_openings`),
|
||
avoiding floating panels or an invisible Stage 2 bound that reframes them;
|
||
- Flat, editors and `houseplan-space-card` retain their old symbols and DOM.
|
||
|
||
Stage 2 adds no window beam, Glow source, sun renderer, material config,
|
||
network request or HA service path. Structural topology/projection exceptions
|
||
still use the Stage 1 latched Flat fallback. The known independent exact-SHA
|
||
view-toggle performance debt remains tracked in #124; #122 neither weakens its
|
||
budget nor treats fallback as benchmark success.
|
||
|
||
## Stage 4 visual handoff (#570)
|
||
|
||
Stage 4 refines the same hidden `iso` presentation behind `hp_alpha`; it adds no
|
||
public switch, configuration field or experiment id. Stage 3 history remains in
|
||
`docs/specs/160-isometric-stage3.md` and
|
||
`docs/adr/160-isometric-stage3-overlays.md`; the current acceptance contract is
|
||
the reviewed specification in issue #570 plus its revision-3 designer handoff.
|
||
|
||
### Camera and low screen-facing overlays
|
||
|
||
The camera is orthographic `rotDeg=0`, `tiltDeg=20`, with the same `[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.
|
||
|
||
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. Each
|
||
first clears wall silhouettes. A second deterministic group pass separates the
|
||
complete screen-space footprints of devices and lock badges from one another
|
||
within one absolute 48 CSS-pixel budget, using a bounded spatial index and
|
||
stable kind/id tie-break; it never writes the correction back to configuration.
|
||
The live pass enumerates one-pixel lattice candidates only at critical edges
|
||
and intersections of the forbidden screen-space roots it actually encounters;
|
||
it does not scan the area of the displacement disk or depend on a coarse grid.
|
||
Room, wall-silhouette and radius checks remain exact final filters, so a legal
|
||
slit narrower than four CSS pixels is still found without weakening masonry or
|
||
ownership safety. The two full isometric profiles therefore use the ordinary
|
||
150/60/75 ms resize/pan/state noise allowances again (#585).
|
||
Room labels do not enter that mutual pass and stay below interactive roots. If
|
||
the owning room truly has no legal placement, the least-overlapping result is
|
||
kept as an explicit degraded diagnostic without hiding or shrinking an item.
|
||
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 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.
|
||
|
||
### Openings, occlusion and materials
|
||
|
||
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. Opening geometry stays inert and lock actions stay on the existing
|
||
guarded lock badge/card.
|
||
|
||
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.
|
||
|
||
The constant material/filter set remains theme-aware and bounded. Only the
|
||
building ambient shadow remains; wall-contact and leaf shadows are omitted.
|
||
Forced colours or missing filter support remove texture and the ambient shadow, not geometry,
|
||
ownership or actions. `show_borders:false` is the exact no-volume branch: the
|
||
floor keeps the real 0°/20° affine matrix and interactive overlays return to
|
||
their floor anchors. `hide_openings` removes vertical decoration but preserves
|
||
cuts, Glow/SUN and lock semantics.
|
||
|
||
The eight-entry structural LRU fingerprints the 0°/20°/84 profile, opening
|
||
policy revision 3 and structural algorithm 5. It excludes HA state, live
|
||
opening amount, hover/selection, theme, SUN and filter capability. The lazy
|
||
`iso-scene-render` graph is still not requested with alpha off; topology,
|
||
projection or module mismatch still enters the established fingerprint-latched
|
||
Flat fallback. Linux exact-SHA goldens and performance profiles remain the
|
||
canonical release evidence.
|