2D: Text/Textspalte auf Inline-Editing direkt auf der Zeichenfläche umgestellt

Nutzer-Report: Textfelder waren nicht mehrzeilig. Ursache: die Text-/
Textspalte-Werkzeuge holten ihre Eingabe über die Befehlszeile
(CommandLine.tsx) — ein einzeiliges <input>, Enter committet sofort, kein
Zeilenumbruch möglich. Das betraf auch "Textspalte", obwohl die explizit für
mehrzeiligen Wortumbruch gedacht ist.

Lösung (Nutzer-Vorgabe: kein Dialog, direktes Editieren auf der Fläche wie
im Layout-System): text.ts/textbox.ts committen nach dem Platzieren (Anker
bzw. Anker+Breite) sofort ein LEERES Text-Element und melden
`focusDrawingId` — dafür CommandResult/EngineHost um einen Passthrough
erweitert (engine.ts applyResult ruft host.focusDrawing(id)). App.tsx setzt
darauf Auswahl + editTextId (dieselbe State-Variable wie beim bisherigen
Doppelklick-Editieren). PlanView rendert bei gesetztem editTextId jetzt
einen RichTextEditor-Overlay direkt über dem Element (position:fixed via
vbToClient/toScreen, eigene Mini-Toolbar erscheint darüber) statt des
bisherigen TextEditorDialog-Modals — Prinzip 1:1 vom Layout-Blatt-Inline-
Editor (LayoutSheet.tsx) übernommen. Die neuen Props (editTextId/
onCommitTextEdit/onCloseTextEdit) laufen durch Content/LevelPlanView/
SectionPlanView (viewportContent.tsx) bis zu PlanView durch.

Verhalten der beiden Werkzeuge (Nutzer-Vorgabe): "text" bleibt ohne
Wortumbruch (kein `width`) — Enter erzeugt weitere Zeilen, aber keine
automatische Breite. "textbox" setzt `width` und verhält sich damit wie ein
InDesign-Textrahmen (echter Wortumbruch live beim Tippen, kein reines
Render-Detail mehr).

commitTextEdit (useMouseSelectionHandlers.ts) schreibt jetzt bei JEDEM
Tastendruck (kein Guard mehr gegen leeren Text — nötig, damit Löschen bis
auf null Zeichen sich sofort spiegelt); neues closeTextEdit räumt ein beim
Schliessen noch leeres Element auf (verhindert Leichen bei abgebrochener
Neuerstellung).

