God-Files aufgeteilt (ResourceManager/generatePlan/toWalls3d/App.tsx-Handler), Totcode entfernt (geometry-Crate, OCCT/HLR-Spike), PDF/Bild-Import ergänzt.
ResourceManager.tsx, generatePlan.ts und toWalls3d.ts sind nach Bauteil- Domäne in src/ui/resourceManager/, src/plan/generatePlan/ und src/plan/toWalls3d/ aufgeteilt (reine Verschiebung, keine Verhaltens- änderung). App.tsx verliert weitere Handler-Gruppen an eigene Hooks (useExportHandlers, useImportHandling, useKeyboardShortcuts) sowie Hit-Testing an src/viewport/planHitTest.ts. Unbenutzte Rust-geometry-Crate (durch WASM-Joins abgelöst) sowie der alte OCCT/HLR-Schnitt-Spike (hlr.ts/occt.ts, vite occt-Aliases) entfernt. Neu: PDF-Import als Rasterbild (src/io/pdfImport.ts) + gemeinsamer Bild-Asset-Helfer (src/io/imageAsset.ts) für Plan- und Layout-Bilder. tsc -b sauber, vitest 891/891 grün.
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
// Bündelt die globalen Tastatur-Shortcut-Effekte (Undo/Redo, Tab-Feld-Zyklus,
|
||||
// Werkzeug-Zifferntasten, aktives-Werkzeug Esc/Enter/Backspace, Alles-wählen +
|
||||
// Löschen, Split/Join, Kopieren/Einfügen von 2D-Elementen, Boolean-Operationen,
|
||||
// Transformations-Shortcuts) — nur aus App.tsx herausgezogen, um die Funktion
|
||||
// zu entlasten. Reine Verlagerung der `useEffect`-Registrierungen inkl. ihrer
|
||||
// Abhängigkeits-Arrays (auch dort, wo bewusst `eslint-disable` steht) — keine
|
||||
// Verhaltensänderung. Die Aktions-Funktionen selbst (deleteSelection, runSplit,
|
||||
// applyBoolean, …) bleiben in App.tsx, da sie auch von Menüs/TopBar gebraucht
|
||||
// werden, und werden hier nur als Parameter durchgereicht.
|
||||
//
|
||||
// Extrahiert aus App.tsx. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { MutableRefObject, RefObject } from "react";
|
||||
import { getTool } from "../tools/tools";
|
||||
import type { ToolContext, ToolDraft, ToolId, ToolState } from "../tools/types";
|
||||
import type { CopyMode, TransformOp } from "../tools/transform";
|
||||
import type { CommandEngine } from "../commands/engine";
|
||||
import type { CommandLineHandle } from "../ui/CommandLine";
|
||||
import type { Drawing2D, Project, Vec2 } from "../model/types";
|
||||
import { differenceDrawings, intersectionDrawings, unionDrawings } from "../editors/booleanOps";
|
||||
import type { BooleanResult } from "../editors/booleanOps";
|
||||
|
||||
/** Aktive Transformation (Bewegen/Spiegeln/Drehen) — Form aus useDrawingToolState. */
|
||||
type ActiveTransform = {
|
||||
op: TransformOp;
|
||||
copyMode: CopyMode;
|
||||
count: number;
|
||||
points: Vec2[];
|
||||
} | null;
|
||||
|
||||
const isEditableTarget = (e: KeyboardEvent): boolean => {
|
||||
const el = e.target as HTMLElement | null;
|
||||
return !!el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable);
|
||||
};
|
||||
|
||||
/** Globaler Undo/Redo-Shortcut: Ctrl+Z / Cmd+Z, Ctrl+Shift+Z / Cmd+Shift+Z bzw. Ctrl+Y für redo. */
|
||||
export function useUndoRedoShortcut(undo: () => void, redo: () => void): void {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
const key = e.key.toLowerCase();
|
||||
if (!mod || (key !== "z" && key !== "y")) return;
|
||||
if (isEditableTarget(e)) return;
|
||||
e.preventDefault();
|
||||
if (key === "y" || (key === "z" && e.shiftKey)) redo();
|
||||
else undo();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [undo, redo]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Globaler Tab-Handler: läuft ein Befehl MIT Feldern (§2.7), zyklt Tab das
|
||||
* aktive Feld (Präzisionseingabe), ohne den Fokus zu stehlen — so kann man bei
|
||||
* der Maus bleiben. Sonst fokussiert/öffnet Tab die Command-Line. Direktes
|
||||
* Zahlen-Tippen beim Zeichnen (VW-Verhalten): läuft ein Befehl mit Tab-Feldern
|
||||
* und tippt man eine Ziffer (oder . , -), ohne dass ein Eingabefeld fokussiert
|
||||
* ist, startet das die Werteingabe in der Befehlszeile.
|
||||
*/
|
||||
export function useCommandTabShortcut(
|
||||
engineRef: RefObject<CommandEngine | null>,
|
||||
commandLineRef: RefObject<CommandLineHandle | null>,
|
||||
gripDragInfoRef: RefObject<{ index: number; raw: Vec2 } | null>,
|
||||
gripEditRef: RefObject<{
|
||||
anchor: Vec2;
|
||||
active: "length" | "angle";
|
||||
locks: { length?: number; angle?: number };
|
||||
} | null>,
|
||||
cycleGripEditFieldRef: RefObject<() => void>,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const inInput = isEditableTarget(e);
|
||||
if (
|
||||
!inInput &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
/^[0-9.,-]$/.test(e.key) &&
|
||||
engineRef.current?.isActive() &&
|
||||
engineRef.current.hasFields()
|
||||
) {
|
||||
e.preventDefault();
|
||||
commandLineRef.current?.beginTyping(e.key);
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Tab") return;
|
||||
if (inInput) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const eng = engineRef.current;
|
||||
if (eng?.isActive() && eng.hasFields()) {
|
||||
eng.cycleField();
|
||||
return;
|
||||
}
|
||||
// (c) Läuft ein Vertex-Griff-Drag, öffnet/zyklt Tab das Editier-Feld
|
||||
// (Länge/Winkel relativ zur Nachbarecke). Beim ersten Tab wird zusätzlich
|
||||
// das Befehlsfeld fokussiert, damit man den Zahlenwert direkt tippen kann.
|
||||
if (gripDragInfoRef.current) {
|
||||
const wasOpen = gripEditRef.current !== null;
|
||||
cycleGripEditFieldRef.current?.();
|
||||
if (!wasOpen) commandLineRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
commandLineRef.current?.focus();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nummern-Shortcuts für Zeichenwerkzeuge (Vectorworks-Stil): 1 Text, 2 Linie,
|
||||
* 3 Kreis, 4 Rechteck, 5 Polylinie, 6 Wand, 7 Decke, 8 Fenster, 9 Türe, 0 Raum.
|
||||
* Nur auf Geschoss-Tabs, nicht mit Modifikator (Ctrl/Cmd/Alt bleiben frei). Der
|
||||
* Handler wird einmal registriert und liest die aktuelle Auswahl-Funktion/
|
||||
* Ebenen-Art über ein Ref (sonst müsste der Effekt bei jedem Render neu
|
||||
* registrieren).
|
||||
*/
|
||||
export function useToolNumberShortcuts(
|
||||
onSelectTool: (id: ToolId) => void,
|
||||
isFloor: boolean,
|
||||
engineRef: RefObject<CommandEngine | null>,
|
||||
): void {
|
||||
const toolShortcutRef = useRef({ onSelectTool, floor: isFloor });
|
||||
toolShortcutRef.current = { onSelectTool, floor: isFloor };
|
||||
useEffect(() => {
|
||||
const MAP: Record<string, ToolId> = {
|
||||
"1": "text",
|
||||
"2": "line",
|
||||
"3": "circle",
|
||||
"4": "rect",
|
||||
"5": "polyline",
|
||||
"6": "wall",
|
||||
"7": "ceiling",
|
||||
"8": "window",
|
||||
"9": "door",
|
||||
"0": "room",
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
const el = e.target as HTMLElement | null;
|
||||
const isInput =
|
||||
!!el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable);
|
||||
if (isInput) {
|
||||
const isCmdLineInput = el.classList.contains("cmdline-input");
|
||||
const empty = isCmdLineInput && (el as HTMLInputElement).value === "";
|
||||
const eng = engineRef.current;
|
||||
const cmdLineFreeToSwitch =
|
||||
empty && !eng?.hasFields() && !eng?.acceptsFreeText();
|
||||
if (!cmdLineFreeToSwitch) return;
|
||||
}
|
||||
const id = MAP[e.key];
|
||||
if (!id) return;
|
||||
if (!toolShortcutRef.current.floor) return; // Werkzeuge nur auf Geschossen
|
||||
e.preventDefault();
|
||||
toolShortcutRef.current.onSelectTool(id);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tastatur für aktive Zeichenwerkzeuge: Esc verwirft den Entwurf (bzw. schaltet
|
||||
* im Ruhezustand zurück auf Auswahl), Enter schließt mehrteilige Werkzeuge ab,
|
||||
* Backspace nimmt den letzten Punkt zurück. In echten Eingaben inaktiv. Läuft
|
||||
* ein gekoppelter Befehl (Linie/Rechteck/Polylinie über die Engine), gehören
|
||||
* Esc/Enter/Backspace der Engine bzw. dem Befehlsfeld — der Legacy-Werkzeug-
|
||||
* Pfad bleibt stumm (keine doppelte Mechanik).
|
||||
*/
|
||||
export function useActiveToolKeys(
|
||||
activeTool: ToolId,
|
||||
toolCtx: ToolContext,
|
||||
engineRef: RefObject<CommandEngine | null>,
|
||||
toolStateRef: MutableRefObject<ToolState>,
|
||||
setDraft: (draft: ToolDraft | null) => void,
|
||||
setActiveTool: (id: ToolId) => void,
|
||||
setProject: (next: Project | ((p: Project) => Project)) => void,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (activeTool === "select") return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (isEditableTarget(e)) return;
|
||||
if (engineRef.current?.isActive()) return;
|
||||
const tool = getTool(activeTool);
|
||||
const drawing = toolStateRef.current.phase !== "idle";
|
||||
if (e.key === "Escape") {
|
||||
if (drawing) {
|
||||
const [st] = tool.onCancel(toolStateRef.current);
|
||||
toolStateRef.current = st;
|
||||
setDraft(null);
|
||||
} else {
|
||||
setActiveTool("select");
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === "Enter") {
|
||||
const [st, res] = tool.onCommitGesture(toolStateRef.current, toolCtx);
|
||||
toolStateRef.current = st;
|
||||
if (res.commit) setProject(res.commit);
|
||||
setDraft(null);
|
||||
if (res.done) toolStateRef.current = tool.init();
|
||||
e.preventDefault();
|
||||
} else if (e.key === "Backspace" && tool.onUndoPoint) {
|
||||
const [st, res] = tool.onUndoPoint(toolStateRef.current, toolCtx);
|
||||
toolStateRef.current = st;
|
||||
setDraft(res.draft);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [activeTool, toolCtx]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tastatur im Auswahl-Werkzeug: Ctrl/Cmd+A = alles auf der aktiven Ebene
|
||||
* wählen; Entf/Backspace = ALLE Selektierten (Wände + 2D-Elemente) löschen.
|
||||
* `hasSelectionToDelete`/`deleteSelection` bleiben in App.tsx (auch vom Menü
|
||||
* „Bearbeiten > Löschen" gebraucht) und werden hier nur aufgerufen.
|
||||
*/
|
||||
export function useSelectAllAndDeleteShortcut(opts: {
|
||||
activeTool: ToolId;
|
||||
activeTransform: ActiveTransform;
|
||||
project: Project;
|
||||
activeLevelId: string;
|
||||
selectedDrawingIds: string[];
|
||||
selectedWallIds: string[];
|
||||
selectedCeilingIds: string[];
|
||||
selectedOpeningIds: string[];
|
||||
selectedStairIds: string[];
|
||||
selectedRoomIds: string[];
|
||||
selectedExtrudedSolidIds: string[];
|
||||
selectedColumnIds: string[];
|
||||
selectedRoofIds: string[];
|
||||
selectedContextObjectIds: string[];
|
||||
selectedSectionLineId: string | null;
|
||||
setSelectedWallIds: (ids: string[]) => void;
|
||||
setSelectedDrawingIds: (ids: string[]) => void;
|
||||
setSelectedCeilingIds: (ids: string[]) => void;
|
||||
setSelectedStairIds: (ids: string[]) => void;
|
||||
setSelectedRoomIds: (ids: string[]) => void;
|
||||
setSelectedExtrudedSolidIds: (ids: string[]) => void;
|
||||
setSelectedColumnIds: (ids: string[]) => void;
|
||||
hasSelectionToDelete: () => boolean;
|
||||
deleteSelection: () => void;
|
||||
}): void {
|
||||
const {
|
||||
activeTool,
|
||||
activeTransform,
|
||||
project,
|
||||
activeLevelId,
|
||||
setSelectedWallIds,
|
||||
setSelectedDrawingIds,
|
||||
setSelectedCeilingIds,
|
||||
setSelectedStairIds,
|
||||
setSelectedRoomIds,
|
||||
setSelectedExtrudedSolidIds,
|
||||
setSelectedColumnIds,
|
||||
hasSelectionToDelete,
|
||||
deleteSelection,
|
||||
} = opts;
|
||||
useEffect(() => {
|
||||
if (activeTool !== "select") return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (isEditableTarget(e)) return;
|
||||
// Alles auswählen (Wände + Zeichnungen des aktiven Geschosses/der Ebene).
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === "a" || e.key === "A")) {
|
||||
const wallIds = project.walls
|
||||
.filter((w) => w.floorId === activeLevelId)
|
||||
.map((w) => w.id);
|
||||
const drawingIds = project.drawings2d
|
||||
.filter((d) => d.levelId === activeLevelId)
|
||||
.map((d) => d.id);
|
||||
const ceilingIds = (project.ceilings ?? [])
|
||||
.filter((c) => c.floorId === activeLevelId)
|
||||
.map((c) => c.id);
|
||||
const stairIds = (project.stairs ?? [])
|
||||
.filter((s) => s.floorId === activeLevelId)
|
||||
.map((s) => s.id);
|
||||
const roomIds = (project.rooms ?? [])
|
||||
.filter((r) => r.floorId === activeLevelId)
|
||||
.map((r) => r.id);
|
||||
const extrudedSolidIds = (project.extrudedSolids ?? [])
|
||||
.filter((s) => s.levelId === activeLevelId)
|
||||
.map((s) => s.id);
|
||||
const columnIds = (project.columns ?? [])
|
||||
.filter((c) => c.floorId === activeLevelId)
|
||||
.map((c) => c.id);
|
||||
if (
|
||||
wallIds.length ||
|
||||
drawingIds.length ||
|
||||
ceilingIds.length ||
|
||||
stairIds.length ||
|
||||
roomIds.length ||
|
||||
extrudedSolidIds.length ||
|
||||
columnIds.length
|
||||
) {
|
||||
setSelectedWallIds(wallIds);
|
||||
setSelectedDrawingIds(drawingIds);
|
||||
setSelectedCeilingIds(ceilingIds);
|
||||
setSelectedStairIds(stairIds);
|
||||
setSelectedRoomIds(roomIds);
|
||||
setSelectedExtrudedSolidIds(extrudedSolidIds);
|
||||
setSelectedColumnIds(columnIds);
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeTransform) return; // während einer Transformation nicht löschen
|
||||
if (hasSelectionToDelete()) {
|
||||
deleteSelection();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [
|
||||
opts.activeTool,
|
||||
opts.activeTransform,
|
||||
opts.selectedDrawingIds,
|
||||
opts.selectedWallIds,
|
||||
opts.selectedCeilingIds,
|
||||
opts.selectedOpeningIds,
|
||||
opts.selectedStairIds,
|
||||
opts.selectedExtrudedSolidIds,
|
||||
opts.selectedColumnIds,
|
||||
opts.selectedRoofIds,
|
||||
opts.selectedContextObjectIds,
|
||||
opts.selectedSectionLineId,
|
||||
opts.project.walls,
|
||||
opts.project.drawings2d,
|
||||
opts.project.ceilings,
|
||||
opts.project.roofs,
|
||||
opts.project.stairs,
|
||||
opts.project.extrudedSolids,
|
||||
opts.project.columns,
|
||||
opts.activeLevelId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ctrl/Cmd+S = Split, Ctrl/Cmd+J = Join der selektierten Drawing2D-Elemente.
|
||||
* Join läuft über DENSELBEN Engine-Befehl wie getipptes „join" (Sofort-
|
||||
* Befehl, autoRun). Split bleibt die direkte Aktion (`runSplit`, in App.tsx —
|
||||
* bleibt dort, da es auch für einen etwaigen Menü-Eintrag geteilt wird). Beide
|
||||
* rufen preventDefault (sonst Browser-Speichern/-Lesezeichen). Wände sind
|
||||
* ausgenommen (no-op). Tool-unabhängig, solange eine 2D-Auswahl steht.
|
||||
*/
|
||||
export function useSplitJoinShortcut(
|
||||
selectedDrawingIds: string[],
|
||||
engine: CommandEngine,
|
||||
runSplit: (ids: string[]) => void,
|
||||
setSelectedDrawingIds: (ids: string[]) => void,
|
||||
setSelectedWallIds: (ids: string[]) => void,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (!(e.ctrlKey || e.metaKey)) return;
|
||||
const k = e.key.toLowerCase();
|
||||
if (k !== "s" && k !== "j") return;
|
||||
if (isEditableTarget(e)) return;
|
||||
const ids = selectedDrawingIds;
|
||||
if (ids.length === 0) return; // nur 2D-Auswahl bedient; Wände → no-op
|
||||
e.preventDefault();
|
||||
|
||||
if (k === "s") {
|
||||
runSplit(ids);
|
||||
return;
|
||||
}
|
||||
// Join: exakt derselbe Befehl wie „join" tippen (Sofort-Befehl, autoRun).
|
||||
// Nach dem Verbinden zeigt die alte Auswahl auf ersetzte IDs → aufräumen.
|
||||
engine.start("join");
|
||||
setSelectedDrawingIds([]);
|
||||
setSelectedWallIds([]);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDrawingIds, engine]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ctrl/Cmd+C / Ctrl/Cmd+V = 2D-Zeichenelemente kopieren/einfügen. Kopieren legt
|
||||
* die GEWÄHLTEN Drawing2D tief in die Zwischenablage; Einfügen erzeugt Klone mit
|
||||
* neuen IDs auf dem AKTIVEN Geschoss (Zeichnungsebene) und wählt sie aus — so
|
||||
* wandert z. B. importiertes Mobiliar von einem Geschoss auf ein anderes. Im
|
||||
* Textfeld bleibt das native Copy/Paste; ohne 2D-Auswahl kein preventDefault.
|
||||
*/
|
||||
export function useDrawingClipboardShortcut(
|
||||
selectedDrawingIds: string[],
|
||||
drawingClipboard: Drawing2D[],
|
||||
setDrawingClipboard: (items: Drawing2D[]) => void,
|
||||
activeLevelId: string,
|
||||
drawings2d: Drawing2D[],
|
||||
importDrawings: (drawings: Drawing2D[]) => void,
|
||||
setSelectedDrawingIds: (ids: string[]) => void,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (!(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey) return;
|
||||
const k = e.key.toLowerCase();
|
||||
if (k !== "c" && k !== "v") return;
|
||||
if (isEditableTarget(e)) return;
|
||||
if (k === "c") {
|
||||
const ids = new Set(selectedDrawingIds);
|
||||
if (ids.size === 0) return; // nur bei 2D-Auswahl (sonst Browser-Copy)
|
||||
const picked = drawings2d.filter((d) => ids.has(d.id));
|
||||
if (picked.length === 0) return;
|
||||
e.preventDefault();
|
||||
setDrawingClipboard(picked.map((d) => structuredClone(d)));
|
||||
return;
|
||||
}
|
||||
// Einfügen: Klone auf dem aktiven Geschoss, neue IDs, danach ausgewählt.
|
||||
if (drawingClipboard.length === 0) return;
|
||||
e.preventDefault();
|
||||
const stamp = Date.now();
|
||||
const pasted: Drawing2D[] = drawingClipboard.map((d, i) => ({
|
||||
...structuredClone(d),
|
||||
id: `drawing-${stamp}-${i}`,
|
||||
levelId: activeLevelId,
|
||||
}));
|
||||
importDrawings(pasted);
|
||||
setSelectedDrawingIds(pasted.map((p) => p.id));
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDrawingIds, drawingClipboard, activeLevelId, drawings2d]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ctrl/Cmd+Shift+U/D/I = Boolean Vereinigung/Subtraktion/Schnitt der gewählten
|
||||
* geschlossenen Drawing2D. preventDefault (sonst Browser-Aktionen). Braucht ≥2
|
||||
* geschlossene Flächen; sonst No-op. (Menü spiegelt diese Aktionen.) `applyBoolean`
|
||||
* bleibt in App.tsx (auch vom Kontextmenü gebraucht).
|
||||
*/
|
||||
export function useBooleanOpsShortcut(
|
||||
selectedDrawingIds: string[],
|
||||
drawings2d: Drawing2D[],
|
||||
applyBoolean: (op: (srcs: Drawing2D[]) => BooleanResult) => void,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (!(e.ctrlKey || e.metaKey) || !e.shiftKey) return;
|
||||
const k = e.key.toLowerCase();
|
||||
if (k !== "u" && k !== "d" && k !== "i") return;
|
||||
if (isEditableTarget(e)) return;
|
||||
if (selectedDrawingIds.length < 2) return;
|
||||
e.preventDefault();
|
||||
if (k === "u") applyBoolean(unionDrawings);
|
||||
else if (k === "d") applyBoolean(differenceDrawings);
|
||||
else applyBoolean(intersectionDrawings);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDrawingIds, drawings2d]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformations-Shortcuts (nur im Auswahl-Werkzeug):
|
||||
* m = Bewegen, s = Spiegeln, d = Drehen (startet auf der Auswahl);
|
||||
* während einer Transformation: u/i/o/p = Modus (move/copy/array/distribute),
|
||||
* Esc = abbrechen. O/P fragen die Kopienanzahl ab. „q" = Messen (Distanz/
|
||||
* Winkel) — braucht KEINE Auswahl.
|
||||
*/
|
||||
export function useTransformShortcuts(
|
||||
activeTool: ToolId,
|
||||
activeTransform: ActiveTransform,
|
||||
setActiveTransform: (
|
||||
next: ActiveTransform | ((t: ActiveTransform) => ActiveTransform),
|
||||
) => void,
|
||||
selectedWallIds: string[],
|
||||
selectedDrawingId: string | null,
|
||||
selectedExtrudedSolidId: string | null,
|
||||
selectedColumnId: string | null,
|
||||
engine: CommandEngine,
|
||||
setDraft: (draft: ToolDraft | null) => void,
|
||||
requestCopyCount: (mode: "array" | "distribute") => void,
|
||||
onRunCommand: (name: string) => void,
|
||||
commandLineRef: RefObject<CommandLineHandle | null>,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (activeTool !== "select") return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (isEditableTarget(e)) return;
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
if (activeTransform) {
|
||||
const k = e.key.toLowerCase();
|
||||
if (k === "u") setActiveTransform((t) => (t ? { ...t, copyMode: "move" } : t));
|
||||
else if (k === "i") setActiveTransform((t) => (t ? { ...t, copyMode: "copy" } : t));
|
||||
else if (k === "o") {
|
||||
requestCopyCount("array");
|
||||
} else if (k === "p") {
|
||||
requestCopyCount("distribute");
|
||||
} else if (e.key === "Escape") {
|
||||
setActiveTransform(null);
|
||||
setDraft(null);
|
||||
} else return;
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
// „q" = Messen (Distanz/Winkel) — braucht KEINE Auswahl, daher vor hasSel.
|
||||
if (e.key.toLowerCase() === "q") {
|
||||
onRunCommand("measure");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Transformierbar sind Wände, 2D-Zeichnungen, extrudierte Körper und Stützen
|
||||
// (dieselben Arten wie transformSel). Öffnungen/Treppen laufen bewusst NICHT
|
||||
// hierüber, sondern über ihre eigenen Bewegpunkte (Grips) — sie sind wand-
|
||||
// bzw. geschossgebunden.
|
||||
const hasSel =
|
||||
selectedWallIds.length > 0 ||
|
||||
!!selectedDrawingId ||
|
||||
!!selectedExtrudedSolidId ||
|
||||
!!selectedColumnId;
|
||||
if (!hasSel) return;
|
||||
const k = e.key.toLowerCase();
|
||||
// Bewegen/Spiegeln/Drehen laufen ALLE über die interaktive Transformation
|
||||
// (`activeTransform`) mit den U/I/O/P-Kopiermodi (Bewegen/Kopie/Reihe/
|
||||
// Verteilen). Prompt + Optionen + Live-Feld speisen die Command-Line; die
|
||||
// Kürzel U/I/O/P wirken dort auch bei fokussiertem Eingabefeld (options.key).
|
||||
const op: TransformOp | null =
|
||||
k === "m" ? "move" : k === "s" ? "mirror" : k === "d" ? "rotate" : null;
|
||||
if (op) {
|
||||
setActiveTransform({ op, copyMode: "move", count: 1, points: [] });
|
||||
setDraft(null);
|
||||
requestAnimationFrame(() => commandLineRef.current?.focus());
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [
|
||||
activeTool,
|
||||
activeTransform,
|
||||
selectedWallIds,
|
||||
selectedDrawingId,
|
||||
selectedExtrudedSolidId,
|
||||
selectedColumnId,
|
||||
engine,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user