73d147b37d
Nutzer-Report nach der Umstellung auf Inline-Editing: beides funktionierte nicht — "Text" liess sich kein Ankerpunkt setzen, "Textspalte" sollte ein Rechteck statt nur eine Breite sein. Root Cause für "Text": `.planview-inline-text` bekam ohne Spaltenbreite `width: undefined` — bei position:fixed ohne rechten Rand greift shrink-to- fit-Sizing, das für ein LEERES contentEditable auf ~0px kollabiert. Der Klick erzeugte tatsächlich ein Element + öffnete den Editor, beides war nur unsichtbar (eine 0px-Box lässt sich nicht anklicken/fokussieren). Fix: neue Klasse .planview-inline-text-free (kein wrapWidth) erzwingt `width: max-content` + `white-space: pre` auf Editor/Surface (Box wächst mit dem Inhalt statt umzubrechen — "Text" bleibt einzeilig bis Enter, wie gewünscht) plus eine CSS-Mindestbreite (160px) für den leeren Startzustand. "Textspalte" (textbox.ts) zeichnet jetzt ein RECHTECK (zwei diagonale Ecken, beliebige Zugrichtung) statt nur eine horizontale Breitenlinie — die aufgezogene Höhe wird nicht im Modell gespeichert (das Textformat kennt nur `width` für den Wortumbruch, keine feste Rahmenhöhe), sondern als Editor-MINDESThöhe durchgereicht: CommandResult/EngineHost.focusDrawing um `focusMinHeightM` erweitert (Passthrough wie focusDrawingId), App.tsx hält sie in einem neuen editTextMinHeightM-State (useContextMenuState.ts), PlanView nutzt sie als CSS-min-height des Editors — der Rahmen wächst bei mehr Text darüber hinaus, schrumpft aber nicht darunter (InDesign-Verhalten). Beim Doppelklick-Editieren bestehender Elemente wird die Mindesthöhe zurückgesetzt (kein Nachwirken einer vorigen Textspalten-Erstellung). textbox.test.ts an die neue Rechteck-Geste angepasst (+ Test für beliebige Zugrichtung). tsc -b / vitest run (919/919) / npm run build grün. Die eigentliche Sichtbarkeits-Vermutung (CSS-Kollaps) konnte ich nicht interaktiv im Browser verifizieren (kein Browser-Tool verfügbar) — Nutzer prüft erneut in der laufenden App.
548 lines
20 KiB
TypeScript
548 lines
20 KiB
TypeScript
// Maus-Schema: Auswahl + Kontextmenü aus Grundriss / 3D. Übersetzt rohe Pick-/
|
|
// Marquee-/Kontextmenü-Events aus PlanView/Viewport3D in Selektions-Mutationen
|
|
// (+ ein paar eng verwandte Doppelklick-Trigger: Raum-Stempel-Editor, Text-
|
|
// Inhalts-Editor). Reine Verschiebung aus App.tsx, keine Verhaltensänderung.
|
|
//
|
|
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
|
|
|
import type { Project, Vec2 } from "../model/types";
|
|
import type { CategoryDisplayResolver } from "../plan/generatePlan";
|
|
import type { DisplayResolver } from "../viewport/Viewport3D";
|
|
import type { PlanSelection, MarqueeSelection } from "../plan/PlanView";
|
|
import type { Pick3dHit } from "../viewport/Wasm3DViewport";
|
|
import type { ViewportContextInfo } from "../viewport/Viewport3D";
|
|
import type { RichTextDoc } from "../text/richText";
|
|
import { plainText } from "../text/richText";
|
|
import { hitTestWall } from "../viewport/planHitTest";
|
|
import type { useSelectionState } from "./useSelectionState";
|
|
import type { useContextMenuState } from "../menus/useContextMenuState";
|
|
|
|
export function useMouseSelectionHandlers(opts: {
|
|
project: Project;
|
|
setProject: (updater: (p: Project) => Project) => void;
|
|
activeLevelId: string;
|
|
visibleCodes: Set<string>;
|
|
categoryDisplay: CategoryDisplayResolver & DisplayResolver;
|
|
selectionState: Pick<
|
|
ReturnType<typeof useSelectionState>,
|
|
| "selectedWallIds"
|
|
| "setSelectedWallIds"
|
|
| "selectedDrawingIds"
|
|
| "setSelectedDrawingIds"
|
|
| "setSelectedDrawingId"
|
|
| "selectedCeilingIds"
|
|
| "setSelectedCeilingIds"
|
|
| "selectedOpeningIds"
|
|
| "setSelectedOpeningIds"
|
|
| "selectedStairIds"
|
|
| "setSelectedStairIds"
|
|
| "selectedRoomIds"
|
|
| "setSelectedRoomIds"
|
|
| "selectedExtrudedSolidIds"
|
|
| "setSelectedExtrudedSolidIds"
|
|
| "selectedColumnIds"
|
|
| "setSelectedColumnIds"
|
|
| "selectedRoofIds"
|
|
| "setSelectedRoofIds"
|
|
| "selectedContextObjectIds"
|
|
| "setSelectedContextObjectIds"
|
|
| "selectedSectionLineId"
|
|
| "setSelectedSectionLineId"
|
|
>;
|
|
contextMenuState: Pick<
|
|
ReturnType<typeof useContextMenuState>,
|
|
"setMenu" | "setStampEditorRoomId" | "editTextId" | "setEditTextId" | "setEditTextMinHeightM"
|
|
>;
|
|
}) {
|
|
const { project, setProject, activeLevelId, visibleCodes, categoryDisplay, selectionState, contextMenuState } = opts;
|
|
const {
|
|
selectedWallIds,
|
|
setSelectedWallIds,
|
|
selectedDrawingIds,
|
|
setSelectedDrawingIds,
|
|
setSelectedDrawingId,
|
|
selectedCeilingIds,
|
|
setSelectedCeilingIds,
|
|
selectedOpeningIds,
|
|
setSelectedOpeningIds,
|
|
selectedStairIds,
|
|
setSelectedStairIds,
|
|
selectedRoomIds,
|
|
setSelectedRoomIds,
|
|
selectedExtrudedSolidIds,
|
|
setSelectedExtrudedSolidIds,
|
|
selectedColumnIds,
|
|
setSelectedColumnIds,
|
|
selectedRoofIds,
|
|
setSelectedRoofIds,
|
|
selectedContextObjectIds,
|
|
setSelectedContextObjectIds,
|
|
selectedSectionLineId,
|
|
setSelectedSectionLineId,
|
|
} = selectionState;
|
|
const { setMenu, setStampEditorRoomId, editTextId, setEditTextId, setEditTextMinHeightM } = contextMenuState;
|
|
|
|
// ── Maus-Schema: Auswahl + Kontextmenü aus Grundriss / 3D ─────────────────
|
|
// Links-Klick im Grundriss: genau eine Wand wählen (Menge ersetzen) bzw.
|
|
// Leerraum → Auswahl löschen.
|
|
// Doppelklick auf einen Raum-Stempel: den Raum wählen + den Stempel-Editor
|
|
// öffnen (Rich-Text-Popup).
|
|
const onOpenStampEditor = (roomId: string) => {
|
|
setSelectedRoomIds([roomId]);
|
|
setSelectedWallIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedSectionLineId(null);
|
|
setStampEditorRoomId(roomId);
|
|
};
|
|
// Doppelklick auf ein Text-2D-Element: selektieren + Inhalts-Editor öffnen.
|
|
const onEditDrawingText = (drawingId: string) => {
|
|
const d = project.drawings2d.find((x) => x.id === drawingId);
|
|
if (!d || d.geom.shape !== "text") return;
|
|
setSelectedDrawingId(drawingId);
|
|
setSelectedWallIds([]);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedSectionLineId(null);
|
|
// Bestehendes Element per Doppelklick: keine Rahmen-Mindesthöhe (nur bei
|
|
// frisch aufgezogener Textspalte relevant, s. textbox.ts focusMinHeightM)
|
|
// — sonst könnte eine vorige Textspalten-Erstellung hier nachwirken.
|
|
setEditTextMinHeightM(null);
|
|
setEditTextId(drawingId);
|
|
};
|
|
|
|
// Bearbeitetes Rich-Text-Dokument übernehmen (immutabel; nur bei Text-Geometrie).
|
|
// `text` bleibt als Reintext-Spiegel bestehen (DXF-Export, Suche, Alt-Konsumenten).
|
|
// LIVE bei jedem Tastendruck (Inline-Editor auf der Zeichenfläche, kein Dialog
|
|
// mehr) — schreibt AUCH einen leeren Zustand zurück (kein Guard mehr), damit
|
|
// Löschen bis auf null Zeichen sich sofort im Modell spiegelt; das eigentliche
|
|
// Aufräumen eines leer verlassenen NEUEN Elements übernimmt `closeTextEdit`.
|
|
const commitTextEdit = (doc: RichTextDoc) => {
|
|
const id = editTextId;
|
|
if (!id) return;
|
|
setProject((p) => ({
|
|
...p,
|
|
drawings2d: p.drawings2d.map((d) =>
|
|
d.id === id && d.geom.shape === "text"
|
|
? { ...d, geom: { ...d.geom, doc, text: plainText(doc) } }
|
|
: d,
|
|
),
|
|
}));
|
|
};
|
|
|
|
// Inline-Editor schliessen (Backdrop-Klick/Escape): bleibt das Element leer
|
|
// (frisch über `text`/`textbox` erzeugt, nie befüllt, oder komplett wieder
|
|
// geleert), wird es entfernt statt als leere Leiche im Modell zu bleiben.
|
|
const closeTextEdit = () => {
|
|
const id = editTextId;
|
|
if (id) {
|
|
setProject((p) => {
|
|
const d = p.drawings2d.find((x) => x.id === id);
|
|
// `text` ist immer der Reintext-Spiegel von `doc` (s. commitTextEdit) —
|
|
// reicht daher allein als Leer-Test, egal ob `doc` je gesetzt wurde.
|
|
if (d && d.geom.shape === "text" && d.geom.text.trim() === "") {
|
|
return { ...p, drawings2d: p.drawings2d.filter((x) => x.id !== id) };
|
|
}
|
|
return p;
|
|
});
|
|
}
|
|
setEditTextId(null);
|
|
setEditTextMinHeightM(null);
|
|
};
|
|
// Kategorie-Code des getroffenen Elements (für die Sperr-Prüfung unten) —
|
|
// nur die Kanäle, deren Modell-Typ eine categoryCode trägt (ExtrudedSolid/
|
|
// Kontext-Objekte bewusst ohne, siehe deren Typdefinitionen).
|
|
const categoryCodeOfSelection = (sel: PlanSelection): string | undefined => {
|
|
if (sel.wallId) return project.walls.find((w) => w.id === sel.wallId)?.categoryCode;
|
|
if (sel.ceilingId)
|
|
return (project.ceilings ?? []).find((c) => c.id === sel.ceilingId)?.categoryCode;
|
|
if (sel.openingId)
|
|
return (project.openings ?? []).find((o) => o.id === sel.openingId)?.categoryCode;
|
|
if (sel.stairId)
|
|
return (project.stairs ?? []).find((s) => s.id === sel.stairId)?.categoryCode;
|
|
if (sel.roomId) return (project.rooms ?? []).find((r) => r.id === sel.roomId)?.categoryCode;
|
|
if (sel.columnId)
|
|
return (project.columns ?? []).find((c) => c.id === sel.columnId)?.categoryCode;
|
|
if (sel.roofId) return (project.roofs ?? []).find((r) => r.id === sel.roofId)?.categoryCode;
|
|
if (sel.drawingId)
|
|
return project.drawings2d.find((d) => d.id === sel.drawingId)?.categoryCode;
|
|
return undefined;
|
|
};
|
|
|
|
const onPlanSelect = (sel: PlanSelection) => {
|
|
// Darstellungsmodus „Andere grau & gesperrt": ein gesperrtes Element lässt
|
|
// sich nicht anwählen (und damit auch nicht verschieben/bearbeiten) — der
|
|
// Klick wird ignoriert, als läge dort nichts.
|
|
const hitCode = categoryCodeOfSelection(sel);
|
|
if (hitCode !== undefined && categoryDisplay(hitCode).locked) return;
|
|
// Shift+Klick: Mehrfach-Auswahl — getroffenes Element zu-/abschalten, den
|
|
// Rest behalten (Wände und 2D-Zeichnungen unabhängig, gemischte Auswahl
|
|
// erlaubt). Leerraum-Klick mit Shift lässt die Auswahl unverändert.
|
|
if (sel.shift) {
|
|
// Schnittlinie ist Einzelauswahl — Shift+Klick togglet sie an/aus.
|
|
if (sel.sectionLineId) {
|
|
setSelectedSectionLineId(
|
|
selectedSectionLineId === sel.sectionLineId ? null : sel.sectionLineId,
|
|
);
|
|
return;
|
|
}
|
|
if (sel.wallId) {
|
|
const id = sel.wallId;
|
|
setSelectedWallIds(
|
|
selectedWallIds.includes(id)
|
|
? selectedWallIds.filter((w) => w !== id)
|
|
: [...selectedWallIds, id],
|
|
);
|
|
} else if (sel.drawingId) {
|
|
const id = sel.drawingId;
|
|
setSelectedDrawingIds(
|
|
selectedDrawingIds.includes(id)
|
|
? selectedDrawingIds.filter((d) => d !== id)
|
|
: [...selectedDrawingIds, id],
|
|
);
|
|
} else if (sel.ceilingId) {
|
|
const id = sel.ceilingId;
|
|
setSelectedCeilingIds(
|
|
selectedCeilingIds.includes(id)
|
|
? selectedCeilingIds.filter((c) => c !== id)
|
|
: [...selectedCeilingIds, id],
|
|
);
|
|
} else if (sel.openingId) {
|
|
const id = sel.openingId;
|
|
setSelectedOpeningIds(
|
|
selectedOpeningIds.includes(id)
|
|
? selectedOpeningIds.filter((o) => o !== id)
|
|
: [...selectedOpeningIds, id],
|
|
);
|
|
} else if (sel.stairId) {
|
|
const id = sel.stairId;
|
|
setSelectedStairIds(
|
|
selectedStairIds.includes(id)
|
|
? selectedStairIds.filter((s) => s !== id)
|
|
: [...selectedStairIds, id],
|
|
);
|
|
} else if (sel.roomId) {
|
|
const id = sel.roomId;
|
|
setSelectedRoomIds(
|
|
selectedRoomIds.includes(id)
|
|
? selectedRoomIds.filter((r) => r !== id)
|
|
: [...selectedRoomIds, id],
|
|
);
|
|
} else if (sel.extrudedSolidId) {
|
|
const id = sel.extrudedSolidId;
|
|
setSelectedExtrudedSolidIds(
|
|
selectedExtrudedSolidIds.includes(id)
|
|
? selectedExtrudedSolidIds.filter((x) => x !== id)
|
|
: [...selectedExtrudedSolidIds, id],
|
|
);
|
|
} else if (sel.columnId) {
|
|
const id = sel.columnId;
|
|
setSelectedColumnIds(
|
|
selectedColumnIds.includes(id)
|
|
? selectedColumnIds.filter((x) => x !== id)
|
|
: [...selectedColumnIds, id],
|
|
);
|
|
} else if (sel.roofId) {
|
|
const id = sel.roofId;
|
|
setSelectedRoofIds(
|
|
selectedRoofIds.includes(id)
|
|
? selectedRoofIds.filter((x) => x !== id)
|
|
: [...selectedRoofIds, id],
|
|
);
|
|
} else if (sel.contextObjectId) {
|
|
const id = sel.contextObjectId;
|
|
setSelectedContextObjectIds(
|
|
selectedContextObjectIds.includes(id)
|
|
? selectedContextObjectIds.filter((x) => x !== id)
|
|
: [...selectedContextObjectIds, id],
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
// Ohne Shift: ersetzen. Öffnung hat Vorrang (ihr Symbol liegt in der Lücke),
|
|
// dann Wand, dann 2D-Element, dann Decke; sonst leeren. Die Kanäle schließen
|
|
// sich gegenseitig aus.
|
|
// Alle Kanäle ausser dem gewählten leeren — als Helfer, damit jeder Zweig
|
|
// (inkl. des neuen Dach-Kanals) konsistent bleibt.
|
|
const clearOthers = (keep: string) => {
|
|
if (keep !== "wall") setSelectedWallIds([]);
|
|
if (keep !== "drawing") setSelectedDrawingId(null);
|
|
if (keep !== "ceiling") setSelectedCeilingIds([]);
|
|
if (keep !== "opening") setSelectedOpeningIds([]);
|
|
if (keep !== "stair") setSelectedStairIds([]);
|
|
if (keep !== "room") setSelectedRoomIds([]);
|
|
if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]);
|
|
if (keep !== "column") setSelectedColumnIds([]);
|
|
if (keep !== "roof") setSelectedRoofIds([]);
|
|
if (keep !== "contextObject") setSelectedContextObjectIds([]);
|
|
if (keep !== "sectionLine") setSelectedSectionLineId(null);
|
|
};
|
|
if (sel.sectionLineId) {
|
|
setSelectedSectionLineId(sel.sectionLineId);
|
|
clearOthers("sectionLine");
|
|
} else if (sel.openingId) {
|
|
setSelectedOpeningIds([sel.openingId]);
|
|
clearOthers("opening");
|
|
} else if (sel.wallId) {
|
|
setSelectedWallIds([sel.wallId]);
|
|
clearOthers("wall");
|
|
} else if (sel.columnId) {
|
|
setSelectedColumnIds([sel.columnId]);
|
|
clearOthers("column");
|
|
} else if (sel.drawingId) {
|
|
setSelectedDrawingId(sel.drawingId);
|
|
clearOthers("drawing");
|
|
} else if (sel.extrudedSolidId) {
|
|
setSelectedExtrudedSolidIds([sel.extrudedSolidId]);
|
|
clearOthers("extrudedSolid");
|
|
} else if (sel.ceilingId) {
|
|
setSelectedCeilingIds([sel.ceilingId]);
|
|
clearOthers("ceiling");
|
|
} else if (sel.stairId) {
|
|
setSelectedStairIds([sel.stairId]);
|
|
clearOthers("stair");
|
|
} else if (sel.roomId) {
|
|
setSelectedRoomIds([sel.roomId]);
|
|
clearOthers("room");
|
|
} else if (sel.roofId) {
|
|
setSelectedRoofIds([sel.roofId]);
|
|
clearOthers("roof");
|
|
} else if (sel.contextObjectId) {
|
|
setSelectedContextObjectIds([sel.contextObjectId]);
|
|
clearOthers("contextObject");
|
|
} else {
|
|
clearOthers("");
|
|
}
|
|
};
|
|
// Links-Drag (Marquee) im Grundriss: die getroffene Wand-MENGE übernehmen
|
|
// (nach CAD-Konvention von PlanView bestimmt). Ersetzt die bisherige Auswahl.
|
|
const onPlanMarquee = (sel: MarqueeSelection) => {
|
|
setSelectedWallIds(sel.wallIds);
|
|
setSelectedDrawingIds(sel.drawingIds ?? []);
|
|
// Decken/Öffnungen/Treppen/Räume/Extrusionen/Dächer sind (noch) nicht Teil
|
|
// des Marquee → beim Aufziehen abwählen.
|
|
setSelectedCeilingIds([]);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedSectionLineId(null);
|
|
};
|
|
// Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die
|
|
// NICHT bereits in der Auswahl ist, wird die Auswahl auf sie gesetzt (damit die
|
|
// Aktionen einen Bezug haben); innerhalb einer bestehenden Mehrfachauswahl
|
|
// bleibt die Menge erhalten.
|
|
const onPlanContextMenu = (info: {
|
|
clientX: number;
|
|
clientY: number;
|
|
model: Vec2;
|
|
}) => {
|
|
const wallId = hitTestWall(project, activeLevelId, visibleCodes, info.model);
|
|
if (wallId && !selectedWallIds.includes(wallId)) setSelectedWallIds([wallId]);
|
|
setMenu({ kind: "plan", x: info.clientX, y: info.clientY, wallId });
|
|
};
|
|
// Links-Klick im 3D: per Raycast getroffene Wand wählen (Einzelauswahl).
|
|
const onViewportSelectWall = (wallId: string | null) => {
|
|
setSelectedWallIds(wallId ? [wallId] : []);
|
|
if (wallId) {
|
|
setSelectedDrawingId(null);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedSectionLineId(null);
|
|
}
|
|
};
|
|
// Links-Klick im 3D: per Raycast getroffene Treppe wählen (Einzelauswahl).
|
|
const onViewportSelectStair = (stairId: string | null) => {
|
|
setSelectedStairIds(stairId ? [stairId] : []);
|
|
if (stairId) {
|
|
setSelectedWallIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedSectionLineId(null);
|
|
}
|
|
};
|
|
// Links-Klick im 3D: per Raycast getroffene Decke wählen (Einzelauswahl).
|
|
const onViewportSelectCeiling = (ceilingId: string | null) => {
|
|
setSelectedCeilingIds(ceilingId ? [ceilingId] : []);
|
|
if (ceilingId) {
|
|
setSelectedWallIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedStairIds([]);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedSectionLineId(null);
|
|
}
|
|
};
|
|
// Links-Klick im 3D: per Raycast getroffene Öffnung wählen (Einzelauswahl).
|
|
const onViewportSelectOpening = (openingId: string | null) => {
|
|
setSelectedOpeningIds(openingId ? [openingId] : []);
|
|
if (openingId) {
|
|
setSelectedWallIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedSectionLineId(null);
|
|
}
|
|
};
|
|
// Links-KLICK im Nordstern-3D-Viewport (WASM, TS-Raycast): getroffenes Bauteil
|
|
// wählen, sodass das Attribute-/Objekt-Info-Panel es zeigt (erster Schritt zur
|
|
// 3D-Editierbarkeit). `additive` (Shift/Ctrl) = zur Auswahl DERSELBEN Art
|
|
// hinzufügen, sonst ersetzen; beim Setzen die ANDEREN Arten-Arrays leeren
|
|
// (Einzel-Selektion über Arten hinweg, konsistent mit dem 2D-Verhalten).
|
|
// Leertreffer (kein Bauteil) → alle Selektionen leeren.
|
|
const onViewport3dPick = (hit: Pick3dHit, additive: boolean) => {
|
|
if (!hit) {
|
|
setSelectedWallIds([]);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedSectionLineId(null);
|
|
return;
|
|
}
|
|
if (hit.kind === "contextObject") {
|
|
setSelectedContextObjectIds([hit.id]);
|
|
setSelectedWallIds([]);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedSectionLineId(null);
|
|
return;
|
|
}
|
|
// Öffnung (Fenster/Tür) und Treppe sind jetzt auch im WASM-Viewport pickbar
|
|
// (raycast3d/pickGeometry). Jede Art ersetzt die Auswahl exklusiv; nur Wände
|
|
// unterstützen additive Mehrfachauswahl (wie bisher).
|
|
if (hit.kind === "opening") {
|
|
setSelectedOpeningIds([hit.id]);
|
|
setSelectedWallIds([]);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedSectionLineId(null);
|
|
return;
|
|
}
|
|
if (hit.kind === "stair") {
|
|
setSelectedStairIds([hit.id]);
|
|
setSelectedWallIds([]);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedSectionLineId(null);
|
|
return;
|
|
}
|
|
if (hit.kind === "roof") {
|
|
setSelectedRoofIds([hit.id]);
|
|
setSelectedWallIds([]);
|
|
setSelectedCeilingIds([]);
|
|
setSelectedDrawingId(null);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
return;
|
|
}
|
|
if (hit.kind === "wall") {
|
|
setSelectedWallIds(
|
|
additive ? Array.from(new Set([...selectedWallIds, hit.id])) : [hit.id],
|
|
);
|
|
setSelectedCeilingIds([]);
|
|
} else {
|
|
setSelectedCeilingIds(
|
|
additive ? Array.from(new Set([...selectedCeilingIds, hit.id])) : [hit.id],
|
|
);
|
|
setSelectedWallIds([]);
|
|
}
|
|
setSelectedDrawingId(null);
|
|
setSelectedOpeningIds([]);
|
|
setSelectedStairIds([]);
|
|
setSelectedContextObjectIds([]);
|
|
setSelectedRoomIds([]);
|
|
setSelectedExtrudedSolidIds([]);
|
|
setSelectedColumnIds([]);
|
|
setSelectedRoofIds([]);
|
|
setSelectedSectionLineId(null);
|
|
};
|
|
// Rechts-Klick im 3D: Wand-Kontextmenü an der Cursorposition.
|
|
const onViewportContextMenu = (info: ViewportContextInfo) => {
|
|
if (info.wallId && !selectedWallIds.includes(info.wallId)) {
|
|
setSelectedWallIds([info.wallId]);
|
|
}
|
|
setMenu({
|
|
kind: "viewport",
|
|
x: info.clientX,
|
|
y: info.clientY,
|
|
wallId: info.wallId,
|
|
});
|
|
};
|
|
|
|
return {
|
|
onOpenStampEditor,
|
|
onEditDrawingText,
|
|
commitTextEdit,
|
|
closeTextEdit,
|
|
onPlanSelect,
|
|
onPlanMarquee,
|
|
onPlanContextMenu,
|
|
onViewportSelectWall,
|
|
onViewportSelectStair,
|
|
onViewportSelectCeiling,
|
|
onViewportSelectOpening,
|
|
onViewport3dPick,
|
|
onViewportContextMenu,
|
|
};
|
|
}
|