2D: Tab in ein Griff-/Kanten-Feld friert die Maus ein, bis der erste Wert getippt ist
Nutzer-Wunsch: "wenn ich mit Tab in die Werte springe, soll die Maus dann nicht mehr steuern, bis man den ersten Wert eingegeben hat -- das kann man aber mit Esc lösen." Bisher folgte der Punkt/die Kante nach Tab weiter der Maus, solange noch kein Wert gelockt war -- ein Zittern der Hand beim Greifen zur Tastatur konnte die Position also noch verändern, bevor überhaupt getippt wurde. Neue gripFrozenPointRef/edgeFrozenPointRef (useGripEditing.ts): beim ÖFFNEN des Feld-Controllers (erstes Tab, noch kein vorheriges Feld offen) wird die AKTUELLE Position eingefroren; onGripMove/onEdgeMove halten den Punkt/die Kante bei aktivem Feld-Controller OHNE Lock exakt dort fest, unabhängig von der Mausposition. Der erste getippte Wert (submitGripEditValue/submitEdgeEditValue) löst das Einfrieren -- ab da gilt wieder die bisherige Locked/Live-Mischung (gelockte Felder fest, ungelockte folgen der Maus). Neue closeGripEditField/closeEdgeEditField-Funktionen lösen das Einfrieren explizit auf und schließen NUR den Feld-Prompt (Punkt/Kante bleibt bewaffnet, Maus steuert wieder normal) -- ersetzen in App.tsx die bisherigen direkten setGripEdit(null)/setEdgeEdit(null)-Aufrufe im CommandLine-onCancel (Escape MIT Fokus in der Befehlszeile). Das bestehende, GLOBALE Escape (ohne Fokus in einem Eingabefeld) bleibt unverändert: es verwirft den ganzen Griff-/Kanten-Drag komplett auf die Ursprungsposition (PlanView.tsx, voriger Commit) -- zwei bewusst verschiedene Eskalationsstufen. tsc/vitest 922/922 grün.
This commit is contained in:
+4
-4
@@ -1290,15 +1290,15 @@ export default function App() {
|
|||||||
edgeGrips,
|
edgeGrips,
|
||||||
gripHandlers,
|
gripHandlers,
|
||||||
gripEdit,
|
gripEdit,
|
||||||
setGripEdit,
|
|
||||||
gripEditFields,
|
gripEditFields,
|
||||||
cycleGripEditField,
|
cycleGripEditField,
|
||||||
submitGripEditValue,
|
submitGripEditValue,
|
||||||
|
closeGripEditField,
|
||||||
edgeEdit,
|
edgeEdit,
|
||||||
setEdgeEdit,
|
|
||||||
edgeEditFields,
|
edgeEditFields,
|
||||||
cycleEdgeEditField,
|
cycleEdgeEditField,
|
||||||
submitEdgeEditValue,
|
submitEdgeEditValue,
|
||||||
|
closeEdgeEditField,
|
||||||
gripDragInfoRef,
|
gripDragInfoRef,
|
||||||
edgeDragInfoRef,
|
edgeDragInfoRef,
|
||||||
} = useGripEditing({
|
} = useGripEditing({
|
||||||
@@ -2886,9 +2886,9 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
onCancel={() =>
|
onCancel={() =>
|
||||||
gripEdit
|
gripEdit
|
||||||
? setGripEdit(null)
|
? closeGripEditField()
|
||||||
: edgeEdit
|
: edgeEdit
|
||||||
? setEdgeEdit(null)
|
? closeEdgeEditField()
|
||||||
: activeTransform
|
: activeTransform
|
||||||
? cancelTransform()
|
? cancelTransform()
|
||||||
: engine.cancel()
|
: engine.cancel()
|
||||||
|
|||||||
+78
-22
@@ -540,26 +540,36 @@ export function useGripEditing(opts: {
|
|||||||
|
|
||||||
// Stabiler Ref auf cycleGripEditField (der globale Tab-Handler hat leere Deps).
|
// Stabiler Ref auf cycleGripEditField (der globale Tab-Handler hat leere Deps).
|
||||||
const cycleGripEditFieldRef = useRef<() => void>(() => {});
|
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<Vec2 | null>(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 cycleGripEditField = () => {
|
||||||
const info = gripDragInfoRef.current;
|
const info = gripDragInfoRef.current;
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
const anchor = resolveGripAnchor(info.index);
|
const anchor = resolveGripAnchor(info.index);
|
||||||
if (!anchor) return; // ohne Anker keine polare Eingabe möglich
|
if (!anchor) return; // ohne Anker keine polare Eingabe möglich
|
||||||
setGripEdit((prev) =>
|
setGripEdit((prev) => {
|
||||||
prev
|
if (prev) return { ...prev, active: prev.active === "length" ? "angle" : "length" };
|
||||||
? { ...prev, active: prev.active === "length" ? "angle" : "length" }
|
gripFrozenPointRef.current = grips[info.index];
|
||||||
: { anchor, active: "length", locks: {} },
|
return { anchor, active: "length", locks: {} };
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
cycleGripEditFieldRef.current = cycleGripEditField;
|
cycleGripEditFieldRef.current = cycleGripEditField;
|
||||||
|
|
||||||
// Getippte Zahl im Vertex-Drag: aktives Feld locken, aufs nächste vorrücken und
|
// 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 submitGripEditValue = (value: number) => {
|
||||||
const ge = gripEditRef.current;
|
const ge = gripEditRef.current;
|
||||||
const info = gripDragInfoRef.current;
|
const info = gripDragInfoRef.current;
|
||||||
if (!ge || !info) return;
|
if (!ge || !info) return;
|
||||||
|
gripFrozenPointRef.current = null;
|
||||||
const locks = { ...ge.locks, [ge.active]: value };
|
const locks = { ...ge.locks, [ge.active]: value };
|
||||||
const next = ge.active === "length" ? "angle" : "length";
|
const next = ge.active === "length" ? "angle" : "length";
|
||||||
setGripEdit({ ...ge, locks, active: next });
|
setGripEdit({ ...ge, locks, active: next });
|
||||||
@@ -568,6 +578,14 @@ export function useGripEditing(opts: {
|
|||||||
applyGrip(info.index, pt);
|
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-
|
// Dasselbe Feld-Controller-Muster wie oben (gripEdit), jetzt für einen Kanten-
|
||||||
// Griff (Dreieck-Anfasser, „Seite parallel verschieben"). Ein Feld genügt für
|
// Griff (Dreieck-Anfasser, „Seite parallel verschieben"). Ein Feld genügt für
|
||||||
// geführte Kanten (rect/geschlossene Polylinie/Wand — Bewegung ist auf die
|
// geführte Kanten (rect/geschlossene Polylinie/Wand — Bewegung ist auf die
|
||||||
@@ -640,19 +658,23 @@ export function useGripEditing(opts: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cycleEdgeEditFieldRef = useRef<() => void>(() => {});
|
const cycleEdgeEditFieldRef = useRef<() => void>(() => {});
|
||||||
// Tab im Kanten-Drag: Feld-Controller öffnen; bei freien Kanten zykelt es
|
// Eingefrorene Zielposition, solange das Feld offen, aber noch KEIN Wert
|
||||||
// zusätzlich Länge ↔ Winkel (geführte Kanten haben nur das eine Feld).
|
// getippt ist — dasselbe Prinzip wie gripFrozenPointRef oben.
|
||||||
|
const edgeFrozenPointRef = useRef<Vec2 | null>(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 cycleEdgeEditField = () => {
|
||||||
const info = edgeDragInfoRef.current;
|
const info = edgeDragInfoRef.current;
|
||||||
if (!info) return;
|
if (!info) return;
|
||||||
const { edge } = info;
|
const { edge, raw } = info;
|
||||||
setEdgeEdit((prev) =>
|
setEdgeEdit((prev) => {
|
||||||
prev
|
if (prev) {
|
||||||
? edge.free
|
return edge.free ? { ...prev, active: prev.active === "length" ? "angle" : "length" } : prev;
|
||||||
? { ...prev, active: prev.active === "length" ? "angle" : "length" }
|
}
|
||||||
: prev
|
edgeFrozenPointRef.current = edgeEditPoint(edge, edge.mid, {}, raw);
|
||||||
: { anchor: edge.mid, active: "length", locks: {} },
|
return { anchor: edge.mid, active: "length", locks: {} };
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
cycleEdgeEditFieldRef.current = cycleEdgeEditField;
|
cycleEdgeEditFieldRef.current = cycleEdgeEditField;
|
||||||
|
|
||||||
@@ -675,6 +697,7 @@ export function useGripEditing(opts: {
|
|||||||
const ee = edgeEditRef.current;
|
const ee = edgeEditRef.current;
|
||||||
const info = edgeDragInfoRef.current;
|
const info = edgeDragInfoRef.current;
|
||||||
if (!ee || !info) return;
|
if (!ee || !info) return;
|
||||||
|
edgeFrozenPointRef.current = null;
|
||||||
const { edge, raw } = info;
|
const { edge, raw } = info;
|
||||||
const locks = { ...ee.locks, [ee.active]: value };
|
const locks = { ...ee.locks, [ee.active]: value };
|
||||||
const next = edge.free ? (ee.active === "length" ? "angle" : "length") : ee.active;
|
const next = edge.free ? (ee.active === "length" ? "angle" : "length") : ee.active;
|
||||||
@@ -683,6 +706,13 @@ export function useGripEditing(opts: {
|
|||||||
applyEdgeToward(edge, ee.anchor, pt);
|
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
|
// 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.
|
// auf die Wand/2D-Aktionen. So teilen Plan- UND 3D-Griffe denselben Pfad.
|
||||||
const applyGrip = (index: number, pt: Vec2) => {
|
const applyGrip = (index: number, pt: Vec2) => {
|
||||||
@@ -777,8 +807,10 @@ export function useGripEditing(opts: {
|
|||||||
moveCoincidentRef.current = null; // Körper-Koinzidenz-Gruppe verwerfen
|
moveCoincidentRef.current = null; // Körper-Koinzidenz-Gruppe verwerfen
|
||||||
gripDragInfoRef.current = null; // (c) Vertex-Drag-Info verwerfen
|
gripDragInfoRef.current = null; // (c) Vertex-Drag-Info verwerfen
|
||||||
gripOriginRef.current = null; // Flucht-Bezugspunkt verwerfen
|
gripOriginRef.current = null; // Flucht-Bezugspunkt verwerfen
|
||||||
|
gripFrozenPointRef.current = null; // Einfrieren verwerfen
|
||||||
setGripEdit(null); // (c) Feld-Controller schließen
|
setGripEdit(null); // (c) Feld-Controller schließen
|
||||||
edgeDragInfoRef.current = null; // Kanten-Drag-Info verwerfen
|
edgeDragInfoRef.current = null; // Kanten-Drag-Info verwerfen
|
||||||
|
edgeFrozenPointRef.current = null; // Einfrieren verwerfen
|
||||||
setEdgeEdit(null); // Kanten-Feld-Controller schließen
|
setEdgeEdit(null); // Kanten-Feld-Controller schließen
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -788,16 +820,27 @@ export function useGripEditing(opts: {
|
|||||||
// AKTUELLE — hier noch unveränderte — Position als Flucht-Bezugspunkt
|
// AKTUELLE — hier noch unveränderte — Position als Flucht-Bezugspunkt
|
||||||
// verankern (s. Kommentar an gripOriginRef).
|
// verankern (s. Kommentar an gripOriginRef).
|
||||||
if (gripDragInfoRef.current === null) gripOriginRef.current = grips[index];
|
if (gripDragInfoRef.current === null) gripOriginRef.current = grips[index];
|
||||||
// (c) Vertex-Drag mit aktivem Feld-Controller + Locks: den Zielpunkt polar
|
// (c) Vertex-Drag mit aktivem Feld-Controller: solange EIN Wert gelockt
|
||||||
// aus Anker + gelockter Länge/Winkel bilden (ungelockt folgt dem Cursor).
|
// 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;
|
const ge = gripEditRef.current;
|
||||||
gripDragInfoRef.current = { index, raw };
|
gripDragInfoRef.current = { index, raw };
|
||||||
if (ge && (ge.locks.length != null || ge.locks.angle != null)) {
|
if (ge) {
|
||||||
|
const hasLock = ge.locks.length != null || ge.locks.angle != null;
|
||||||
|
if (hasLock) {
|
||||||
const pt = gripEditPoint(ge.anchor, ge.locks, raw);
|
const pt = gripEditPoint(ge.anchor, ge.locks, raw);
|
||||||
applyGrip(index, pt);
|
applyGrip(index, pt);
|
||||||
setDraft({ preview: [], vertices: [], snap: null, hud: hudFor(index, pt) });
|
setDraft({ preview: [], vertices: [], snap: null, hud: hudFor(index, pt) });
|
||||||
return;
|
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
|
// Mehrfachauswahl: das Element, dem DIESER Griff gehört (nicht zwingend
|
||||||
// das singuläre selectedDrawingId/selectedWallId) — von den eigenen
|
// das singuläre selectedDrawingId/selectedWallId) — von den eigenen
|
||||||
// Snap-Kandidaten ausschließen (sonst fängt der Punkt an sich selbst).
|
// Snap-Kandidaten ausschließen (sonst fängt der Punkt an sich selbst).
|
||||||
@@ -918,13 +961,24 @@ export function useGripEditing(opts: {
|
|||||||
const anchor = edge.mid; // Momentaufnahme vom Grab, ändert sich während des Drags nicht
|
const anchor = edge.mid; // Momentaufnahme vom Grab, ändert sich während des Drags nicht
|
||||||
const ee = edgeEditRef.current;
|
const ee = edgeEditRef.current;
|
||||||
// Feld-Controller mit Lock aktiv: Zielpunkt aus Anker + gelocktem Wert
|
// Feld-Controller mit Lock aktiv: Zielpunkt aus Anker + gelocktem Wert
|
||||||
// (Maus wirkt für das gelockte Feld nicht mehr — Lock dominiert).
|
// (Maus wirkt für das gelockte Feld nicht mehr — Lock dominiert). Feld
|
||||||
if (ee && (ee.locks.length != null || ee.locks.angle != null)) {
|
// 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);
|
const pt = edgeEditPoint(edge, anchor, ee.locks, raw);
|
||||||
applyEdgeToward(edge, anchor, pt);
|
applyEdgeToward(edge, anchor, pt);
|
||||||
setDraft({ preview: [], vertices: [], snap: null, hud: edgeHudFor(edge, anchor, pt) });
|
setDraft({ preview: [], vertices: [], snap: null, hud: edgeHudFor(edge, anchor, pt) });
|
||||||
return;
|
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) {
|
if (edge.free) {
|
||||||
// Keine Geometrie-Snap-Projektion (keine feste Kanten-Normale, an die
|
// Keine Geometrie-Snap-Projektion (keine feste Kanten-Normale, an die
|
||||||
// sich ein gefangener Punkt anlehnen könnte) — der Cursor bewegt den
|
// sich ein gefangener Punkt anlehnen könnte) — der Cursor bewegt den
|
||||||
@@ -1002,11 +1056,13 @@ export function useGripEditing(opts: {
|
|||||||
gripEditFields,
|
gripEditFields,
|
||||||
cycleGripEditField,
|
cycleGripEditField,
|
||||||
submitGripEditValue,
|
submitGripEditValue,
|
||||||
|
closeGripEditField,
|
||||||
edgeEdit,
|
edgeEdit,
|
||||||
setEdgeEdit,
|
setEdgeEdit,
|
||||||
edgeEditFields,
|
edgeEditFields,
|
||||||
cycleEdgeEditField,
|
cycleEdgeEditField,
|
||||||
submitEdgeEditValue,
|
submitEdgeEditValue,
|
||||||
|
closeEdgeEditField,
|
||||||
// Für useToolNumberShortcuts: solange ein Punkt- ODER Kanten-Griff
|
// Für useToolNumberShortcuts: solange ein Punkt- ODER Kanten-Griff
|
||||||
// bewaffnet ist, dürfen Zifferntasten NICHT als Werkzeug-Wechsel
|
// bewaffnet ist, dürfen Zifferntasten NICHT als Werkzeug-Wechsel
|
||||||
// interpretiert werden (auch nicht, wenn das Befehlsfeld fokussiert und
|
// interpretiert werden (auch nicht, wenn das Befehlsfeld fokussiert und
|
||||||
|
|||||||
Reference in New Issue
Block a user