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:
+52
@@ -1820,6 +1820,7 @@ export default function App() {
|
|||||||
const setElementForeground = useStore((s) => s.setElementForeground);
|
const setElementForeground = useStore((s) => s.setElementForeground);
|
||||||
const setElementBackground = useStore((s) => s.setElementBackground);
|
const setElementBackground = useStore((s) => s.setElementBackground);
|
||||||
const updateDrawing2D = useStore((s) => s.updateDrawing2D);
|
const updateDrawing2D = useStore((s) => s.updateDrawing2D);
|
||||||
|
const reorderDrawings = useStore((s) => s.reorderDrawings);
|
||||||
const resizeElement = useStore((s) => s.resizeElement);
|
const resizeElement = useStore((s) => s.resizeElement);
|
||||||
const updateWall = useStore((s) => s.updateWall);
|
const updateWall = useStore((s) => s.updateWall);
|
||||||
const setWallThickness = useStore((s) => s.setWallThickness);
|
const setWallThickness = useStore((s) => s.setWallThickness);
|
||||||
@@ -3475,6 +3476,11 @@ export default function App() {
|
|||||||
union: () => applyBoolean(unionDrawings),
|
union: () => applyBoolean(unionDrawings),
|
||||||
difference: () => applyBoolean(differenceDrawings),
|
difference: () => applyBoolean(differenceDrawings),
|
||||||
intersect: () => applyBoolean(intersectionDrawings),
|
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)}
|
onClose={() => setMenu(null)}
|
||||||
/>
|
/>
|
||||||
@@ -3734,6 +3740,12 @@ interface MenuActions {
|
|||||||
union: () => void;
|
union: () => void;
|
||||||
difference: () => void;
|
difference: () => void;
|
||||||
intersect: () => 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: <MenuIcon name="flip_to_front" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.bringToFront,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.forward"),
|
||||||
|
icon: <MenuIcon name="arrow_upward" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.bringForward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.backward"),
|
||||||
|
icon: <MenuIcon name="arrow_downward" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.sendBackward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("ctx.arrange.back"),
|
||||||
|
icon: <MenuIcon name="flip_to_back" />,
|
||||||
|
disabled: !arrangeEnabled,
|
||||||
|
title: arrangeTitle,
|
||||||
|
onClick: a.sendToBack,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (wallId) {
|
if (wallId) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -3902,6 +3950,8 @@ function planMenuItems(
|
|||||||
{ divider: true },
|
{ divider: true },
|
||||||
...booleanItems,
|
...booleanItems,
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
|
...arrangeItems,
|
||||||
|
{ divider: true },
|
||||||
{
|
{
|
||||||
label: t("ctx.fit"),
|
label: t("ctx.fit"),
|
||||||
icon: <MenuIcon name="fit_screen" />,
|
icon: <MenuIcon name="fit_screen" />,
|
||||||
@@ -3912,6 +3962,8 @@ function planMenuItems(
|
|||||||
return [
|
return [
|
||||||
...booleanItems,
|
...booleanItems,
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
|
...arrangeItems,
|
||||||
|
{ divider: true },
|
||||||
{
|
{
|
||||||
label: t("ctx.fit"),
|
label: t("ctx.fit"),
|
||||||
icon: <MenuIcon name="fit_screen" />,
|
icon: <MenuIcon name="fit_screen" />,
|
||||||
|
|||||||
@@ -390,6 +390,11 @@ export const de = {
|
|||||||
"ctx.intersect": "Schneiden",
|
"ctx.intersect": "Schneiden",
|
||||||
"ctx.boolean.disabled":
|
"ctx.boolean.disabled":
|
||||||
"Mindestens zwei geschlossene Flächen auswählen (Rechteck oder geschlossene Polylinie).",
|
"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": "Keine Wand getroffen",
|
||||||
"ctx.noWallHit.hint": "Rechtsklick auf eine Wand wählt sie und zeigt Wand-Aktionen.",
|
"ctx.noWallHit.hint": "Rechtsklick auf eine Wand wählt sie und zeigt Wand-Aktionen.",
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,11 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"ctx.intersect": "Intersect",
|
"ctx.intersect": "Intersect",
|
||||||
"ctx.boolean.disabled":
|
"ctx.boolean.disabled":
|
||||||
"Select at least two closed areas (rectangle or closed polyline).",
|
"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": "No wall hit",
|
||||||
"ctx.noWallHit.hint": "Right-clicking a wall selects it and shows wall actions.",
|
"ctx.noWallHit.hint": "Right-clicking a wall selects it and shows wall actions.",
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,18 @@ export interface ProjectSlice {
|
|||||||
* `hatchSource`), die kein eigenes Setter-Paar haben.
|
* `hatchSource`), die kein eigenes Setter-Paar haben.
|
||||||
*/
|
*/
|
||||||
updateDrawing2D: (id: string, patch: Partial<Drawing2D>) => void;
|
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
|
* Skaliert die Geometrie eines Elements auf Zielbreite×-höhe (Meter) um einen
|
||||||
* Ankerpunkt (fx/fy ∈ [0,1] relativ zur bbox; 0,0 = oben-links).
|
* 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)),
|
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) ────────────────────────────────────────────
|
// ── Größe (Resize um Anker) ────────────────────────────────────────────
|
||||||
resizeElement: (kind, id, w, h, anchor) =>
|
resizeElement: (kind, id, w, h, anchor) =>
|
||||||
setProject((p) => resizeElement(p, 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) };
|
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(
|
function mapCeiling(
|
||||||
project: Project,
|
project: Project,
|
||||||
ceilingId: string,
|
ceilingId: string,
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user