2D-Plan: z-Anordnen (nach vorn/hinten holen)

Selektierte 2D-Zeichnungselemente lassen sich jetzt in der Zeichen-
reihenfolge umsortieren — Kontextmenü „Ganz nach vorn / Nach vorn /
Nach hinten / Ganz nach hinten". Reine Array-Umsortierung in
project.drawings2d (spätere Position = optisch oben), Undo-fähig über
setProject. reorderDrawings(ids, front|forward|backward|back).
This commit is contained in:
2026-07-04 12:40:25 +02:00
parent 1db661f19a
commit 018fef56bf
5 changed files with 213 additions and 0 deletions
+65
View File
@@ -162,6 +162,18 @@ export interface ProjectSlice {
* `hatchSource`), die kein eigenes Setter-Paar haben.
*/
updateDrawing2D: (id: string, patch: Partial<Drawing2D>) => void;
/**
* Sortiert die gewählten 2D-Zeichnungselemente in `drawings2d` um (z-Anordnen
* im Grundriss). Die Array-Reihenfolge IST die Zeichenreihenfolge — spätere
* Position = weiter oben/vorn (siehe `generatePlan.ts`). Unbekannte IDs
* werden ignoriert; die relative Reihenfolge der Auswahl bleibt erhalten.
* - "front"/"back": Auswahl ans Ende/an den Anfang des Arrays.
* - "forward"/"backward": Auswahl je einen Schritt Richtung Ende/Anfang.
*/
reorderDrawings: (
ids: string[],
op: "front" | "back" | "forward" | "backward",
) => void;
/**
* Skaliert die Geometrie eines Elements auf Zielbreite×-höhe (Meter) um einen
* Ankerpunkt (fx/fy ∈ [0,1] relativ zur bbox; 0,0 = oben-links).
@@ -828,6 +840,12 @@ export function createProjectSlice(
drawings2d: p.drawings2d.map((d) => (d.id === id ? { ...d, ...patch } : d)),
})),
reorderDrawings: (ids, op) =>
setProject((p) => ({
...p,
drawings2d: reorderDrawings2D(p.drawings2d, ids, op),
})),
// ── Größe (Resize um Anker) ────────────────────────────────────────────
resizeElement: (kind, id, w, h, anchor) =>
setProject((p) => resizeElement(p, kind, id, w, h, anchor)),
@@ -1031,6 +1049,53 @@ function setOverrideField(
return { ...project, drawings2d: project.drawings2d.map(apply) };
}
/**
* Z-Anordnen im Grundriss: `drawings2d` IST die Zeichenreihenfolge (spätere
* Position = weiter oben, siehe `generatePlan.ts`). Unbekannte IDs werden
* ignoriert; reine, immutable Umsortierung.
*
* - "front"/"back": die gewählten Elemente wandern gesammelt ans Ende/an den
* Anfang des Arrays, ihre relative Reihenfolge untereinander bleibt erhalten.
* - "forward"/"backward": jedes gewählte Element rückt einzeln um eine
* Position Richtung Ende/Anfang vor. Bei "forward" von hinten nach vorn
* iteriert (sonst würde ein vorderes Element ein gerade verschobenes
* sofort wieder überholen); bei "backward" entsprechend von vorn nach
* hinten. Über die Array-Grenzen hinaus passiert nichts (kein Wrap).
*/
function reorderDrawings2D(
drawings: Drawing2D[],
ids: string[],
op: "front" | "back" | "forward" | "backward",
): Drawing2D[] {
const selected = new Set(ids.filter((id) => drawings.some((d) => d.id === id)));
if (selected.size === 0) return drawings;
if (op === "front" || op === "back") {
const picked = drawings.filter((d) => selected.has(d.id));
const rest = drawings.filter((d) => !selected.has(d.id));
return op === "front" ? [...rest, ...picked] : [...picked, ...rest];
}
// "forward" / "backward": jedes gewählte Element um genau eine Position
// tauschen — Iterationsrichtung verhindert, dass mehrfach-selektierte
// Nachbarn sich gegenseitig überholen.
const arr = drawings.slice();
if (op === "forward") {
for (let i = arr.length - 2; i >= 0; i--) {
if (selected.has(arr[i].id) && !selected.has(arr[i + 1].id)) {
[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
}
}
} else {
for (let i = 1; i < arr.length; i++) {
if (selected.has(arr[i].id) && !selected.has(arr[i - 1].id)) {
[arr[i], arr[i - 1]] = [arr[i - 1], arr[i]];
}
}
}
return arr;
}
function mapCeiling(
project: Project,
ceilingId: string,
+86
View File
@@ -0,0 +1,86 @@
/**
* Z-Anordnen von 2D-Zeichnungselementen (`reorderDrawings` in
* `src/state/projectSlice.ts`). Die Array-Reihenfolge in `drawings2d` IST die
* Zeichenreihenfolge im Grundriss (spätere Position = weiter oben, siehe
* `generatePlan.ts`) — „front"/„back"/„forward"/„backward" sortieren das
* Array um.
*/
import { describe, it, expect, beforeEach } from "vitest";
import { getState, setState } from "./appStore";
import { sampleProject } from "../model/sampleProject";
import type { Drawing2D } from "../model/types";
function rect(id: string): Drawing2D {
return {
id,
type: "drawing2d",
levelId: "eg",
categoryCode: "A",
geom: { shape: "rect", min: { x: 0, y: 0 }, max: { x: 1, y: 1 } },
};
}
beforeEach(() => {
setState({
project: {
...sampleProject,
drawings2d: [rect("A"), rect("B"), rect("C"), rect("D")],
},
undoStack: [],
redoStack: [],
canUndo: false,
canRedo: false,
});
});
function ids(): string[] {
return getState().project.drawings2d.map((d) => d.id);
}
describe("reorderDrawings", () => {
it("front: schiebt die Auswahl ans Ende, Reihenfolge untereinander bleibt", () => {
getState().reorderDrawings(["B", "A"], "front");
// Auswahl-Reihenfolge im Array bleibt A vor B (Array-Reihenfolge zählt,
// nicht die Aufruf-Reihenfolge der ids).
expect(ids()).toEqual(["C", "D", "A", "B"]);
});
it("back: schiebt die Auswahl an den Anfang", () => {
getState().reorderDrawings(["C"], "back");
expect(ids()).toEqual(["C", "A", "B", "D"]);
});
it("forward: contiguous block überholt genau einen Nachbarn", () => {
getState().reorderDrawings(["B", "C"], "forward");
expect(ids()).toEqual(["A", "D", "B", "C"]);
});
it("backward: contiguous block überholt genau einen Nachbarn rückwärts", () => {
getState().reorderDrawings(["B", "C"], "backward");
expect(ids()).toEqual(["B", "C", "A", "D"]);
});
it("forward: letztes Element bleibt am Ende (kein Überlauf)", () => {
getState().reorderDrawings(["D"], "forward");
expect(ids()).toEqual(["A", "B", "C", "D"]);
});
it("backward: erstes Element bleibt am Anfang (kein Überlauf)", () => {
getState().reorderDrawings(["A"], "backward");
expect(ids()).toEqual(["A", "B", "C", "D"]);
});
it("unbekannte IDs werden ignoriert", () => {
getState().reorderDrawings(["nope"], "front");
expect(ids()).toEqual(["A", "B", "C", "D"]);
});
it("ist ein normaler Undo-Schritt", () => {
getState().reorderDrawings(["A"], "front");
expect(ids()).toEqual(["B", "C", "D", "A"]);
expect(getState().canUndo).toBe(true);
getState().undo();
expect(ids()).toEqual(["A", "B", "C", "D"]);
});
});