diff --git a/src/App.tsx b/src/App.tsx index 9edca0b..cd7c661 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1290,15 +1290,15 @@ export default function App() { edgeGrips, gripHandlers, gripEdit, - setGripEdit, gripEditFields, cycleGripEditField, submitGripEditValue, + closeGripEditField, edgeEdit, - setEdgeEdit, edgeEditFields, cycleEdgeEditField, submitEdgeEditValue, + closeEdgeEditField, gripDragInfoRef, edgeDragInfoRef, } = useGripEditing({ @@ -2886,9 +2886,9 @@ export default function App() { } onCancel={() => gripEdit - ? setGripEdit(null) + ? closeGripEditField() : edgeEdit - ? setEdgeEdit(null) + ? closeEdgeEditField() : activeTransform ? cancelTransform() : engine.cancel() diff --git a/src/state/useGripEditing.ts b/src/state/useGripEditing.ts index d782427..a4e67e2 100644 --- a/src/state/useGripEditing.ts +++ b/src/state/useGripEditing.ts @@ -540,26 +540,36 @@ export function useGripEditing(opts: { // Stabiler Ref auf cycleGripEditField (der globale Tab-Handler hat leere Deps). const cycleGripEditFieldRef = useRef<() => void>(() => {}); - // Tab im Vertex-Drag: Feld-Controller öffnen/zyklen (length ↔ angle). + // Eingefrorene Zielposition, solange das Feld offen, aber noch KEIN Wert + // getippt ist (s. Kommentar an onGripMove). null = nicht eingefroren. + const gripFrozenPointRef = useRef(null); + // Tab im Vertex-Drag: Feld-Controller öffnen/zyklen (length ↔ angle). Beim + // ÖFFNEN (noch kein gripEdit) friert die AKTUELLE Position ein — Nutzer- + // Wunsch: „wenn ich mit Tab in die Werte springe, soll die Maus nicht mehr + // steuern, bis man den ersten Wert eingegeben hat" (verhindert, dass ein + // Zittern der Hand beim Greifen zur Tastatur den Punkt noch verschiebt). const cycleGripEditField = () => { const info = gripDragInfoRef.current; if (!info) return; const anchor = resolveGripAnchor(info.index); if (!anchor) return; // ohne Anker keine polare Eingabe möglich - setGripEdit((prev) => - prev - ? { ...prev, active: prev.active === "length" ? "angle" : "length" } - : { anchor, active: "length", locks: {} }, - ); + setGripEdit((prev) => { + if (prev) return { ...prev, active: prev.active === "length" ? "angle" : "length" }; + gripFrozenPointRef.current = grips[info.index]; + return { anchor, active: "length", locks: {} }; + }); }; cycleGripEditFieldRef.current = cycleGripEditField; // Getippte Zahl im Vertex-Drag: aktives Feld locken, aufs nächste vorrücken und - // den Zielpunkt sofort mit dem gelockten Wert neu setzen. + // den Zielpunkt sofort mit dem gelockten Wert neu setzen. Der erste getippte + // Wert löst das Einfrieren (s. o.) — ab jetzt gilt wieder die normale + // Locked/Live-Mischung (gelockte Felder fest, ungelockte folgen der Maus). const submitGripEditValue = (value: number) => { const ge = gripEditRef.current; const info = gripDragInfoRef.current; if (!ge || !info) return; + gripFrozenPointRef.current = null; const locks = { ...ge.locks, [ge.active]: value }; const next = ge.active === "length" ? "angle" : "length"; setGripEdit({ ...ge, locks, active: next }); @@ -568,6 +578,14 @@ export function useGripEditing(opts: { applyGrip(info.index, pt); }; + // Feld-Controller schließen (Escape MIT Fokus in der Befehlszeile, s. + // App.tsx) — löst NUR das Einfrieren/den Feld-Prompt, beendet NICHT den + // ganzen Griff-Drag (der bleibt bewaffnet, Maus steuert wieder normal). + const closeGripEditField = () => { + setGripEdit(null); + gripFrozenPointRef.current = null; + }; + // Dasselbe Feld-Controller-Muster wie oben (gripEdit), jetzt für einen Kanten- // Griff (Dreieck-Anfasser, „Seite parallel verschieben"). Ein Feld genügt für // geführte Kanten (rect/geschlossene Polylinie/Wand — Bewegung ist auf die @@ -640,19 +658,23 @@ export function useGripEditing(opts: { }; const cycleEdgeEditFieldRef = useRef<() => void>(() => {}); - // Tab im Kanten-Drag: Feld-Controller öffnen; bei freien Kanten zykelt es - // zusätzlich Länge ↔ Winkel (geführte Kanten haben nur das eine Feld). + // Eingefrorene Zielposition, solange das Feld offen, aber noch KEIN Wert + // getippt ist — dasselbe Prinzip wie gripFrozenPointRef oben. + const edgeFrozenPointRef = useRef(null); + // Tab im Kanten-Drag: Feld-Controller öffnen (friert dabei die aktuelle + // Position ein, s. o.); bei freien Kanten zykelt es zusätzlich Länge ↔ + // Winkel (geführte Kanten haben nur das eine Feld). const cycleEdgeEditField = () => { const info = edgeDragInfoRef.current; if (!info) return; - const { edge } = info; - setEdgeEdit((prev) => - prev - ? edge.free - ? { ...prev, active: prev.active === "length" ? "angle" : "length" } - : prev - : { anchor: edge.mid, active: "length", locks: {} }, - ); + const { edge, raw } = info; + setEdgeEdit((prev) => { + if (prev) { + return edge.free ? { ...prev, active: prev.active === "length" ? "angle" : "length" } : prev; + } + edgeFrozenPointRef.current = edgeEditPoint(edge, edge.mid, {}, raw); + return { anchor: edge.mid, active: "length", locks: {} }; + }); }; cycleEdgeEditFieldRef.current = cycleEdgeEditField; @@ -675,6 +697,7 @@ export function useGripEditing(opts: { const ee = edgeEditRef.current; const info = edgeDragInfoRef.current; if (!ee || !info) return; + edgeFrozenPointRef.current = null; const { edge, raw } = info; const locks = { ...ee.locks, [ee.active]: value }; const next = edge.free ? (ee.active === "length" ? "angle" : "length") : ee.active; @@ -683,6 +706,13 @@ export function useGripEditing(opts: { applyEdgeToward(edge, ee.anchor, pt); }; + // Feld-Controller schließen (Escape MIT Fokus in der Befehlszeile) — löst + // nur das Einfrieren/den Feld-Prompt, beendet NICHT den Kanten-Drag. + const closeEdgeEditField = () => { + setEdgeEdit(null); + edgeFrozenPointRef.current = null; + }; + // Editier-Dispatch: bei gewählter Decke auf die Decken-Aktionen umleiten, sonst // auf die Wand/2D-Aktionen. So teilen Plan- UND 3D-Griffe denselben Pfad. const applyGrip = (index: number, pt: Vec2) => { @@ -777,8 +807,10 @@ export function useGripEditing(opts: { moveCoincidentRef.current = null; // Körper-Koinzidenz-Gruppe verwerfen gripDragInfoRef.current = null; // (c) Vertex-Drag-Info verwerfen gripOriginRef.current = null; // Flucht-Bezugspunkt verwerfen + gripFrozenPointRef.current = null; // Einfrieren verwerfen setGripEdit(null); // (c) Feld-Controller schließen edgeDragInfoRef.current = null; // Kanten-Drag-Info verwerfen + edgeFrozenPointRef.current = null; // Einfrieren verwerfen setEdgeEdit(null); // Kanten-Feld-Controller schließen }; @@ -788,15 +820,26 @@ export function useGripEditing(opts: { // AKTUELLE — hier noch unveränderte — Position als Flucht-Bezugspunkt // verankern (s. Kommentar an gripOriginRef). if (gripDragInfoRef.current === null) gripOriginRef.current = grips[index]; - // (c) Vertex-Drag mit aktivem Feld-Controller + Locks: den Zielpunkt polar - // aus Anker + gelockter Länge/Winkel bilden (ungelockt folgt dem Cursor). + // (c) Vertex-Drag mit aktivem Feld-Controller: solange EIN Wert gelockt + // ist, gilt die polare Locked/Live-Mischung wie bisher. Ist das Feld + // offen, aber NOCH KEIN Wert getippt, hält `gripFrozenPointRef` den + // Punkt fest (Maus wirkungslos) — s. Kommentar an cycleGripEditField. const ge = gripEditRef.current; gripDragInfoRef.current = { index, raw }; - if (ge && (ge.locks.length != null || ge.locks.angle != null)) { - const pt = gripEditPoint(ge.anchor, ge.locks, raw); - applyGrip(index, pt); - setDraft({ preview: [], vertices: [], snap: null, hud: hudFor(index, pt) }); - return; + if (ge) { + const hasLock = ge.locks.length != null || ge.locks.angle != null; + if (hasLock) { + const pt = gripEditPoint(ge.anchor, ge.locks, raw); + applyGrip(index, pt); + setDraft({ preview: [], vertices: [], snap: null, hud: hudFor(index, pt) }); + return; + } + if (gripFrozenPointRef.current) { + const pt = gripFrozenPointRef.current; + applyGrip(index, pt); + setDraft({ preview: [], vertices: [], snap: null, hud: hudFor(index, pt) }); + return; + } } // Mehrfachauswahl: das Element, dem DIESER Griff gehört (nicht zwingend // das singuläre selectedDrawingId/selectedWallId) — von den eigenen @@ -918,12 +961,23 @@ export function useGripEditing(opts: { const anchor = edge.mid; // Momentaufnahme vom Grab, ändert sich während des Drags nicht const ee = edgeEditRef.current; // Feld-Controller mit Lock aktiv: Zielpunkt aus Anker + gelocktem Wert - // (Maus wirkt für das gelockte Feld nicht mehr — Lock dominiert). - if (ee && (ee.locks.length != null || ee.locks.angle != null)) { - const pt = edgeEditPoint(edge, anchor, ee.locks, raw); - applyEdgeToward(edge, anchor, pt); - setDraft({ preview: [], vertices: [], snap: null, hud: edgeHudFor(edge, anchor, pt) }); - return; + // (Maus wirkt für das gelockte Feld nicht mehr — Lock dominiert). Feld + // offen, aber noch kein Wert getippt: `edgeFrozenPointRef` hält den + // Punkt fest (Maus wirkungslos) — s. Kommentar an cycleEdgeEditField. + if (ee) { + const hasLock = ee.locks.length != null || ee.locks.angle != null; + if (hasLock) { + const pt = edgeEditPoint(edge, anchor, ee.locks, raw); + applyEdgeToward(edge, anchor, pt); + setDraft({ preview: [], vertices: [], snap: null, hud: edgeHudFor(edge, anchor, pt) }); + return; + } + if (edgeFrozenPointRef.current) { + const pt = edgeFrozenPointRef.current; + applyEdgeToward(edge, anchor, pt); + setDraft({ preview: [], vertices: [], snap: null, hud: edgeHudFor(edge, anchor, pt) }); + return; + } } if (edge.free) { // Keine Geometrie-Snap-Projektion (keine feste Kanten-Normale, an die @@ -1002,11 +1056,13 @@ export function useGripEditing(opts: { gripEditFields, cycleGripEditField, submitGripEditValue, + closeGripEditField, edgeEdit, setEdgeEdit, edgeEditFields, cycleEdgeEditField, submitEdgeEditValue, + closeEdgeEditField, // Für useToolNumberShortcuts: solange ein Punkt- ODER Kanten-Griff // bewaffnet ist, dürfen Zifferntasten NICHT als Werkzeug-Wechsel // interpretiert werden (auch nicht, wenn das Befehlsfeld fokussiert und