Decken-Griffe im 3D: Eckpunkte + Kanten-Mittelpunkte (wgpu-Viewport)

Gewählte Decken bekommen im wgpu-3D-Viewport editierbare Griffe — analog
Wand/2D-Element:
- Eckpunkt-Griffe (Umriss-Vertices) → moveCeilingGrip.
- Je Seite ein Kanten-Mittelpunkt-Griff (rautenförmig, eigene Farbe) →
  moveCeilingEdge (Nutzer-Wunsch „an allen seiten auch ein punkt").
- Verschiebe-Griff im Schwerpunkt → moveCeilingBy.
Alle drei Store-Actions existierten bereits (2D-Griffe); neu ist nur die
3D-Griffgeometrie (computeGrips), das Kanten-Drag (GripDrag „edge") und die
Prop-Verdrahtung Wasm3DViewport ← Viewport3D ← App (editCeilings/onEditEdge,
Griff-Ziel um ceilingId erweitert). Die three.js-Sicht bleibt unverändert
(Default ist wgpu). tsc + vitest 620 grün.
This commit is contained in:
2026-07-09 01:39:17 +02:00
parent d0b9d22141
commit 973ac6d04f
5 changed files with 164 additions and 36 deletions
+38 -6
View File
@@ -27,6 +27,7 @@ import {
import { wallCorners } from "./model/geometry"; import { wallCorners } from "./model/geometry";
import { add, dot, leftNormal, normalize, scale, sub } from "./model/geometry"; import { add, dot, leftNormal, normalize, scale, sub } from "./model/geometry";
import type { import type {
Ceiling,
CeilingType, CeilingType,
ContextObject, ContextObject,
DoorType, DoorType,
@@ -2458,6 +2459,17 @@ export default function App() {
: [], : [],
[selectedWallIds, selectedDrawingIds, project.drawings2d], [selectedWallIds, selectedDrawingIds, project.drawings2d],
); );
// Gewählte Decken → 3D-Eckpunkt-/Kanten-/Verschiebe-Griffe. Nur wenn keine
// Wände/2D-Elemente mitselektiert sind (eindeutiger Editierfokus).
const edit3dCeilings = useMemo(
() =>
selectedWallIds.length === 0 && selectedDrawingIds.length === 0
? selectedCeilingIds
.map((id) => (project.ceilings ?? []).find((c) => c.id === id))
.filter((c): c is Ceiling => !!c)
: [],
[selectedWallIds, selectedDrawingIds, selectedCeilingIds, project.ceilings],
);
// Snap beim 3D-Vertex-Drag: dieselbe computeSnap-Logik wie die Plan-Griffe. // Snap beim 3D-Vertex-Drag: dieselbe computeSnap-Logik wie die Plan-Griffe.
// Das/die gerade GEZOGENEN Element(e) werden aus den Snap-Quellen genommen // Das/die gerade GEZOGENEN Element(e) werden aus den Snap-Quellen genommen
// (sonst fängt der Punkt an sich selbst). `excludeWallIds`/`excludeDrawingIds` // (sonst fängt der Punkt an sich selbst). `excludeWallIds`/`excludeDrawingIds`
@@ -2494,7 +2506,7 @@ export default function App() {
// Snap-Marker zu zeigen. `snapped=false` (für mitgeführte Geschwister) wendet // Snap-Marker zu zeigen. `snapped=false` (für mitgeführte Geschwister) wendet
// den Punkt direkt an, ohne erneut zu snappen. // den Punkt direkt an, ohne erneut zu snappen.
const onEdit3dVertex = ( const onEdit3dVertex = (
target: { wallId?: string; drawingId?: string }, target: { wallId?: string; drawingId?: string; ceilingId?: string },
index: number, index: number,
model: Vec2, model: Vec2,
excludeWallIds?: Set<string>, excludeWallIds?: Set<string>,
@@ -2509,11 +2521,19 @@ export default function App() {
); );
pt = snapResult?.point ?? model; pt = snapResult?.point ?? model;
} }
moveGripOf(target.drawingId ?? null, target.wallId ?? null, index, pt); if (target.ceilingId) moveCeilingGrip(target.ceilingId, index, pt);
else moveGripOf(target.drawingId ?? null, target.wallId ?? null, index, pt);
return pt; return pt;
}; };
const onEdit3dBody = (target: { wallId?: string; drawingId?: string }, delta: Vec2) => { const onEdit3dBody = (
moveElementByOf(target.drawingId ?? null, target.wallId ?? null, delta); target: { wallId?: string; drawingId?: string; ceilingId?: string },
delta: Vec2,
) => {
if (target.ceilingId) moveCeilingBy(target.ceilingId, delta);
else moveElementByOf(target.drawingId ?? null, target.wallId ?? null, delta);
};
const onEdit3dEdge = (ceilingId: string, aIndex: number, bIndex: number, delta: Vec2) => {
moveCeilingEdge(ceilingId, aIndex, bIndex, delta);
}; };
const onEdit3dWallTop = (wallId: string, topZ: number) => { const onEdit3dWallTop = (wallId: string, topZ: number) => {
updateWall(wallId, { top: { mode: "custom", z: topZ } }); updateWall(wallId, { top: { mode: "custom", z: topZ } });
@@ -4731,8 +4751,10 @@ export default function App() {
gripHandlers={gripHandlers} gripHandlers={gripHandlers}
edit3dWalls={edit3dWalls} edit3dWalls={edit3dWalls}
edit3dDrawings={edit3dDrawings} edit3dDrawings={edit3dDrawings}
edit3dCeilings={edit3dCeilings}
onEdit3dVertex={onEdit3dVertex} onEdit3dVertex={onEdit3dVertex}
onEdit3dBody={onEdit3dBody} onEdit3dBody={onEdit3dBody}
onEdit3dEdge={onEdit3dEdge}
onEdit3dWallTop={onEdit3dWallTop} onEdit3dWallTop={onEdit3dWallTop}
onEdit3dEnd={onEdit3dEnd} onEdit3dEnd={onEdit3dEnd}
/> />
@@ -5839,8 +5861,10 @@ function Content({
gripHandlers, gripHandlers,
edit3dWalls, edit3dWalls,
edit3dDrawings, edit3dDrawings,
edit3dCeilings,
onEdit3dVertex, onEdit3dVertex,
onEdit3dBody, onEdit3dBody,
onEdit3dEdge,
onEdit3dWallTop, onEdit3dWallTop,
onEdit3dEnd, onEdit3dEnd,
}: { }: {
@@ -5923,14 +5947,20 @@ function Content({
edit3dWalls: Wall[]; edit3dWalls: Wall[];
/** Alle gewählten 2D-Elemente — 3D-Vertex-Griffe (Mehrfachauswahl ⇒ alle). */ /** Alle gewählten 2D-Elemente — 3D-Vertex-Griffe (Mehrfachauswahl ⇒ alle). */
edit3dDrawings: Drawing2D[]; edit3dDrawings: Drawing2D[];
/** Alle gewählten Decken — 3D-Eckpunkt-/Kanten-/Verschiebe-Griffe. */
edit3dCeilings: Ceiling[];
onEdit3dVertex: ( onEdit3dVertex: (
target: { wallId?: string; drawingId?: string }, target: { wallId?: string; drawingId?: string; ceilingId?: string },
index: number, index: number,
model: Vec2, model: Vec2,
excludeWallIds?: Set<string>, excludeWallIds?: Set<string>,
excludeDrawingIds?: Set<string>, excludeDrawingIds?: Set<string>,
) => Vec2; ) => Vec2;
onEdit3dBody: (target: { wallId?: string; drawingId?: string }, delta: Vec2) => void; onEdit3dBody: (
target: { wallId?: string; drawingId?: string; ceilingId?: string },
delta: Vec2,
) => void;
onEdit3dEdge: (ceilingId: string, aIndex: number, bIndex: number, delta: Vec2) => void;
onEdit3dWallTop: (wallId: string, topZ: number) => void; onEdit3dWallTop: (wallId: string, topZ: number) => void;
onEdit3dEnd: () => void; onEdit3dEnd: () => void;
}) { }) {
@@ -6104,8 +6134,10 @@ function Content({
onWorkplaneConfirm={onWorkplaneConfirm} onWorkplaneConfirm={onWorkplaneConfirm}
editWalls={edit3dWalls} editWalls={edit3dWalls}
editDrawings={edit3dDrawings} editDrawings={edit3dDrawings}
editCeilings={edit3dCeilings}
onEditVertex={onEdit3dVertex} onEditVertex={onEdit3dVertex}
onEditBody={onEdit3dBody} onEditBody={onEdit3dBody}
onEditEdge={onEdit3dEdge}
onEditWallTop={onEdit3dWallTop} onEditWallTop={onEdit3dWallTop}
onEditEnd={onEdit3dEnd} onEditEnd={onEdit3dEnd}
/> />
+1
View File
@@ -1209,6 +1209,7 @@ export const de = {
"viewport3d.grip.vertex": "Wandende ziehen", "viewport3d.grip.vertex": "Wandende ziehen",
"viewport3d.grip.height": "Höhe ziehen", "viewport3d.grip.height": "Höhe ziehen",
"viewport3d.grip.move": "Verschieben", "viewport3d.grip.move": "Verschieben",
"viewport3d.grip.edge": "Kante ziehen",
// ── Element-Baum (Elemente-Panel, ROADMAP §11) ────────────────────────────── // ── Element-Baum (Elemente-Panel, ROADMAP §11) ──────────────────────────────
"elements.title": "Elemente", "elements.title": "Elemente",
+1
View File
@@ -1195,6 +1195,7 @@ export const en: Record<TranslationKey, string> = {
"viewport3d.section.show": "Section plane on", "viewport3d.section.show": "Section plane on",
"viewport3d.section.hide": "Section plane off", "viewport3d.section.hide": "Section plane off",
"viewport3d.grip.vertex": "Drag wall endpoint", "viewport3d.grip.vertex": "Drag wall endpoint",
"viewport3d.grip.edge": "Drag edge",
"viewport3d.grip.height": "Drag height", "viewport3d.grip.height": "Drag height",
"viewport3d.grip.move": "Move", "viewport3d.grip.move": "Move",
+13 -5
View File
@@ -152,8 +152,10 @@ export function Viewport3D(
// wie die three.js-Sicht (renderer-agnostisch). // wie die three.js-Sicht (renderer-agnostisch).
editWalls={props.editWalls} editWalls={props.editWalls}
editDrawings={props.editDrawings} editDrawings={props.editDrawings}
editCeilings={props.editCeilings}
onEditVertex={props.onEditVertex} onEditVertex={props.onEditVertex}
onEditBody={props.onEditBody} onEditBody={props.onEditBody}
onEditEdge={props.onEditEdge}
onEditWallTop={props.onEditWallTop} onEditWallTop={props.onEditWallTop}
onEditEnd={props.onEditEnd} onEditEnd={props.onEditEnd}
/> />
@@ -276,23 +278,29 @@ function ThreeViewport3D({
editWalls?: Wall[]; editWalls?: Wall[];
/** Alle gewählten 2D-Elemente — Vertex-Griffe in 3D (Mehrfachauswahl ⇒ alle). */ /** Alle gewählten 2D-Elemente — Vertex-Griffe in 3D (Mehrfachauswahl ⇒ alle). */
editDrawings?: Drawing2D[]; editDrawings?: Drawing2D[];
/** Alle gewählten Decken — Eckpunkt-/Kanten-/Verschiebe-Griffe (nur wgpu-Sicht). */
editCeilings?: Ceiling[];
/** /**
* Endpunkt-/Vertex-Griff gezogen: neuer XY-Modellpunkt für den Vertex `index` * Endpunkt-/Vertex-Griff gezogen: neuer XY-Modellpunkt für den Vertex `index`
* des durch `target` benannten Elements (Wand-ID ODER Drawing-ID). Optional * des durch `target` benannten Elements (Wand-ID, Drawing-ID ODER Decken-ID).
* `excludeWallIds`/`excludeDrawingIds` = Koinzidenz-Gruppe, die aus den * Optional `excludeWallIds`/`excludeDrawingIds` = Koinzidenz-Gruppe, die aus den
* Snap-Quellen genommen wird; nur dann snappt App den Punkt. Liefert den * Snap-Quellen genommen wird; nur dann snappt App den Punkt. Liefert den
* effektiv angewandten (gesnappten) Punkt zurück (für Marker + Mitführen). * effektiv angewandten (gesnappten) Punkt zurück (für Marker + Mitführen).
* Wird auf dieselbe Store-Action (`moveGripOf`) gelegt wie die Plan-Griffe.
*/ */
onEditVertex?: ( onEditVertex?: (
target: { wallId?: string; drawingId?: string }, target: { wallId?: string; drawingId?: string; ceilingId?: string },
index: number, index: number,
model: Vec2, model: Vec2,
excludeWallIds?: Set<string>, excludeWallIds?: Set<string>,
excludeDrawingIds?: Set<string>, excludeDrawingIds?: Set<string>,
) => Vec2; ) => Vec2;
/** Verschiebe-Griff gezogen: das `target`-Element um `delta` (XY) verschieben. */ /** Verschiebe-Griff gezogen: das `target`-Element um `delta` (XY) verschieben. */
onEditBody?: (target: { wallId?: string; drawingId?: string }, delta: Vec2) => void; onEditBody?: (
target: { wallId?: string; drawingId?: string; ceilingId?: string },
delta: Vec2,
) => void;
/** Kanten-Griff einer Decke gezogen: Kante (aIndex→bIndex) um `delta` verschieben. */
onEditEdge?: (ceilingId: string, aIndex: number, bIndex: number, delta: Vec2) => void;
/** Höhen-Griff gezogen: neue absolute Oberkanten-Z der Wand. */ /** Höhen-Griff gezogen: neue absolute Oberkanten-Z der Wand. */
onEditWallTop?: (wallId: string, topZ: number) => void; onEditWallTop?: (wallId: string, topZ: number) => void;
/** Ende eines Griff-Drags (Commit/Aufräumen). */ /** Ende eines Griff-Drags (Commit/Aufräumen). */
+111 -25
View File
@@ -43,8 +43,8 @@
// Pixel an der Viewport-Höhe normiert), damit sich beide Pfade gleich anfühlen. // Pixel an der Viewport-Höhe normiert), damit sich beide Pfade gleich anfühlen.
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { Project, Wall, Drawing2D, Vec2 } from "../model/types"; import type { Project, Wall, Drawing2D, Ceiling, Vec2 } from "../model/types";
import { wallVerticalExtent } from "../model/wall"; import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
import { projectToModel3d, pickGeometry, TRUCK_MESH_READY_EVENT } from "../plan/toWalls3d"; import { projectToModel3d, pickGeometry, TRUCK_MESH_READY_EVENT } from "../plan/toWalls3d";
import type { RenderMode, View3d } from "../ui/TopBar"; import type { RenderMode, View3d } from "../ui/TopBar";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -82,13 +82,15 @@ const CLICK_THRESHOLD_PX = 4;
// GUI-Element aus. Kosten: die Bildschirm-Position muss der Kamera folgen — // GUI-Element aus. Kosten: die Bildschirm-Position muss der Kamera folgen —
// `useEffect` unten pollt sie per `requestAnimationFrame`, solange Griffe // `useEffect` unten pollt sie per `requestAnimationFrame`, solange Griffe
// aktiv sind (billige 2D-Projektion, kein WASM/GPU-Aufruf). // aktiv sind (billige 2D-Projektion, kein WASM/GPU-Aufruf).
type GripKind = "vertex" | "height" | "move"; type GripKind = "vertex" | "height" | "move" | "edge";
interface Grip { interface Grip {
kind: GripKind; kind: GripKind;
/** Element, dessen Griff das ist (Wand-ID ODER Drawing-ID). */ /** Element, dessen Griff das ist (Wand-ID, Drawing-ID ODER Decken-ID). */
target: { wallId?: string; drawingId?: string }; target: { wallId?: string; drawingId?: string; ceilingId?: string };
/** Vertex-Index (nur `kind==="vertex"`); -1 sonst. */ /** Vertex-Index (`vertex`) bzw. erster Kanten-Index (`edge`); -1 sonst. */
index: number; index: number;
/** Zweiter Kanten-Index (nur `kind==="edge"`). */
index2?: number;
pos: Vec3; pos: Vec3;
} }
@@ -103,6 +105,7 @@ function computeGrips(
project: Project, project: Project,
walls: readonly Wall[], walls: readonly Wall[],
drawings: readonly Drawing2D[], drawings: readonly Drawing2D[],
ceilings: readonly Ceiling[],
drawingElevation: number, drawingElevation: number,
): Grip[] { ): Grip[] {
const out: Grip[] = []; const out: Grip[] = [];
@@ -124,6 +127,32 @@ function computeGrips(
const anchor = drawingMoveAnchor(d, verts); const anchor = drawingMoveAnchor(d, verts);
if (anchor) out.push({ kind: "move", target, index: -1, pos: [anchor.x, y + 0.03, anchor.y] }); if (anchor) out.push({ kind: "move", target, index: -1, pos: [anchor.x, y + 0.03, anchor.y] });
} }
// Decken: Eckpunkt-Griffe (Umriss) + je Seite ein Kanten-Mittelpunkt-Griff
// (Nutzer-Wunsch „an allen seiten auch ein punkt … wie an den eckpunkten") +
// ein Verschiebe-Griff im Schwerpunkt. Griffe sitzen auf der Deckenoberkante.
for (const c of ceilings) {
const pts = c.outline;
const n = pts.length;
if (n < 2) continue;
const target = { ceilingId: c.id };
const y = ceilingVerticalExtent(project, c).zTop + 0.02;
let sx = 0;
let sy = 0;
pts.forEach((p, i) => {
out.push({ kind: "vertex", target, index: i, pos: [p.x, y, p.y] });
const q = pts[(i + 1) % n];
out.push({
kind: "edge",
target,
index: i,
index2: (i + 1) % n,
pos: [(p.x + q.x) / 2, y, (p.y + q.y) / 2],
});
sx += p.x;
sy += p.y;
});
out.push({ kind: "move", target, index: -1, pos: [sx / n, y + 0.03, sy / n] });
}
return out; return out;
} }
@@ -266,8 +295,10 @@ export function Wasm3DViewport({
highlightLines = null, highlightLines = null,
editWalls = [], editWalls = [],
editDrawings = [], editDrawings = [],
editCeilings = [],
onEditVertex, onEditVertex,
onEditBody, onEditBody,
onEditEdge,
onEditWallTop, onEditWallTop,
onEditEnd, onEditEnd,
}: { }: {
@@ -304,14 +335,21 @@ export function Wasm3DViewport({
editWalls?: Wall[]; editWalls?: Wall[];
/** Gewählte 2D-Zeichnungen — Quelle der 3D-Vertex-/Verschiebe-Griffe. */ /** Gewählte 2D-Zeichnungen — Quelle der 3D-Vertex-/Verschiebe-Griffe. */
editDrawings?: Drawing2D[]; editDrawings?: Drawing2D[];
/** Gewählte Decken — Quelle der 3D-Eckpunkt-/Kanten-/Verschiebe-Griffe. */
editCeilings?: Ceiling[];
/** Endpunkt-Griff gezogen: neuer XY-Modellpunkt für den Vertex `index` des Elements. */ /** Endpunkt-Griff gezogen: neuer XY-Modellpunkt für den Vertex `index` des Elements. */
onEditVertex?: ( onEditVertex?: (
target: { wallId?: string; drawingId?: string }, target: { wallId?: string; drawingId?: string; ceilingId?: string },
index: number, index: number,
model: Vec2, model: Vec2,
) => Vec2; ) => Vec2;
/** Verschiebe-Griff gezogen: das Element um `delta` (XY) verschieben. */ /** Verschiebe-Griff gezogen: das Element um `delta` (XY) verschieben. */
onEditBody?: (target: { wallId?: string; drawingId?: string }, delta: Vec2) => void; onEditBody?: (
target: { wallId?: string; drawingId?: string; ceilingId?: string },
delta: Vec2,
) => void;
/** Kanten-Griff einer Decke gezogen: Kante (aIndex→bIndex) um `delta` (XY) verschieben. */
onEditEdge?: (ceilingId: string, aIndex: number, bIndex: number, delta: Vec2) => void;
/** Höhen-Griff gezogen: neue absolute Oberkanten-Z der Wand. */ /** Höhen-Griff gezogen: neue absolute Oberkanten-Z der Wand. */
onEditWallTop?: (wallId: string, topZ: number) => void; onEditWallTop?: (wallId: string, topZ: number) => void;
/** Ende eines Griff-Drags. */ /** Ende eines Griff-Drags. */
@@ -330,10 +368,14 @@ export function Wasm3DViewport({
editWallsRef.current = editWalls; editWallsRef.current = editWalls;
const editDrawingsRef = useRef(editDrawings); const editDrawingsRef = useRef(editDrawings);
editDrawingsRef.current = editDrawings; editDrawingsRef.current = editDrawings;
const editCeilingsRef = useRef(editCeilings);
editCeilingsRef.current = editCeilings;
const onEditVertexRef = useRef(onEditVertex); const onEditVertexRef = useRef(onEditVertex);
onEditVertexRef.current = onEditVertex; onEditVertexRef.current = onEditVertex;
const onEditBodyRef = useRef(onEditBody); const onEditBodyRef = useRef(onEditBody);
onEditBodyRef.current = onEditBody; onEditBodyRef.current = onEditBody;
const onEditEdgeRef = useRef(onEditEdge);
onEditEdgeRef.current = onEditEdge;
const onEditWallTopRef = useRef(onEditWallTop); const onEditWallTopRef = useRef(onEditWallTop);
onEditWallTopRef.current = onEditWallTop; onEditWallTopRef.current = onEditWallTop;
const onEditEndRef = useRef(onEditEnd); const onEditEndRef = useRef(onEditEnd);
@@ -477,7 +519,10 @@ export function Wasm3DViewport({
// einen WASM-Push auslösen (reine 2D-Projektionsmathe, kein GPU-Aufruf). // einen WASM-Push auslösen (reine 2D-Projektionsmathe, kein GPU-Aufruf).
const [gripScreens, setGripScreens] = useState<{ grip: Grip; x: number; y: number }[]>([]); const [gripScreens, setGripScreens] = useState<{ grip: Grip; x: number; y: number }[]>([]);
useEffect(() => { useEffect(() => {
if (!ready || (editWalls.length === 0 && editDrawings.length === 0)) { if (
!ready ||
(editWalls.length === 0 && editDrawings.length === 0 && editCeilings.length === 0)
) {
setGripScreens([]); setGripScreens([]);
return; return;
} }
@@ -493,6 +538,7 @@ export function Wasm3DViewport({
projectRef.current, projectRef.current,
editWallsRef.current, editWallsRef.current,
editDrawingsRef.current, editDrawingsRef.current,
editCeilingsRef.current,
groundElevationRef.current, groundElevationRef.current,
); );
const next: { grip: Grip; x: number; y: number }[] = []; const next: { grip: Grip; x: number; y: number }[] = [];
@@ -507,7 +553,7 @@ export function Wasm3DViewport({
}; };
raf = requestAnimationFrame(tick); raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf); return () => cancelAnimationFrame(raf);
}, [ready, editWalls, editDrawings]); }, [ready, editWalls, editDrawings, editCeilings]);
// Canvas-Größe beobachten: bei Layout-Änderung mit aktueller Kamera neu // Canvas-Größe beobachten: bei Layout-Änderung mit aktueller Kamera neu
// zeichnen (die Surface-Anpassung übernimmt der Hook im render()). // zeichnen (die Surface-Anpassung übernimmt der Hook im render()).
@@ -528,11 +574,25 @@ export function Wasm3DViewport({
// Pfad; "height" zieht auf einer VERTIKALEN Ebene entlang der Wandachse // Pfad; "height" zieht auf einer VERTIKALEN Ebene entlang der Wandachse
// (analog `heightZAt` dort), damit eine überwiegend vertikale Mausbewegung // (analog `heightZAt` dort), damit eine überwiegend vertikale Mausbewegung
// auf eine Höhenänderung abbildet. // auf eine Höhenänderung abbildet.
type GripTarget = { wallId?: string; drawingId?: string; ceilingId?: string };
type GripDrag = type GripDrag =
| { kind: "vertex"; target: { wallId?: string; drawingId?: string }; index: number; planeY: number } | { kind: "vertex"; target: GripTarget; index: number; planeY: number }
| { kind: "move"; target: { wallId?: string; drawingId?: string }; planeY: number; lastModel: Vec2 | null } | { kind: "move"; target: GripTarget; planeY: number; lastModel: Vec2 | null }
| { kind: "edge"; ceilingId: string; a: number; b: number; planeY: number; lastModel: Vec2 | null }
| { kind: "height"; wallId: string }; | { kind: "height"; wallId: string };
const gripDragRef = useRef<GripDrag | null>(null); const gripDragRef = useRef<GripDrag | null>(null);
// Arbeitsebene (world-Y) eines Griffs: Wand-UK, Decken-OK bzw. Geschossebene.
const gripPlaneY = (target: GripTarget): number | null => {
if (target.wallId) {
const wall = editWallsRef.current.find((w) => w.id === target.wallId);
return wall ? wallVerticalExtent(projectRef.current, wall).zBottom : null;
}
if (target.ceilingId) {
const c = editCeilingsRef.current.find((x) => x.id === target.ceilingId);
return c ? ceilingVerticalExtent(projectRef.current, c).zTop : null;
}
return groundElevationRef.current;
};
const startGripDrag = (grip: Grip) => (e: React.PointerEvent) => { const startGripDrag = (grip: Grip) => (e: React.PointerEvent) => {
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
@@ -541,18 +601,22 @@ export function Wasm3DViewport({
if (!grip.target.wallId) return; if (!grip.target.wallId) return;
gripDragRef.current = { kind: "height", wallId: grip.target.wallId }; gripDragRef.current = { kind: "height", wallId: grip.target.wallId };
} else { } else {
let planeY: number; const planeY = gripPlaneY(grip.target);
if (grip.target.wallId) { if (planeY == null) return;
const wall = editWallsRef.current.find((w) => w.id === grip.target.wallId); if (grip.kind === "vertex") {
if (!wall) return; gripDragRef.current = { kind: "vertex", target: grip.target, index: grip.index, planeY };
planeY = wallVerticalExtent(projectRef.current, wall).zBottom; } else if (grip.kind === "edge" && grip.target.ceilingId && grip.index2 != null) {
gripDragRef.current = {
kind: "edge",
ceilingId: grip.target.ceilingId,
a: grip.index,
b: grip.index2,
planeY,
lastModel: null,
};
} else { } else {
planeY = groundElevationRef.current; gripDragRef.current = { kind: "move", target: grip.target, planeY, lastModel: null };
} }
gripDragRef.current =
grip.kind === "vertex"
? { kind: "vertex", target: grip.target, index: grip.index, planeY }
: { kind: "move", target: grip.target, planeY, lastModel: null };
} }
(e.target as Element).setPointerCapture(e.pointerId); (e.target as Element).setPointerCapture(e.pointerId);
}; };
@@ -588,6 +652,16 @@ export function Wasm3DViewport({
onEditVertexRef.current?.(drag.target, drag.index, model); onEditVertexRef.current?.(drag.target, drag.index, model);
return; return;
} }
if (drag.kind === "edge") {
// Kanten-Griff (Decke): inkrementelles Delta auf beide Kanten-Endpunkte.
if (drag.lastModel) {
const delta: Vec2 = { x: model.x - drag.lastModel.x, y: model.y - drag.lastModel.y };
if (delta.x !== 0 || delta.y !== 0)
onEditEdgeRef.current?.(drag.ceilingId, drag.a, drag.b, delta);
}
drag.lastModel = model;
return;
}
// "move": inkrementelles Delta wie im three.js-Pendant (workplaneModel + lastModel). // "move": inkrementelles Delta wie im three.js-Pendant (workplaneModel + lastModel).
if (drag.lastModel) { if (drag.lastModel) {
const delta: Vec2 = { x: model.x - drag.lastModel.x, y: model.y - drag.lastModel.y }; const delta: Vec2 = { x: model.x - drag.lastModel.x, y: model.y - drag.lastModel.y };
@@ -838,7 +912,7 @@ export function Wasm3DViewport({
Verschieben grün. */} Verschieben grün. */}
{gripScreens.map(({ grip, x, y }) => ( {gripScreens.map(({ grip, x, y }) => (
<div <div
key={`${grip.kind}:${grip.target.wallId ?? grip.target.drawingId}:${grip.index}`} key={`${grip.kind}:${grip.target.wallId ?? grip.target.drawingId ?? grip.target.ceilingId}:${grip.index}`}
onPointerDown={startGripDrag(grip)} onPointerDown={startGripDrag(grip)}
onPointerMove={onGripPointerMove} onPointerMove={onGripPointerMove}
onPointerUp={endGripDrag} onPointerUp={endGripDrag}
@@ -848,6 +922,8 @@ export function Wasm3DViewport({
? "viewport3d.grip.height" ? "viewport3d.grip.height"
: grip.kind === "move" : grip.kind === "move"
? "viewport3d.grip.move" ? "viewport3d.grip.move"
: grip.kind === "edge"
? "viewport3d.grip.edge"
: "viewport3d.grip.vertex", : "viewport3d.grip.vertex",
)} )}
style={{ style={{
@@ -856,8 +932,18 @@ export function Wasm3DViewport({
top: y - 8, top: y - 8,
width: 16, width: 16,
height: 16, height: 16,
borderRadius: "50%", // Kanten-Griffe rautenförmig + eigene Farbe, damit sie sich klar von
background: grip.kind === "height" ? "#2f8fff" : grip.kind === "move" ? "#18b85a" : "#ff8c1a", // den Eckpunkt-Griffen abheben (Nutzer-Wunsch: „an allen seiten").
borderRadius: grip.kind === "edge" ? "3px" : "50%",
transform: grip.kind === "edge" ? "rotate(45deg)" : undefined,
background:
grip.kind === "height"
? "#2f8fff"
: grip.kind === "move"
? "#18b85a"
: grip.kind === "edge"
? "#b06cff"
: "#ff8c1a",
border: "2px solid rgba(0,0,0,0.6)", border: "2px solid rgba(0,0,0,0.6)",
boxShadow: "0 0 5px rgba(0,0,0,0.55)", boxShadow: "0 0 5px rgba(0,0,0,0.55)",
cursor: grip.kind === "height" ? "ns-resize" : "grab", cursor: grip.kind === "height" ? "ns-resize" : "grab",