/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Level display mode — Pascal Editor's Stacked / Exploded / Solo * pattern, adapted to ifc-lite's spatial hierarchy. * * - Stacked (default): every storey at its native elevation. * - Exploded : each storey lifted along world +Y by * `storey-index × explodedGap`, indexed * after sorting storeys by elevation * ascending. Index 0 stays at its * native Y; subsequent storeys move up * by `gap`. * - Solo : only the active storey renders. Solo is * NOT a second isolation channel — it reuses * the storey filter (`selectedStoreys`), set * and cleared together with the mode by * `store/levelDisplay.applyLevelDisplayMode`. * * The slice owns mode + parameters only. The Exploded mesh * translation is applied by `useLevelDisplayEffect`, which watches * the slice and flushes per-entity offsets to the renderer via * `pendingMeshTranslations`. Solo isolation is NOT applied here — * it rides the storey filter (`selectedStoreys`), driven by * `store/levelDisplay.applyLevelDisplayMode`; the effect only adds * a guard that drops Solo → Stacked when that filter is cleared. * * Reversibility: the slice keeps the LAST APPLIED offset per * storey so the effect can compute the delta between target and * applied when the user toggles modes — no "remember the original * positions" gymnastics. Switching Exploded → Stacked subtracts * the applied offset; switching gap mid-Exploded shifts by the * difference. */ import type { StateCreator } from 'zustand'; export type LevelDisplayMode = 'stacked' | 'exploded' | 'solo'; /** Per-model snapshot of the currently-applied storey offsets. */ export type AppliedStoreyOffsets = Map< string /* modelId */, Map >; export interface LevelDisplaySlice { levelDisplayMode: LevelDisplayMode; /** Per-storey gap in metres for Exploded. Default 4 m. */ explodedGap: number; /** * Bookkeeping — last applied Y offset per storey, per model. * Read by the effect to compute deltas without re-doing the * "what was the previous gap?" math; written by the effect * after each successful flush. Tests can probe this directly. */ appliedStoreyOffsets: AppliedStoreyOffsets; setLevelDisplayMode: (mode: LevelDisplayMode) => void; setExplodedGap: (metres: number) => void; /** Effect-only: record the offsets that were just flushed to * the renderer so the next toggle knows what to subtract. */ setAppliedStoreyOffsets: (next: AppliedStoreyOffsets) => void; } const LEVEL_DISPLAY_DEFAULTS = { mode: 'stacked' as LevelDisplayMode, gap: 4, }; export const createLevelDisplaySlice: StateCreator = (set) => ({ levelDisplayMode: LEVEL_DISPLAY_DEFAULTS.mode, explodedGap: LEVEL_DISPLAY_DEFAULTS.gap, appliedStoreyOffsets: new Map(), setLevelDisplayMode: (levelDisplayMode) => set({ levelDisplayMode }), setExplodedGap: (metres) => { // Guard against non-finite / non-positive — UI lets the user // type, but a 0 gap means "Exploded = Stacked" and a negative // gap inverts the sort, which is rarely useful. Clamp to // [0, 100] to keep behaviour sane. const clamped = Math.max(0, Math.min(100, Number.isFinite(metres) ? metres : 0)); set({ explodedGap: clamped }); }, setAppliedStoreyOffsets: (appliedStoreyOffsets) => set({ appliedStoreyOffsets }), });