Dach-3D-Griffe: Eckpunkt-Resize + Verschieben + First-Griff für Neigung
Gewählte Dächer bekommen im wgpu-Viewport Ziehgriffe wie Wände/Decken: Eckpunkte (achsparalleles Resize, Gegenecke fix), Verschiebe-Griff im Schwerpunkt und ein First-Griff am höchsten Dachpunkt — vertikales Ziehen rechnet aus der neuen First-Höhe die Dachneigung (behält die Firstlage). - projectSlice: moveRoofGrip/moveRoofBy (coalescing pro Dach) - Wasm3DViewport: roofpitch-Griff (Render + vertikaler Drag auf Apex-Achse) - App: edit3dRoofs + roofId-Routing in onEdit3dVertex/Body + onEdit3dRoofPitch - Viewport3D: editRoofs/onEditRoofPitch durchgereicht - Tests: roofGrips.test.ts (Resize/Verschieben/Coalescing)
This commit is contained in:
@@ -240,6 +240,10 @@ export interface ProjectSlice {
|
||||
id: string,
|
||||
patch: Partial<import("../model/types").Roof>,
|
||||
) => void;
|
||||
/** Dach-Eckgriff (3D): Umriss-Ecke `index` auf `pt`, Gegenecke fix (Rechteck). */
|
||||
moveRoofGrip: (roofId: string, index: number, pt: import("../model/types").Vec2) => void;
|
||||
/** Dach als Ganzes um `delta` verschieben (Verschiebe-Griff). */
|
||||
moveRoofBy: (roofId: string, delta: import("../model/types").Vec2) => void;
|
||||
|
||||
// ── Treppen-Editieren ──────────────────────────────────────────────────────
|
||||
/** Verschiebt eine Treppe (Antritt) um `delta`. */
|
||||
@@ -926,6 +930,48 @@ export function createProjectSlice(
|
||||
roofs: (p.roofs ?? []).map((r) => (r.id === id ? { ...r, ...patch } : r)),
|
||||
})),
|
||||
|
||||
// Dach-Eckgriff (3D): der gezogene Umriss-Eckpunkt `index` wandert auf `pt`;
|
||||
// die GEGENÜBERLIEGENDE Ecke bleibt fix → der Umriss bleibt ein achsparalleles
|
||||
// Rechteck (die Dach-Geometrie rechnet ohnehin auf der Bounding-Box).
|
||||
moveRoofGrip: (roofId, index, pt) =>
|
||||
setProject(
|
||||
(p) => ({
|
||||
...p,
|
||||
roofs: (p.roofs ?? []).map((r) => {
|
||||
if (r.id !== roofId || r.outline.length < 4) return r;
|
||||
const opp = r.outline[(index + 2) % r.outline.length];
|
||||
const x0 = Math.min(pt.x, opp.x);
|
||||
const x1 = Math.max(pt.x, opp.x);
|
||||
const y0 = Math.min(pt.y, opp.y);
|
||||
const y1 = Math.max(pt.y, opp.y);
|
||||
return {
|
||||
...r,
|
||||
outline: [
|
||||
{ x: x0, y: y0 },
|
||||
{ x: x1, y: y0 },
|
||||
{ x: x1, y: y1 },
|
||||
{ x: x0, y: y1 },
|
||||
],
|
||||
};
|
||||
}),
|
||||
}),
|
||||
`moveRoofGrip:${roofId}`,
|
||||
),
|
||||
|
||||
// Dach als Ganzes verschieben (Verschiebe-Griff).
|
||||
moveRoofBy: (roofId, delta) =>
|
||||
setProject(
|
||||
(p) => ({
|
||||
...p,
|
||||
roofs: (p.roofs ?? []).map((r) =>
|
||||
r.id === roofId
|
||||
? { ...r, outline: r.outline.map((v) => ({ x: v.x + delta.x, y: v.y + delta.y })) }
|
||||
: r,
|
||||
),
|
||||
}),
|
||||
`moveRoofBy:${roofId}`,
|
||||
),
|
||||
|
||||
// ── Treppen-Editieren ──────────────────────────────────────────────────
|
||||
// (coalesceKey: siehe Kommentar bei moveGripOf.)
|
||||
moveStairBy: (stairId, delta) =>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Dach-3D-Griffe: Store-Mutationen moveRoofGrip (Eckpunkt-Resize, gegenüber-
|
||||
// liegende Ecke bleibt fix) und moveRoofBy (Verschieben des ganzen Umrisses).
|
||||
// Beide werden bei jedem Maus-Move-Schritt aufgerufen und müssen zu EINEM
|
||||
// Undo-Schritt koaleszieren (coalesceKey pro Dach).
|
||||
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { getState, setState } from "./appStore";
|
||||
import type { Project, Roof } from "../model/types";
|
||||
|
||||
const roof: Roof = {
|
||||
id: "RF1",
|
||||
type: "roof",
|
||||
floorId: "eg",
|
||||
categoryCode: "31",
|
||||
outline: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 6, y: 0 },
|
||||
{ x: 6, y: 4 },
|
||||
{ x: 0, y: 4 },
|
||||
],
|
||||
shape: "sattel",
|
||||
pitchDeg: 45,
|
||||
overhang: 0,
|
||||
ridgeAxis: "x",
|
||||
thickness: 0.2,
|
||||
};
|
||||
|
||||
const baseProject = {
|
||||
id: "p",
|
||||
name: "T",
|
||||
layers: [],
|
||||
drawingLevels: [
|
||||
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, baseElevation: 0 },
|
||||
],
|
||||
walls: [],
|
||||
drawings2d: [],
|
||||
roofs: [roof],
|
||||
} as unknown as Project;
|
||||
|
||||
beforeEach(() => {
|
||||
setState({
|
||||
project: JSON.parse(JSON.stringify(baseProject)),
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
canUndo: false,
|
||||
canRedo: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveRoofGrip — Eckpunkt-Resize", () => {
|
||||
it("zieht Ecke 0 und hält die gegenüberliegende Ecke (2) fix", () => {
|
||||
// Ecke 0 (0,0) auf (-1,-1) ziehen; Gegenecke 2 = (6,4) bleibt.
|
||||
getState().moveRoofGrip("RF1", 0, { x: -1, y: -1 });
|
||||
const r = getState().project.roofs![0];
|
||||
// Umriss neu als achsparallles Rechteck [minX,minY]..[maxX,maxY].
|
||||
expect(r.outline).toEqual([
|
||||
{ x: -1, y: -1 },
|
||||
{ x: 6, y: -1 },
|
||||
{ x: 6, y: 4 },
|
||||
{ x: -1, y: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("hochfrequente Drags koaleszieren zu EINEM Undo-Schritt", () => {
|
||||
// Saubere Koaleszenz-Grenze: eine diskrete (keyless) Aktion setzt
|
||||
// lastCoalesceKey zurück, damit der erste Drag garantiert pusht (der
|
||||
// Coalesce-Zustand ist ein Modul-Closure und überlebt sonst zwischen Tests).
|
||||
getState().updateRoof("RF1", { overhang: 0.1 });
|
||||
setState({ undoStack: [], canUndo: false });
|
||||
getState().moveRoofGrip("RF1", 2, { x: 5, y: 3 });
|
||||
getState().moveRoofGrip("RF1", 2, { x: 5.5, y: 3.5 });
|
||||
getState().moveRoofGrip("RF1", 2, { x: 7, y: 5 });
|
||||
expect(getState().undoStack.length).toBe(1);
|
||||
getState().undo();
|
||||
// Nach einem Undo wieder der Ausgangsumriss.
|
||||
expect(getState().project.roofs![0].outline).toEqual(baseProject.roofs![0].outline);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveRoofBy — Verschieben", () => {
|
||||
it("verschiebt alle Eckpunkte um delta", () => {
|
||||
getState().moveRoofBy("RF1", { x: 2, y: -1 });
|
||||
const r = getState().project.roofs![0];
|
||||
expect(r.outline).toEqual([
|
||||
{ x: 2, y: -1 },
|
||||
{ x: 8, y: -1 },
|
||||
{ x: 8, y: 3 },
|
||||
{ x: 2, y: 3 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user