4 neue Tests (text/textbox: sofortiger Commit + focusDrawingId). tsc -b /
vitest run (918/918) / npm run build grün. Rendering/Positionierung des
Overlays selbst nicht automatisiert testbar — Nutzer prüft visuell in der
Tauri-App.
This commit is contained in:
2026-08-20 21:34:40 +02:00
parent 4f48b73267
commit f2d169cc67
11 changed files with 399 additions and 100 deletions
+73
View File
@@ -16,6 +16,9 @@ import {
import type { HatchRender, Plan, Primitive } from "./generatePlan";
import type { EdgeGrip, Vec2 } from "../model/types";
import type { ToolHandlers, ToolId, ToolMods } from "../tools/types";
import type { RichTextDoc } from "../text/richText";
import { docFromText } from "../text/richText";
import { RichTextEditor } from "../text/RichTextEditor";
import { useGlPlanRenderer } from "./useGlPlanRenderer";
import { useWasmPlanRenderer } from "./useWasmPlanRenderer";
import { DEFAULT_MARQUEE_COLOR, DEFAULT_SNAP_COLOR } from "../theme/accents";
@@ -301,6 +304,19 @@ export interface PlanViewProps {
onStampEdit?: (roomId: string) => void;
/** Doppelklick auf ein Text-2D-Element: Inhalt bearbeiten (Editor öffnen). */
onEditDrawingText?: (drawingId: string) => void;
/**
* Drawing2D-ID, deren Inhalt gerade inline auf der Zeichenfläche editiert
* wird (frisch über `text`/`textbox` erzeugt ODER per Doppelklick auf ein
* bestehendes Element geöffnet) — `null`/undefined = kein Inline-Editor
* offen. PlanView rendert dafür einen `RichTextEditor`-Overlay direkt über
* dem Element (kein Dialog), positioniert/skaliert über den aktuellen
* Ausschnitt.
*/
editTextId?: string | null;
/** Jede Änderung im Inline-Text-Editor (live, pro Tastendruck). */
onCommitTextEdit?: (doc: RichTextDoc) => void;
/** Inline-Text-Editor schliessen (Backdrop-Klick/Escape). */
onCloseTextEdit?: () => void;
/** Doppelklick auf eine Schnittlinie: in deren 2D-Schnittansicht springen
* (Zeichnungsebene aktivieren). */
onOpenSectionLevel?: (levelId: string) => void;
@@ -428,6 +444,9 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
onStampMove,
onStampEdit,
onEditDrawingText,
editTextId,
onCommitTextEdit,
onCloseTextEdit,
onOpenSectionLevel,
activeTool,
toolInputActive,
@@ -2450,6 +2469,60 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
/>
)}
</svg>
{/* Inline-Text-Editor DIREKT auf der Zeichenfläche (kein Dialog, Nutzer-
Feedback) — für ein frisch über `text`/`textbox` erzeugtes Element
(sofort nach dem Platzieren, s. App.tsx host.focusDrawing) ODER ein
per Doppelklick geöffnetes bestehendes. `RichTextEditor` bringt seine
eigene Mini-Toolbar mit (erscheint darüber, wie im Layout-Editor).
`at` ist die Baseline-links der ersten Zeile (s. primitives.tsx
"drawingText") — der Editor-Rahmen sitzt daher ~eine Zeilenhöhe
darüber. OHNE `wrapWidth` (Text-Werkzeug) bleibt die Breite frei
(kein Wortumbruch, nur Enter erzeugt neue Zeilen); MIT `wrapWidth`
(Textspalte) bricht der Editor live um wie ein InDesign-Textrahmen —
`position:"fixed"` mit echten Client-Pixeln (vbToClient), damit die
Positionierung unabhängig vom umgebenden Layout stimmt. */}
{editTextId &&
(() => {
const prim = plan.primitives.find(
(p): p is Extract<Primitive, { kind: "drawingText" }> =>
p.kind === "drawingText" && p.drawingId === editTextId,
);
if (!prim) return null;
const doc = prim.doc ?? docFromText(prim.text, prim.marks ?? {});
const pxPerM = currentPxPerMeter();
const topAnchor = toScreen({ x: prim.at.x, y: prim.at.y + prim.heightM });
const screen = vbToClient(topAnchor.x, topAnchor.y);
const FONT_PX_AT_12PT = 16; // 12pt @ 96dpi (RichTextEditor-Default, s. LayoutSheet.tsx).
const scale = (prim.heightM * pxPerM) / FONT_PX_AT_12PT;
const widthPx = prim.wrapWidth ? (prim.wrapWidth * pxPerM) / scale : undefined;
return (
<>
<div
className="planview-inline-backdrop"
onPointerDown={() => onCloseTextEdit?.()}
/>
<div
className="planview-inline-text"
style={{
left: screen.x,
top: screen.y,
width: widthPx,
transform: `scale(${scale})`,
transformOrigin: "0 0",
}}
onPointerDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.stopPropagation();
onCloseTextEdit?.();
}
}}
>
<RichTextEditor value={doc} onChange={(d) => onCommitTextEdit?.(d)} autoFocus />
</div>
</>
);
})()}
{/* Lineale (oben + links, VW-Stil): Modell-Meter mit Ticks, Beschriftung
und Cursor-Marker — screen-fix über den sichtbaren Ausschnitt gelegt.
BEWUSST ein eigenes Sibling-SVG (nicht mehr Kind von `svgRef`): beim