From 8f85e135ee92296a6a6d5389f1ee421c6b4b5ccf Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 11 Jul 2026 00:52:52 +0200 Subject: [PATCH] Zeichnen: getippte Werte live im Cursor-HUD + direktes Zahlen-Tippen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VW-Verhalten vervollständigt: das Cursor-Kästchen spiegelt jetzt den Feld- Status der Befehlszeile (Tab-Feld-Zyklus) — der getippte Wert erscheint SOFORT im aktiven Feld oben am Cursor UND unten im Befehl; gelockte Werte fett/dunkel, das aktive Tab-Ziel als blaue Pille mit weissem Text. Tab wechselt Länge ↔ Winkel (global, auch ohne Fokus), und eine Ziffer (oder . , -) beim Zeichnen startet die Werteingabe direkt (beginTyping fokussiert die Befehlszeile mit dem ersten Zeichen — kein Klick nötig). - PlanView: HudFieldsState + Segment-HUD (aktiv/gelockt/getippt je Feld) - CommandLine: onTextChange (Live-Echo) + Handle beginTyping(seed) - App: cmdTyped-State, hudFields an alle PlanView-Pfade, globaler Ziffern-Handler vor dem Tab-Zyklus 737/737 grün. --- src/App.tsx | 51 +++++++++++++++++-- src/plan/PlanView.tsx | 112 +++++++++++++++++++++++++++++++++++------ src/styles.css | 12 +++++ src/ui/CommandLine.tsx | 24 ++++++++- 4 files changed, 178 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 85bacf6..c42d840 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -93,6 +93,7 @@ import type { PlanSelection, MarqueeSelection, GripHandlers, + HudFieldsState, } from "./plan/PlanView"; import { Viewport3D } from "./viewport/Viewport3D"; import type { DisplayResolver, ViewportContextInfo } from "./viewport/Viewport3D"; @@ -1017,6 +1018,21 @@ export default function App() { void engineTick; const engineView = engine.view(); const commandActive = engineView.active; + // Live-Echo des Befehlszeilen-Texts fürs Cursor-HUD: der getippte Wert + // erscheint sofort im aktiven Tab-Feld am Cursor (VW-Verhalten). + const [cmdTyped, setCmdTyped] = useState(""); + const hudFields = + commandActive && engineView.fields.length > 0 + ? { + fields: engineView.fields.map((f) => ({ + id: f.id, + value: f.value, + locked: f.locked, + active: f.active, + })), + typed: cmdTyped, + } + : null; // ── GUI-Werkzeug ↔ Befehls-Engine (ein Eingabe-Hub) ─────────────────────── // Ein Klick auf ein gekoppeltes Zeichen-Werkzeug (Wand/Linie/Rechteck/Polylinie) @@ -1100,12 +1116,28 @@ export default function App() { // (das CommandLine-Input selbst behandelt Tab eigenständig). useEffect(() => { const onKey = (e: KeyboardEvent) => { - if (e.key !== "Tab") return; const el = e.target as HTMLElement | null; + const inInput = + !!el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable); + // 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 — der Wert erscheint live im Cursor-HUD UND unten im Feld. if ( - el && - (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) + !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(); @@ -5024,6 +5056,7 @@ export default function App() { activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} + hudFields={hudFields} grips={grips} edgeGrips={edgeGrips} selectedDrawingId={selectedDrawingId} @@ -5093,6 +5126,7 @@ export default function App() { gripEdit ? setGripEdit(null) : activeTransform ? cancelTransform() : engine.cancel() } suggest={(prefix) => engine.suggest(prefix)} + onTextChange={setCmdTyped} /> )} @@ -6158,6 +6192,7 @@ function Content({ activeTool, toolInputActive, toolHandlers, + hudFields, grips, edgeGrips, selectedDrawingId, @@ -6246,6 +6281,7 @@ function Content({ activeTool: ToolId; toolInputActive: boolean; toolHandlers: ToolHandlers; + hudFields: HudFieldsState | null; grips: Vec2[]; edgeGrips: EdgeGrip[]; selectedDrawingId: string | null; @@ -6345,6 +6381,7 @@ function Content({ activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} + hudFields={hudFields} grips={grips} edgeGrips={edgeGrips} selectedDrawingId={selectedDrawingId} @@ -6401,6 +6438,7 @@ function Content({ activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} + hudFields={hudFields} grips={grips} edgeGrips={edgeGrips} selectedDrawingId={selectedDrawingId} @@ -6497,6 +6535,7 @@ function Content({ activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} + hudFields={hudFields} grips={grips} edgeGrips={edgeGrips} selectedDrawingId={selectedDrawingId} @@ -6543,6 +6582,7 @@ function LevelPlanView({ activeTool, toolInputActive, toolHandlers, + hudFields, grips, edgeGrips, selectedDrawingId, @@ -6586,6 +6626,7 @@ function LevelPlanView({ activeTool: ToolId; toolInputActive: boolean; toolHandlers: ToolHandlers; + hudFields: HudFieldsState | null; grips: Vec2[]; edgeGrips: EdgeGrip[]; selectedDrawingId: string | null; @@ -6660,6 +6701,7 @@ function LevelPlanView({ activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} + hudFields={hudFields} grips={grips} edgeGrips={edgeGrips} hairline={hairline} @@ -6714,6 +6756,7 @@ function SectionPlanView({ activeTool, toolInputActive, toolHandlers, + hudFields, grips, edgeGrips, selectedDrawingId, @@ -6749,6 +6792,7 @@ function SectionPlanView({ activeTool: ToolId; toolInputActive: boolean; toolHandlers: ToolHandlers; + hudFields: HudFieldsState | null; grips: Vec2[]; edgeGrips: EdgeGrip[]; selectedDrawingId: string | null; @@ -6851,6 +6895,7 @@ function SectionPlanView({ activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} + hudFields={hudFields} grips={grips} edgeGrips={edgeGrips} hairline={hairline} diff --git a/src/plan/PlanView.tsx b/src/plan/PlanView.tsx index 6e7c621..c2bcde5 100644 --- a/src/plan/PlanView.tsx +++ b/src/plan/PlanView.tsx @@ -24,6 +24,16 @@ import { buildRandomHatchRuns, zigzagPoints, motifPoints, DASH_MM_TO_M } from ". import { dashHasDot } from "../ui/lineSegments"; import { DEFAULT_MARQUEE_COLOR, DEFAULT_SNAP_COLOR } from "../theme/accents"; +/** + * Feld-Status der Befehlszeile fürs Cursor-HUD (Tab-Feld-Zyklus §2.7): Werte je + * Feld (gelockt oder live), aktives Tab-Ziel und der gerade getippte Text — + * das HUD spiegelt die Eingabe LIVE am Cursor (VW-Verhalten). + */ +export interface HudFieldsState { + fields: { id: string; value: number | null; locked: boolean; active: boolean }[]; + typed: string; +} + const PX_PER_M = 90; // viewBox-Einheiten je Meter (Modell → SVG-Benutzerraum) const PAD = 60; // Rand in viewBox-Einheiten @@ -349,6 +359,14 @@ export interface PlanViewProps { * und zeichnet den zurückgegebenen `draft` als Overlay (Vorschau + Snap-Marker). */ toolHandlers?: ToolHandlers; + /** + * Feld-Status der Befehlszeile fürs Cursor-HUD (Tab-Feld-Zyklus §2.7): + * gelockte/live Werte je Feld, aktives Tab-Ziel und der gerade getippte + * Text. Das HUD spiegelt damit die Eingabe LIVE am Cursor (VW-Verhalten: + * tippen → Wert erscheint oben im Kästchen UND unten im Befehl); ohne + * Felder fällt es auf die draft.hud-Livewerte zurück. + */ + hudFields?: HudFieldsState | null; /** * Editier-Griffe (Grips) des aktuell EINZELN selektierten Elements, in Modell- * Metern (Wand-Enden, 2D-Vertices/Ecken). Werden als ziehbare Quadrate @@ -437,6 +455,7 @@ export const PlanView = forwardRef( activeTool, toolInputActive, toolHandlers, + hudFields, grips, edgeGrips, hairline, @@ -2302,6 +2321,7 @@ export const PlanView = forwardRef( toScreen={toScreen} vbPerPx={1 / (meetScale(view, svgRef.current) ?? 1)} snapColor={snapColor} + hudFields={hudFields ?? null} /> )} @@ -2324,11 +2344,16 @@ function DraftOverlay({ toScreen, vbPerPx, snapColor, + hudFields = null, }: { draft: ToolDraft; toScreen: (v: Vec2) => Vec2; vbPerPx: number; snapColor: string; + hudFields?: { + fields: { id: string; value: number | null; locked: boolean; active: boolean }[]; + typed: string; + } | null; }) { const vertSize = 4.5 * vbPerPx; // halbe Kantenlänge eines Stützpunkt-Quadrats (klein + einheitlich) const markSize = 6.5 * vbPerPx; // halbe Kantenlänge des Snap-Markers (grösser = besser sichtbar) @@ -2386,7 +2411,7 @@ function DraftOverlay({ )} {/* Maß-/Winkel-HUD am Cursor (VW-Stil `L: … W: …`, oder einfacher Text). */} - {draft.hud && } + {draft.hud && } ); } @@ -2402,37 +2427,92 @@ function DraftHud({ hud, toScreen, vbPerPx, + hudFields = null, }: { hud: NonNullable; toScreen: (v: Vec2) => Vec2; vbPerPx: number; + hudFields?: { + fields: { id: string; value: number | null; locked: boolean; active: boolean }[]; + typed: string; + } | null; }) { - const text = - hud.length != null && hud.angleDeg != null - ? `L: ${hud.length.toFixed(3)}m W: ${hud.angleDeg.toFixed(3)}°` - : hud.text; - if (!text) return null; + // Segmente: mit Befehlszeilen-Feldern (Tab-Zyklus) spiegelt das HUD deren + // Zustand — getippter Text erscheint LIVE im aktiven Feld (VW-Verhalten), + // gelockte Werte fest, ungelockte folgen der Maus (draft.hud-Livewerte als + // Fallback je Feld-Id). Ohne Felder: bisherige L/W- bzw. Text-Anzeige. + interface Seg { + text: string; + active: boolean; + locked: boolean; + } + const liveFor = (id: string): number | null => + id === "length" ? (hud.length ?? null) : id === "angle" ? (hud.angleDeg ?? null) : null; + const segs: Seg[] = []; + if (hudFields && hudFields.fields.length > 0) { + for (const f of hudFields.fields) { + const label = f.id === "length" ? "L" : f.id === "angle" ? "W" : f.id; + const unit = f.id === "angle" ? "°" : "m"; + const typedHere = f.active && hudFields.typed.trim() !== ""; + const value = typedHere + ? hudFields.typed + : f.value != null + ? f.value.toFixed(3) + : (liveFor(f.id)?.toFixed(3) ?? "—"); + segs.push({ text: `${label}: ${value}${unit}`, active: f.active, locked: f.locked }); + } + } else if (hud.length != null && hud.angleDeg != null) { + segs.push({ text: `L: ${hud.length.toFixed(3)}m`, active: false, locked: false }); + segs.push({ text: `W: ${hud.angleDeg.toFixed(3)}°`, active: false, locked: false }); + } else if (hud.text) { + segs.push({ text: hud.text, active: false, locked: false }); + } + if (segs.length === 0) return null; + const p = toScreen(hud.at); const fontSize = 12 * vbPerPx; const padX = 6 * vbPerPx; const padY = 4 * vbPerPx; + const gap = 10 * vbPerPx; const charW = fontSize * 0.62; // Monospace-Schätzung für die Kastenbreite - const w = text.length * charW + padX * 2; + const widths = segs.map((s) => s.text.length * charW); + const w = widths.reduce((a, b) => a + b, 0) + gap * (segs.length - 1) + padX * 2; const h = fontSize + padY * 2; const x = p.x + 14 * vbPerPx; const y = p.y + 14 * vbPerPx; + let cx = x + padX; return ( - - {text} - + {segs.map((s, i) => { + const sx = cx; + cx += widths[i] + gap; + return ( + + {s.active && ( + + )} + + {s.text} + + + ); + })} ); } diff --git a/src/styles.css b/src/styles.css index ae03c85..e3dbf12 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2041,6 +2041,18 @@ body { font-family: var(--mono, ui-monospace, monospace); font-weight: 600; } +/* Aktives Tab-Feld im HUD: blaue Pille, weisser Text (VW: Eingabefeld-Optik). */ +.plan-svg .plan-hud-active { + fill: #2864ff; +} +.plan-svg .plan-hud-text.active { + fill: #ffffff; +} +/* Gelockter (getippter) Wert: dunkler + fett, unterscheidet sich vom Live-Wert. */ +.plan-svg .plan-hud-text.locked { + fill: #143a99; + font-weight: 700; +} /* Weicher Winkel-Snap: gestrichelte Führungslinie (dezent) + gelbliches Winkel-Badge nahe der Liniemitte. */ diff --git a/src/ui/CommandLine.tsx b/src/ui/CommandLine.tsx index f9c1e48..8ae19c8 100644 --- a/src/ui/CommandLine.tsx +++ b/src/ui/CommandLine.tsx @@ -48,6 +48,12 @@ export interface CommandLineProps { onCycleField: () => void; /** Esc: laufenden Befehl abbrechen. */ onCancel: () => void; + /** + * Live-Echo des Eingabetexts (bei jeder Änderung, inkl. Leeren nach Submit/ + * Esc) — speist das Cursor-HUD, das den getippten Wert im aktiven Feld + * spiegelt (VW-Verhalten: tippen → Wert erscheint oben UND unten). + */ + onTextChange?: (text: string) => void; /** Autocomplete-Kandidaten für ein Präfix (nur im Ruhezustand sinnvoll). */ suggest: (prefix: string) => string[]; } @@ -55,18 +61,32 @@ export interface CommandLineProps { export interface CommandLineHandle { /** Fokussiert (und öffnet) das Eingabefeld — vom globalen Tab-Handler genutzt. */ focus: () => void; + /** + * Startet die Zahleneingabe mit einem ersten Zeichen (globales Tippen beim + * Zeichnen, ohne dass das Feld fokussiert war): fokussiert + setzt den Text. + */ + beginTyping: (seed: string) => void; } export const CommandLine = forwardRef( function CommandLine( - { active, promptKey, options, fields, onSubmit, onOption, onCycleField, onCancel, suggest }, + { active, promptKey, options, fields, onSubmit, onOption, onCycleField, onCancel, suggest, onTextChange }, ref, ) { - const [text, setText] = useState(""); + const [text, setTextRaw] = useState(""); const inputRef = useRef(null); + // Jede Textänderung (auch programmatisch/Leeren) ans Live-Echo melden. + const setText = (v: string) => { + setTextRaw(v); + onTextChange?.(v); + }; useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), + beginTyping: (seed: string) => { + setText(seed); + inputRef.current?.focus(); + }, })); // Autocomplete nur im Ruhezustand (Befehlsname tippen), nicht in einem Schritt.