Files
DOSSIER-STANDALONE/src/state/viewSnapshots.ts
T
karim 35299307d6 Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)
Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md -Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
2026-07-09 00:57:29 +02:00

161 lines
5.9 KiB
TypeScript

// Ausschnitte / View-Snapshots — reine Erfassungs-/Wiederherstellungslogik.
//
// Ein „Ausschnitt" (View-Snapshot, DOSSIER A2) ist eine benannte, gespeicherte
// Ansicht, die den kompletten Darstellungszustand einfängt und wiederherstellt.
// KERN-PRINZIP: nichts neu erfinden — die Bausteine existieren schon (View-
// State, Sichtbarkeits-Snapshots wie `LayerCombo`/`DrawingCombo`, Overrides,
// aktives Geschoss) und werden hier nur KOMPONIERT + benannt gebündelt.
//
// Dieses Modul hält die eigentliche Logik als PURE Funktionen (testbar, ohne
// React/Store): Capture aus einem Zustandsobjekt → ViewSnapshot, und Read eines
// ViewSnapshots → wiederherstellbarer Kernzustand (den das Wiring in App.tsx auf
// die konkreten Setter abbildet). Das Setter-/rAF-Wiring bleibt in App.tsx.
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import type { OverrideRule, ViewSnapshot } from "../model/types";
import type { DetailLevel, View3d, ViewType } from "../ui/TopBar";
/**
* Der wiederherstellbare KERNZUSTAND eines Ausschnitts — alle Felder, die ein
* Snapshot einfängt, OHNE Meta (`id`/`name`/`folder`). Sowohl `captureViewSnapshot`
* (Eingang) als auch `readViewSnapshot` (Ausgang) sprechen diese Form; damit ist
* ein sauberer Roundtrip (capture → read) trivial prüfbar.
*/
export interface ViewSnapshotState {
viewType: ViewType;
view3d: View3d;
fov: number;
scaleDenominator: number;
detail: DetailLevel;
activeLevelId: string;
layerVisibility: Record<string, boolean>;
drawingVisibility: Record<string, boolean>;
/** Ids der aktiven (`enabled`) Override-Regeln zum Erfassungszeitpunkt. */
enabledOverrideRuleIds: string[];
}
/**
* Baut aus einem Kernzustand einen benannten Ausschnitt. Pure — `id`/`name`
* kommen vom Aufrufer (App erzeugt die Id, der Nutzer den Namen); die
* Sichtbarkeits-/Overrides-Maps werden defensiv kopiert, damit der Snapshot vom
* Live-Zustand entkoppelt ist. `folder` optional (Phase 1: flach).
*/
export function captureViewSnapshot(
id: string,
name: string,
state: ViewSnapshotState,
folder?: string,
): ViewSnapshot {
const snap: ViewSnapshot = {
id,
name,
viewType: state.viewType,
view3d: state.view3d,
fov: state.fov,
scaleDenominator: state.scaleDenominator,
detail: state.detail,
activeLevelId: state.activeLevelId,
layerVisibility: { ...state.layerVisibility },
drawingVisibility: { ...state.drawingVisibility },
enabledOverrideRuleIds: [...state.enabledOverrideRuleIds],
};
if (folder && folder.trim()) snap.folder = folder.trim();
return snap;
}
/**
* Liest den wiederherstellbaren Kernzustand aus einem Ausschnitt. Fehlende
* optionale Felder (aus hand-/altangelegten Snapshots) werden defensiv
* aufgefüllt (`fov` → 50, `enabledOverrideRuleIds` → []). Die Sichtbarkeits-Maps
* werden kopiert, damit der Aufrufer sie gefahrlos weiterreichen kann.
*/
export function readViewSnapshot(snap: ViewSnapshot): ViewSnapshotState {
return {
viewType: snap.viewType,
view3d: snap.view3d,
fov: snap.fov ?? 50,
scaleDenominator: snap.scaleDenominator,
detail: snap.detail,
activeLevelId: snap.activeLevelId,
layerVisibility: { ...snap.layerVisibility },
drawingVisibility: { ...snap.drawingVisibility },
enabledOverrideRuleIds: snap.enabledOverrideRuleIds
? [...snap.enabledOverrideRuleIds]
: [],
};
}
/** Deep-Equal zweier `Record<string, boolean>`-Maps: gleiche Keys UND Werte. */
function boolMapEqual(
a: Record<string, boolean>,
b: Record<string, boolean>,
): boolean {
const ka = Object.keys(a);
const kb = Object.keys(b);
if (ka.length !== kb.length) return false;
for (const k of ka) {
if (a[k] !== b[k]) return false;
}
return true;
}
/** Mengengleichheit zweier Id-Listen (Reihenfolge egal, Duplikate ignoriert). */
function idSetEqual(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const sa = new Set(a);
for (const id of b) {
if (!sa.has(id)) return false;
}
return sa.size === new Set(b).size;
}
/**
* Prüft, ob ein Ausschnitt-Kernzustand EXAKT dem aktuellen Live-Zustand
* entspricht — Grundlage der „angewählt bleibt bis zur Abweichung"-Logik im
* Ausschnitte-Panel. Vergleicht ALLE erfassten Felder feldweise (Enums/Zahlen
* strikt, Sichtbarkeits-Maps deep-equal, Override-Ids als Menge). Weicht auch
* nur EIN Feld ab → `false`. Pure/testbar; der Aufrufer (App.tsx) normalisiert
* den gespeicherten Snapshot vorher über `readViewSnapshot`, damit fehlende
* optionale Felder (fov/overrides) fair verglichen werden.
*/
export function snapshotMatchesLiveState(
snap: ViewSnapshotState,
live: ViewSnapshotState,
): boolean {
return (
snap.viewType === live.viewType &&
snap.view3d === live.view3d &&
snap.fov === live.fov &&
snap.scaleDenominator === live.scaleDenominator &&
snap.detail === live.detail &&
snap.activeLevelId === live.activeLevelId &&
boolMapEqual(snap.layerVisibility, live.layerVisibility) &&
boolMapEqual(snap.drawingVisibility, live.drawingVisibility) &&
idSetEqual(snap.enabledOverrideRuleIds, live.enabledOverrideRuleIds)
);
}
/**
* Setzt bei jeder Override-Regel `enabled = enabledIds.includes(rule.id)`
* (immutabel). Reproduziert die Aktiv-Menge, die ein Ausschnitt gespeichert hat.
* Regeln, die es beim Erfassen noch nicht gab, bleiben unberührt deaktiviert;
* inzwischen gelöschte Regel-Ids werden schlicht ignoriert. Liefert eine NEUE
* Liste nur, wenn sich etwas ändert (sonst dieselbe Referenz → No-Op-freundlich
* für den setProject-History-Schutz).
*/
export function applyOverrideRuleEnabled(
rules: OverrideRule[],
enabledIds: string[],
): OverrideRule[] {
const active = new Set(enabledIds);
let changed = false;
const next = rules.map((r) => {
const enabled = active.has(r.id);
if (enabled === r.enabled) return r;
changed = true;
return { ...r, enabled };
});
return changed ? next : rules;
}