diff --git a/src/App.tsx b/src/App.tsx
index c5255d2..13cd5ec 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1820,6 +1820,7 @@ export default function App() {
const setElementForeground = useStore((s) => s.setElementForeground);
const setElementBackground = useStore((s) => s.setElementBackground);
const updateDrawing2D = useStore((s) => s.updateDrawing2D);
+ const reorderDrawings = useStore((s) => s.reorderDrawings);
const resizeElement = useStore((s) => s.resizeElement);
const updateWall = useStore((s) => s.updateWall);
const setWallThickness = useStore((s) => s.setWallThickness);
@@ -3475,6 +3476,11 @@ export default function App() {
union: () => applyBoolean(unionDrawings),
difference: () => applyBoolean(differenceDrawings),
intersect: () => applyBoolean(intersectionDrawings),
+ drawingSelectionCount: selectedDrawingIds.length,
+ bringToFront: () => reorderDrawings(selectedDrawingIds, "front"),
+ bringForward: () => reorderDrawings(selectedDrawingIds, "forward"),
+ sendBackward: () => reorderDrawings(selectedDrawingIds, "backward"),
+ sendToBack: () => reorderDrawings(selectedDrawingIds, "back"),
})}
onClose={() => setMenu(null)}
/>
@@ -3734,6 +3740,12 @@ interface MenuActions {
union: () => void;
difference: () => void;
intersect: () => void;
+ /** Anzahl aktuell selektierter 2D-Zeichnungselemente (z-Anordnen-Menü). */
+ drawingSelectionCount: number;
+ bringToFront: () => void;
+ bringForward: () => void;
+ sendBackward: () => void;
+ sendToBack: () => void;
}
/**
@@ -3887,6 +3899,42 @@ function planMenuItems(
},
];
+ // Z-Anordnen (nach vorn/hinten holen) — wirkt auf die aktuelle 2D-Auswahl,
+ // sortiert `drawings2d` um (Array-Reihenfolge = Zeichenreihenfolge). Aktiv
+ // ab 1 selektiertem Element; sonst deaktiviert mit erklärendem Tooltip.
+ const arrangeEnabled = a.drawingSelectionCount >= 1;
+ const arrangeTitle = arrangeEnabled ? undefined : t("ctx.arrange.disabled");
+ const arrangeItems: ContextMenuItem[] = [
+ {
+ label: t("ctx.arrange.front"),
+ icon: ,
+ disabled: !arrangeEnabled,
+ title: arrangeTitle,
+ onClick: a.bringToFront,
+ },
+ {
+ label: t("ctx.arrange.forward"),
+ icon: ,
+ disabled: !arrangeEnabled,
+ title: arrangeTitle,
+ onClick: a.bringForward,
+ },
+ {
+ label: t("ctx.arrange.backward"),
+ icon: ,
+ disabled: !arrangeEnabled,
+ title: arrangeTitle,
+ onClick: a.sendBackward,
+ },
+ {
+ label: t("ctx.arrange.back"),
+ icon: ,
+ disabled: !arrangeEnabled,
+ title: arrangeTitle,
+ onClick: a.sendToBack,
+ },
+ ];
+
if (wallId) {
return [
{
@@ -3902,6 +3950,8 @@ function planMenuItems(
{ divider: true },
...booleanItems,
{ divider: true },
+ ...arrangeItems,
+ { divider: true },
{
label: t("ctx.fit"),
icon: ,
@@ -3912,6 +3962,8 @@ function planMenuItems(
return [
...booleanItems,
{ divider: true },
+ ...arrangeItems,
+ { divider: true },
{
label: t("ctx.fit"),
icon: ,
diff --git a/src/i18n/de.ts b/src/i18n/de.ts
index 3b2a038..eb37a8a 100644
--- a/src/i18n/de.ts
+++ b/src/i18n/de.ts
@@ -390,6 +390,11 @@ export const de = {
"ctx.intersect": "Schneiden",
"ctx.boolean.disabled":
"Mindestens zwei geschlossene Flächen auswählen (Rechteck oder geschlossene Polylinie).",
+ "ctx.arrange.front": "Ganz nach vorn",
+ "ctx.arrange.forward": "Nach vorn",
+ "ctx.arrange.backward": "Nach hinten",
+ "ctx.arrange.back": "Ganz nach hinten",
+ "ctx.arrange.disabled": "Mindestens ein 2D-Element auswählen.",
"ctx.noWallHit": "Keine Wand getroffen",
"ctx.noWallHit.hint": "Rechtsklick auf eine Wand wählt sie und zeigt Wand-Aktionen.",
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 496b2f6..aed937c 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -388,6 +388,11 @@ export const en: Record = {
"ctx.intersect": "Intersect",
"ctx.boolean.disabled":
"Select at least two closed areas (rectangle or closed polyline).",
+ "ctx.arrange.front": "Bring to front",
+ "ctx.arrange.forward": "Bring forward",
+ "ctx.arrange.backward": "Send backward",
+ "ctx.arrange.back": "Send to back",
+ "ctx.arrange.disabled": "Select at least one 2D element.",
"ctx.noWallHit": "No wall hit",
"ctx.noWallHit.hint": "Right-clicking a wall selects it and shows wall actions.",
diff --git a/src/state/projectSlice.ts b/src/state/projectSlice.ts
index 71f1b7d..4ac186b 100644
--- a/src/state/projectSlice.ts
+++ b/src/state/projectSlice.ts
@@ -162,6 +162,18 @@ export interface ProjectSlice {
* `hatchSource`), die kein eigenes Setter-Paar haben.
*/
updateDrawing2D: (id: string, patch: Partial) => 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,
diff --git a/src/state/reorderDrawings.test.ts b/src/state/reorderDrawings.test.ts
new file mode 100644
index 0000000..3fc9ffc
--- /dev/null
+++ b/src/state/reorderDrawings.test.ts
@@ -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"]);
+ });
+});