35299307d6
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.
161 lines
5.9 KiB
TypeScript
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;
|
|
}
|