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:
+38
-6
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user