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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user