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.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
// Pure Tests für den Ausschnitte-Baum (state/viewSnapshotFolders.ts):
|
||||
// Aufbau, Reparent, Zyklus-Schutz, verwaiste Referenzen, Löschen ohne
|
||||
// Datenverlust. Kein React/DOM.
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { ViewSnapshot, ViewSnapshotFolder } from "../model/types";
|
||||
import {
|
||||
buildViewSnapshotTree,
|
||||
createViewSnapshotFolder,
|
||||
deleteFolder,
|
||||
isDescendant,
|
||||
moveFolder,
|
||||
moveSnapshotToFolder,
|
||||
} from "./viewSnapshotFolders";
|
||||
|
||||
/** Minimaler ViewSnapshot-Stub (nur id/name/folderId sind hier relevant). */
|
||||
function snap(id: string, folderId?: string): ViewSnapshot {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
...(folderId ? { folderId } : {}),
|
||||
viewType: "grundriss",
|
||||
view3d: "top",
|
||||
scaleDenominator: 100,
|
||||
detail: "mittel",
|
||||
activeLevelId: "L0",
|
||||
layerVisibility: {},
|
||||
drawingVisibility: {},
|
||||
} as ViewSnapshot;
|
||||
}
|
||||
|
||||
function folder(id: string, parentId?: string): ViewSnapshotFolder {
|
||||
return createViewSnapshotFolder(id, id, parentId);
|
||||
}
|
||||
|
||||
describe("createViewSnapshotFolder", () => {
|
||||
it("lässt parentId weg, wenn nicht gesetzt (Wurzelebene)", () => {
|
||||
const f = createViewSnapshotFolder("f1", "Ordner");
|
||||
expect(f).toEqual({ id: "f1", name: "Ordner" });
|
||||
expect("parentId" in f).toBe(false);
|
||||
});
|
||||
it("setzt parentId, wenn gegeben", () => {
|
||||
expect(createViewSnapshotFolder("f2", "Sub", "f1").parentId).toBe("f1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildViewSnapshotTree", () => {
|
||||
it("verschachtelt Ordner + Ausschnitte", () => {
|
||||
const folders = [folder("f1"), folder("f2", "f1")];
|
||||
const snaps = [snap("s0"), snap("s1", "f1"), snap("s2", "f2")];
|
||||
const tree = buildViewSnapshotTree(folders, snaps);
|
||||
expect(tree.rootSnapshots.map((s) => s.id)).toEqual(["s0"]);
|
||||
expect(tree.rootFolders).toHaveLength(1);
|
||||
const f1 = tree.rootFolders[0];
|
||||
expect(f1.folder.id).toBe("f1");
|
||||
expect(f1.snapshots.map((s) => s.id)).toEqual(["s1"]);
|
||||
expect(f1.subfolders).toHaveLength(1);
|
||||
expect(f1.subfolders[0].folder.id).toBe("f2");
|
||||
expect(f1.subfolders[0].snapshots.map((s) => s.id)).toEqual(["s2"]);
|
||||
});
|
||||
|
||||
it("hebt Ausschnitt mit unbekanntem folderId auf die Wurzelebene (verwaist)", () => {
|
||||
const tree = buildViewSnapshotTree([folder("f1")], [snap("s1", "ghost")]);
|
||||
expect(tree.rootSnapshots.map((s) => s.id)).toEqual(["s1"]);
|
||||
expect(tree.rootFolders[0].snapshots).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("hebt Ordner mit unbekanntem parentId auf die Wurzelebene (verwaist)", () => {
|
||||
const tree = buildViewSnapshotTree([folder("f2", "ghost")], []);
|
||||
expect(tree.rootFolders.map((n) => n.folder.id)).toEqual(["f2"]);
|
||||
});
|
||||
|
||||
it("bewahrt die Eingabe-Reihenfolge je Ebene", () => {
|
||||
const folders = [folder("b"), folder("a")];
|
||||
const snaps = [snap("s2"), snap("s1")];
|
||||
const tree = buildViewSnapshotTree(folders, snaps);
|
||||
expect(tree.rootFolders.map((n) => n.folder.id)).toEqual(["b", "a"]);
|
||||
expect(tree.rootSnapshots.map((s) => s.id)).toEqual(["s2", "s1"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDescendant", () => {
|
||||
const folders = [folder("f1"), folder("f2", "f1"), folder("f3", "f2")];
|
||||
it("erkennt sich selbst", () => {
|
||||
expect(isDescendant(folders, "f1", "f1")).toBe(true);
|
||||
});
|
||||
it("erkennt echten Nachfahren (transitiv)", () => {
|
||||
expect(isDescendant(folders, "f3", "f1")).toBe(true);
|
||||
});
|
||||
it("verneint Nicht-Nachfahren", () => {
|
||||
expect(isDescendant(folders, "f1", "f3")).toBe(false);
|
||||
});
|
||||
it("bricht bei kaputter parentId-Kette ab (kein Absturz)", () => {
|
||||
expect(isDescendant([folder("x", "ghost")], "x", "f1")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveSnapshotToFolder", () => {
|
||||
it("setzt folderId", () => {
|
||||
const out = moveSnapshotToFolder([snap("s1")], "s1", "f1");
|
||||
expect(out[0].folderId).toBe("f1");
|
||||
});
|
||||
it("entfernt folderId bei null (Wurzelebene)", () => {
|
||||
const out = moveSnapshotToFolder([snap("s1", "f1")], "s1", null);
|
||||
expect("folderId" in out[0]).toBe(false);
|
||||
});
|
||||
it("no-op bei unbekannter Id", () => {
|
||||
const input = [snap("s1", "f1")];
|
||||
expect(moveSnapshotToFolder(input, "ghost", null)).toEqual(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveFolder", () => {
|
||||
const folders = [folder("f1"), folder("f2", "f1"), folder("f3")];
|
||||
it("hängt Ordner unter neuen Elternordner", () => {
|
||||
const out = moveFolder(folders, "f3", "f1");
|
||||
expect(out.find((f) => f.id === "f3")!.parentId).toBe("f1");
|
||||
});
|
||||
it("verschiebt auf Wurzelebene bei null", () => {
|
||||
const out = moveFolder(folders, "f2", null);
|
||||
expect("parentId" in out.find((f) => f.id === "f2")!).toBe(false);
|
||||
});
|
||||
it("verhindert Zyklus: Ordner in eigenen Nachfahren (No-op)", () => {
|
||||
expect(moveFolder(folders, "f1", "f2")).toEqual(folders);
|
||||
});
|
||||
it("verhindert Ordner in sich selbst (No-op)", () => {
|
||||
expect(moveFolder(folders, "f1", "f1")).toEqual(folders);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFolder", () => {
|
||||
it("zieht Unterordner + Ausschnitte auf die Elternebene (kein Datenverlust)", () => {
|
||||
// f1 > f2 > f3; s1 in f2. f2 löschen → f3 & s1 hängen an f1.
|
||||
const folders = [folder("f1"), folder("f2", "f1"), folder("f3", "f2")];
|
||||
const snaps = [snap("s1", "f2")];
|
||||
const out = deleteFolder("f2", folders, snaps);
|
||||
expect(out.folders.map((f) => f.id).sort()).toEqual(["f1", "f3"]);
|
||||
expect(out.folders.find((f) => f.id === "f3")!.parentId).toBe("f1");
|
||||
expect(out.snapshots[0].folderId).toBe("f1");
|
||||
});
|
||||
|
||||
it("zieht auf die Wurzelebene, wenn der gelöschte Ordner an der Wurzel lag", () => {
|
||||
const folders = [folder("f1"), folder("f2", "f1")];
|
||||
const snaps = [snap("s1", "f1")];
|
||||
const out = deleteFolder("f1", folders, snaps);
|
||||
expect(out.folders).toHaveLength(1);
|
||||
expect("parentId" in out.folders[0]).toBe(false); // f2 an Wurzel
|
||||
expect("folderId" in out.snapshots[0]).toBe(false); // s1 an Wurzel
|
||||
});
|
||||
|
||||
it("no-op bei unbekannter Ordner-Id", () => {
|
||||
const folders = [folder("f1")];
|
||||
const snaps = [snap("s1", "f1")];
|
||||
const out = deleteFolder("ghost", folders, snaps);
|
||||
expect(out.folders).toEqual(folders);
|
||||
expect(out.snapshots).toEqual(snaps);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user