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:
@@ -152,8 +152,10 @@ export function Viewport3D(
|
||||
// wie die three.js-Sicht (renderer-agnostisch).
|
||||
editWalls={props.editWalls}
|
||||
editDrawings={props.editDrawings}
|
||||
editCeilings={props.editCeilings}
|
||||
onEditVertex={props.onEditVertex}
|
||||
onEditBody={props.onEditBody}
|
||||
onEditEdge={props.onEditEdge}
|
||||
onEditWallTop={props.onEditWallTop}
|
||||
onEditEnd={props.onEditEnd}
|
||||
/>
|
||||
@@ -276,23 +278,29 @@ function ThreeViewport3D({
|
||||
editWalls?: Wall[];
|
||||
/** Alle gewählten 2D-Elemente — Vertex-Griffe in 3D (Mehrfachauswahl ⇒ alle). */
|
||||
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`
|
||||
* des durch `target` benannten Elements (Wand-ID ODER Drawing-ID). Optional
|
||||
* `excludeWallIds`/`excludeDrawingIds` = Koinzidenz-Gruppe, die aus den
|
||||
* des durch `target` benannten Elements (Wand-ID, Drawing-ID ODER Decken-ID).
|
||||
* Optional `excludeWallIds`/`excludeDrawingIds` = Koinzidenz-Gruppe, die aus den
|
||||
* Snap-Quellen genommen wird; nur dann snappt App den Punkt. Liefert den
|
||||
* effektiv angewandten (gesnappten) Punkt zurück (für Marker + Mitführen).
|
||||
* Wird auf dieselbe Store-Action (`moveGripOf`) gelegt wie die Plan-Griffe.
|
||||
*/
|
||||
onEditVertex?: (
|
||||
target: { wallId?: string; drawingId?: string },
|
||||
target: { wallId?: string; drawingId?: string; ceilingId?: string },
|
||||
index: number,
|
||||
model: Vec2,
|
||||
excludeWallIds?: Set<string>,
|
||||
excludeDrawingIds?: Set<string>,
|
||||
) => Vec2;
|
||||
/** 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. */
|
||||
onEditWallTop?: (wallId: string, topZ: number) => void;
|
||||
/** Ende eines Griff-Drags (Commit/Aufräumen). */
|
||||
|
||||
+111
-25
@@ -43,8 +43,8 @@
|
||||
// Pixel an der Viewport-Höhe normiert), damit sich beide Pfade gleich anfühlen.
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Project, Wall, Drawing2D, Vec2 } from "../model/types";
|
||||
import { wallVerticalExtent } from "../model/wall";
|
||||
import type { Project, Wall, Drawing2D, Ceiling, Vec2 } from "../model/types";
|
||||
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
|
||||
import { projectToModel3d, pickGeometry, TRUCK_MESH_READY_EVENT } from "../plan/toWalls3d";
|
||||
import type { RenderMode, View3d } from "../ui/TopBar";
|
||||
import { t } from "../i18n";
|
||||
@@ -82,13 +82,15 @@ const CLICK_THRESHOLD_PX = 4;
|
||||
// GUI-Element aus. Kosten: die Bildschirm-Position muss der Kamera folgen —
|
||||
// `useEffect` unten pollt sie per `requestAnimationFrame`, solange Griffe
|
||||
// aktiv sind (billige 2D-Projektion, kein WASM/GPU-Aufruf).
|
||||
type GripKind = "vertex" | "height" | "move";
|
||||
type GripKind = "vertex" | "height" | "move" | "edge";
|
||||
interface Grip {
|
||||
kind: GripKind;
|
||||
/** Element, dessen Griff das ist (Wand-ID ODER Drawing-ID). */
|
||||
target: { wallId?: string; drawingId?: string };
|
||||
/** Vertex-Index (nur `kind==="vertex"`); -1 sonst. */
|
||||
/** Element, dessen Griff das ist (Wand-ID, Drawing-ID ODER Decken-ID). */
|
||||
target: { wallId?: string; drawingId?: string; ceilingId?: string };
|
||||
/** Vertex-Index (`vertex`) bzw. erster Kanten-Index (`edge`); -1 sonst. */
|
||||
index: number;
|
||||
/** Zweiter Kanten-Index (nur `kind==="edge"`). */
|
||||
index2?: number;
|
||||
pos: Vec3;
|
||||
}
|
||||
|
||||
@@ -103,6 +105,7 @@ function computeGrips(
|
||||
project: Project,
|
||||
walls: readonly Wall[],
|
||||
drawings: readonly Drawing2D[],
|
||||
ceilings: readonly Ceiling[],
|
||||
drawingElevation: number,
|
||||
): Grip[] {
|
||||
const out: Grip[] = [];
|
||||
@@ -124,6 +127,32 @@ function computeGrips(
|
||||
const anchor = drawingMoveAnchor(d, verts);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -266,8 +295,10 @@ export function Wasm3DViewport({
|
||||
highlightLines = null,
|
||||
editWalls = [],
|
||||
editDrawings = [],
|
||||
editCeilings = [],
|
||||
onEditVertex,
|
||||
onEditBody,
|
||||
onEditEdge,
|
||||
onEditWallTop,
|
||||
onEditEnd,
|
||||
}: {
|
||||
@@ -304,14 +335,21 @@ export function Wasm3DViewport({
|
||||
editWalls?: Wall[];
|
||||
/** Gewählte 2D-Zeichnungen — Quelle der 3D-Vertex-/Verschiebe-Griffe. */
|
||||
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. */
|
||||
onEditVertex?: (
|
||||
target: { wallId?: string; drawingId?: string },
|
||||
target: { wallId?: string; drawingId?: string; ceilingId?: string },
|
||||
index: number,
|
||||
model: Vec2,
|
||||
) => Vec2;
|
||||
/** 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. */
|
||||
onEditWallTop?: (wallId: string, topZ: number) => void;
|
||||
/** Ende eines Griff-Drags. */
|
||||
@@ -330,10 +368,14 @@ export function Wasm3DViewport({
|
||||
editWallsRef.current = editWalls;
|
||||
const editDrawingsRef = useRef(editDrawings);
|
||||
editDrawingsRef.current = editDrawings;
|
||||
const editCeilingsRef = useRef(editCeilings);
|
||||
editCeilingsRef.current = editCeilings;
|
||||
const onEditVertexRef = useRef(onEditVertex);
|
||||
onEditVertexRef.current = onEditVertex;
|
||||
const onEditBodyRef = useRef(onEditBody);
|
||||
onEditBodyRef.current = onEditBody;
|
||||
const onEditEdgeRef = useRef(onEditEdge);
|
||||
onEditEdgeRef.current = onEditEdge;
|
||||
const onEditWallTopRef = useRef(onEditWallTop);
|
||||
onEditWallTopRef.current = onEditWallTop;
|
||||
const onEditEndRef = useRef(onEditEnd);
|
||||
@@ -477,7 +519,10 @@ export function Wasm3DViewport({
|
||||
// einen WASM-Push auslösen (reine 2D-Projektionsmathe, kein GPU-Aufruf).
|
||||
const [gripScreens, setGripScreens] = useState<{ grip: Grip; x: number; y: number }[]>([]);
|
||||
useEffect(() => {
|
||||
if (!ready || (editWalls.length === 0 && editDrawings.length === 0)) {
|
||||
if (
|
||||
!ready ||
|
||||
(editWalls.length === 0 && editDrawings.length === 0 && editCeilings.length === 0)
|
||||
) {
|
||||
setGripScreens([]);
|
||||
return;
|
||||
}
|
||||
@@ -493,6 +538,7 @@ export function Wasm3DViewport({
|
||||
projectRef.current,
|
||||
editWallsRef.current,
|
||||
editDrawingsRef.current,
|
||||
editCeilingsRef.current,
|
||||
groundElevationRef.current,
|
||||
);
|
||||
const next: { grip: Grip; x: number; y: number }[] = [];
|
||||
@@ -507,7 +553,7 @@ export function Wasm3DViewport({
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [ready, editWalls, editDrawings]);
|
||||
}, [ready, editWalls, editDrawings, editCeilings]);
|
||||
|
||||
// Canvas-Größe beobachten: bei Layout-Änderung mit aktueller Kamera neu
|
||||
// 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
|
||||
// (analog `heightZAt` dort), damit eine überwiegend vertikale Mausbewegung
|
||||
// auf eine Höhenänderung abbildet.
|
||||
type GripTarget = { wallId?: string; drawingId?: string; ceilingId?: string };
|
||||
type GripDrag =
|
||||
| { kind: "vertex"; target: { wallId?: string; drawingId?: string }; index: number; planeY: number }
|
||||
| { kind: "move"; target: { wallId?: string; drawingId?: string }; planeY: number; lastModel: Vec2 | null }
|
||||
| { kind: "vertex"; target: GripTarget; index: number; planeY: number }
|
||||
| { 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 };
|
||||
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) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
@@ -541,18 +601,22 @@ export function Wasm3DViewport({
|
||||
if (!grip.target.wallId) return;
|
||||
gripDragRef.current = { kind: "height", wallId: grip.target.wallId };
|
||||
} else {
|
||||
let planeY: number;
|
||||
if (grip.target.wallId) {
|
||||
const wall = editWallsRef.current.find((w) => w.id === grip.target.wallId);
|
||||
if (!wall) return;
|
||||
planeY = wallVerticalExtent(projectRef.current, wall).zBottom;
|
||||
const planeY = gripPlaneY(grip.target);
|
||||
if (planeY == null) return;
|
||||
if (grip.kind === "vertex") {
|
||||
gripDragRef.current = { kind: "vertex", target: grip.target, index: grip.index, planeY };
|
||||
} 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 {
|
||||
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);
|
||||
};
|
||||
@@ -588,6 +652,16 @@ export function Wasm3DViewport({
|
||||
onEditVertexRef.current?.(drag.target, drag.index, model);
|
||||
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).
|
||||
if (drag.lastModel) {
|
||||
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. */}
|
||||
{gripScreens.map(({ grip, x, y }) => (
|
||||
<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)}
|
||||
onPointerMove={onGripPointerMove}
|
||||
onPointerUp={endGripDrag}
|
||||
@@ -848,6 +922,8 @@ export function Wasm3DViewport({
|
||||
? "viewport3d.grip.height"
|
||||
: grip.kind === "move"
|
||||
? "viewport3d.grip.move"
|
||||
: grip.kind === "edge"
|
||||
? "viewport3d.grip.edge"
|
||||
: "viewport3d.grip.vertex",
|
||||
)}
|
||||
style={{
|
||||
@@ -856,8 +932,18 @@ export function Wasm3DViewport({
|
||||
top: y - 8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: "50%",
|
||||
background: grip.kind === "height" ? "#2f8fff" : grip.kind === "move" ? "#18b85a" : "#ff8c1a",
|
||||
// Kanten-Griffe rautenförmig + eigene Farbe, damit sie sich klar von
|
||||
// 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)",
|
||||
boxShadow: "0 0 5px rgba(0,0,0,0.55)",
|
||||
cursor: grip.kind === "height" ? "ns-resize" : "grab",
|
||||
|
||||
Reference in New Issue
Block a user