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:
2026-07-10 01:02:47 +02:00
parent 9b6dd80d39
commit 4ef40a02c4
7 changed files with 284 additions and 21 deletions
+46
View File
@@ -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) =>