2D: Inline-Text-Editor-Nachbesserung (unsichtbarer Anker) + Textspalte als echtes Rechteck
Nutzer-Report nach der Umstellung auf Inline-Editing: beides funktionierte nicht — "Text" liess sich kein Ankerpunkt setzen, "Textspalte" sollte ein Rechteck statt nur eine Breite sein. Root Cause für "Text": `.planview-inline-text` bekam ohne Spaltenbreite `width: undefined` — bei position:fixed ohne rechten Rand greift shrink-to- fit-Sizing, das für ein LEERES contentEditable auf ~0px kollabiert. Der Klick erzeugte tatsächlich ein Element + öffnete den Editor, beides war nur unsichtbar (eine 0px-Box lässt sich nicht anklicken/fokussieren). Fix: neue Klasse .planview-inline-text-free (kein wrapWidth) erzwingt `width: max-content` + `white-space: pre` auf Editor/Surface (Box wächst mit dem Inhalt statt umzubrechen — "Text" bleibt einzeilig bis Enter, wie gewünscht) plus eine CSS-Mindestbreite (160px) für den leeren Startzustand. "Textspalte" (textbox.ts) zeichnet jetzt ein RECHTECK (zwei diagonale Ecken, beliebige Zugrichtung) statt nur eine horizontale Breitenlinie — die aufgezogene Höhe wird nicht im Modell gespeichert (das Textformat kennt nur `width` für den Wortumbruch, keine feste Rahmenhöhe), sondern als Editor-MINDESThöhe durchgereicht: CommandResult/EngineHost.focusDrawing um `focusMinHeightM` erweitert (Passthrough wie focusDrawingId), App.tsx hält sie in einem neuen editTextMinHeightM-State (useContextMenuState.ts), PlanView nutzt sie als CSS-min-height des Editors — der Rahmen wächst bei mehr Text darüber hinaus, schrumpft aber nicht darunter (InDesign-Verhalten). Beim Doppelklick-Editieren bestehender Elemente wird die Mindesthöhe zurückgesetzt (kein Nachwirken einer vorigen Textspalten-Erstellung). textbox.test.ts an die neue Rechteck-Geste angepasst (+ Test für beliebige Zugrichtung). tsc -b / vitest run (919/919) / npm run build grün. Die eigentliche Sichtbarkeits-Vermutung (CSS-Kollaps) konnte ich nicht interaktiv im Browser verifizieren (kein Browser-Tool verfügbar) — Nutzer prüft erneut in der laufenden App.
This commit is contained in:
+6
-2
@@ -419,6 +419,8 @@ export default function App() {
|
|||||||
setStampEditorRoomId,
|
setStampEditorRoomId,
|
||||||
editTextId,
|
editTextId,
|
||||||
setEditTextId,
|
setEditTextId,
|
||||||
|
editTextMinHeightM,
|
||||||
|
setEditTextMinHeightM,
|
||||||
} = useContextMenuState();
|
} = useContextMenuState();
|
||||||
|
|
||||||
// Gewählte Annotation auf einem offenen Layout-Blatt (Linie/Rechteck/Text/
|
// Gewählte Annotation auf einem offenen Layout-Blatt (Linie/Rechteck/Text/
|
||||||
@@ -801,7 +803,7 @@ export default function App() {
|
|||||||
// applyResult-Reihenfolge commit→focusDrawing). Auswahl-Löschen wie
|
// applyResult-Reihenfolge commit→focusDrawing). Auswahl-Löschen wie
|
||||||
// onEditDrawingText (useMouseSelectionHandlers.ts), damit kein anderes
|
// onEditDrawingText (useMouseSelectionHandlers.ts), damit kein anderes
|
||||||
// Element gleichzeitig markiert bleibt.
|
// Element gleichzeitig markiert bleibt.
|
||||||
focusDrawing: (id) => {
|
focusDrawing: (id, minHeightM) => {
|
||||||
setSelectedWallIds([]);
|
setSelectedWallIds([]);
|
||||||
setSelectedCeilingIds([]);
|
setSelectedCeilingIds([]);
|
||||||
setSelectedOpeningIds([]);
|
setSelectedOpeningIds([]);
|
||||||
@@ -813,6 +815,7 @@ export default function App() {
|
|||||||
setSelectedContextObjectIds([]);
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
setSelectedDrawingId(id);
|
setSelectedDrawingId(id);
|
||||||
|
setEditTextMinHeightM(minHeightM ?? null);
|
||||||
setEditTextId(id);
|
setEditTextId(id);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -2286,7 +2289,7 @@ export default function App() {
|
|||||||
selectedSectionLineId,
|
selectedSectionLineId,
|
||||||
setSelectedSectionLineId,
|
setSelectedSectionLineId,
|
||||||
},
|
},
|
||||||
contextMenuState: { setMenu, setStampEditorRoomId, editTextId, setEditTextId },
|
contextMenuState: { setMenu, setStampEditorRoomId, editTextId, setEditTextId, setEditTextMinHeightM },
|
||||||
});
|
});
|
||||||
|
|
||||||
// „Bild einfügen" (Werkzeug „image") — siehe state/useImportHandling.ts.
|
// „Bild einfügen" (Werkzeug „image") — siehe state/useImportHandling.ts.
|
||||||
@@ -2736,6 +2739,7 @@ export default function App() {
|
|||||||
onStampEdit={onOpenStampEditor}
|
onStampEdit={onOpenStampEditor}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
editTextId={editTextId}
|
editTextId={editTextId}
|
||||||
|
editTextMinHeightM={editTextMinHeightM}
|
||||||
onCommitTextEdit={commitTextEdit}
|
onCommitTextEdit={commitTextEdit}
|
||||||
onCloseTextEdit={closeTextEdit}
|
onCloseTextEdit={closeTextEdit}
|
||||||
onPlanSelect={onPlanSelect}
|
onPlanSelect={onPlanSelect}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* `textboxCommand` — committet seit der Umstellung auf Inline-Editing (kein
|
* `textboxCommand` — committet seit der Umstellung auf Inline-Editing (kein
|
||||||
* Dialog/keine Befehlszeilen-Eingabe mehr, InDesign-artiges Textrahmen-
|
* Dialog/keine Befehlszeilen-Eingabe mehr, InDesign-artiges Textrahmen-
|
||||||
* Verhalten, Nutzer-Wunsch) SOFORT ein leeres Text-Element MIT Spaltenbreite
|
* Verhalten, Nutzer-Wunsch) SOFORT ein leeres Text-Element MIT Spaltenbreite
|
||||||
* nach Anker+Breite und meldet `focusDrawingId`.
|
* nach einem aufgezogenen RECHTECK (zwei diagonale Ecken, nicht nur eine
|
||||||
|
* Breitenlinie) und meldet `focusDrawingId` + `focusMinHeightM` (die
|
||||||
|
* aufgezogene Rahmenhöhe als Mindesthöhe des Inline-Editors).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
@@ -45,23 +47,25 @@ function makeCtx(p: Project): CommandContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("textboxCommand", () => {
|
describe("textboxCommand", () => {
|
||||||
it("committet nach Anker+Breite sofort ein leeres Text-Element mit width und meldet focusDrawingId", () => {
|
it("committet nach einem Rechteck (2 Ecken) ein leeres Text-Element mit width + meldet focusDrawingId/focusMinHeightM", () => {
|
||||||
const p = project();
|
const p = project();
|
||||||
const ctx = makeCtx(p);
|
const ctx = makeCtx(p);
|
||||||
|
|
||||||
const [afterPoint] = textboxCommand.onInput(
|
const [afterFirst] = textboxCommand.onInput(
|
||||||
textboxCommand.init(),
|
textboxCommand.init(),
|
||||||
{ kind: "point", point: { x: 0, y: 0 } },
|
{ kind: "point", point: { x: 0, y: 5 } },
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
|
// Gegenecke: 3m breit, 1.5m hoch (Rahmen, keine reine Breitenlinie mehr).
|
||||||
const [, result] = textboxCommand.onInput(
|
const [, result] = textboxCommand.onInput(
|
||||||
afterPoint,
|
afterFirst,
|
||||||
{ kind: "point", point: { x: 2, y: 0 } }, // Spaltenbreite = 2m
|
{ kind: "point", point: { x: 3, y: 3.5 } },
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
expect(result.done).toBe(true);
|
expect(result.done).toBe(true);
|
||||||
expect(result.commit).toBeDefined();
|
expect(result.commit).toBeDefined();
|
||||||
expect(result.focusDrawingId).toBeDefined();
|
expect(result.focusDrawingId).toBeDefined();
|
||||||
|
expect(result.focusMinHeightM).toBeCloseTo(1.5, 9);
|
||||||
|
|
||||||
const next = result.commit!(p);
|
const next = result.commit!(p);
|
||||||
expect(next.drawings2d.length).toBe(1);
|
expect(next.drawings2d.length).toBe(1);
|
||||||
@@ -70,7 +74,30 @@ describe("textboxCommand", () => {
|
|||||||
expect(d.geom.shape).toBe("text");
|
expect(d.geom.shape).toBe("text");
|
||||||
if (d.geom.shape === "text") {
|
if (d.geom.shape === "text") {
|
||||||
expect(d.geom.text).toBe("");
|
expect(d.geom.text).toBe("");
|
||||||
expect(d.geom.width).toBeCloseTo(2, 9); // InDesign-artiger Textrahmen: Breite gesetzt
|
expect(d.geom.width).toBeCloseTo(3, 9); // InDesign-artiger Textrahmen: Breite gesetzt
|
||||||
|
expect(d.geom.at.x).toBeCloseTo(0, 9); // oben-links des Rahmens (unabhängig von der Zugrichtung)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("beliebige Zugrichtung (Gegenecke oben-links vom Start): Rahmen wird trotzdem korrekt normalisiert", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
const [afterFirst] = textboxCommand.onInput(
|
||||||
|
textboxCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 5, y: 2 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
const [, result] = textboxCommand.onInput(
|
||||||
|
afterFirst,
|
||||||
|
{ kind: "point", point: { x: 2, y: 4 } }, // Gegenecke LINKS und OBEN vom Start
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
const next = result.commit!(p);
|
||||||
|
const d = next.drawings2d[0];
|
||||||
|
if (d.geom.shape === "text") {
|
||||||
|
expect(d.geom.width).toBeCloseTo(3, 9); // |5-2|
|
||||||
|
expect(d.geom.at.x).toBeCloseTo(2, 9); // min(x)
|
||||||
|
}
|
||||||
|
expect(result.focusMinHeightM).toBeCloseTo(2, 9); // |4-2|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
// Textspalte (Absatztext) — wie das Text-Werkzeug, aber mit einer Spaltenbreite:
|
// Textspalte (Absatztext) — wie das Text-Werkzeug, aber als aufgezogener
|
||||||
// der Text bricht beim Rendern UND beim Editieren wortweise auf diese Breite
|
// RAHMEN: Breite UND Höhe kommen aus einem Rechteck (zwei diagonale Punkte),
|
||||||
// um (InDesign-artiges Textrahmen-Verhalten, Nutzer-Wunsch). Schritte:
|
// nicht nur einer Breitenlinie (InDesign-artiges Textrahmen-Verhalten,
|
||||||
// 1) „Ankerpunkt:" → Punkt (oben-links der Spalte)
|
// Nutzer-Wunsch). Schritte:
|
||||||
// 2) „Spaltenbreite:" → Punkt (die horizontale Distanz zum Anker = Breite;
|
// 1) „Erste Ecke:" → Punkt
|
||||||
// Live-Vorschau der Breitenlinie)
|
// 2) „Gegenecke:" → Punkt (Live-Vorschau des Rahmens, beliebige Richtung)
|
||||||
// Committet danach SOFORT ein leeres Text-Element (mit `width`) und meldet
|
// Committet danach SOFORT ein leeres Text-Element (mit `width`) und meldet
|
||||||
// `focusDrawingId` — die UI (PlanView) öffnet den Inline-Rich-Text-Editor
|
// `focusDrawingId` + `focusMinHeightM` (die aufgezogene Rahmenhöhe als
|
||||||
// direkt auf der Zeichenfläche, mit der Spaltenbreite als CSS-Breite (echter
|
// MINDESThöhe, s. types.ts) — die UI (PlanView) öffnet den Inline-Rich-
|
||||||
// Wortumbruch beim Tippen, kein Dialog). Bleibt der Editor leer, verwirft
|
// Text-Editor direkt auf der Zeichenfläche, mit der Spaltenbreite als CSS-
|
||||||
// `commitTextEdit` (App.tsx) das Element wieder.
|
// Breite (echter Wortumbruch beim Tippen, kein Dialog) und dem Rahmen als
|
||||||
|
// Mindesthöhe (wächst bei mehr Text darüber hinaus). Bleibt der Editor
|
||||||
|
// leer, verwirft `commitTextEdit`/`closeTextEdit` (App.tsx) das Element
|
||||||
|
// wieder.
|
||||||
//
|
//
|
||||||
// Reuse des bestehenden {shape:"text"}-Elements (mit `width`) — so erben Selektion,
|
// Reuse des bestehenden {shape:"text"}-Elements (mit `width`) — so erben
|
||||||
// Verschieben und Griff automatisch vom Einzeltext; nur die Breite kommt hinzu.
|
// Selektion, Verschieben und Griff automatisch vom Einzeltext; die Modell-
|
||||||
|
// Geometrie selbst kennt aber keine feste Rahmenhöhe (nur `width` für den
|
||||||
|
// Wortumbruch) — die Höhe bleibt reine Editor-Startgrösse (PlanView-lokal,
|
||||||
|
// s. focusMinHeightM), die Textmenge bestimmt die tatsächliche Höhe.
|
||||||
|
|
||||||
import type { Drawing2D } from "../../model/types";
|
import type { Drawing2D } from "../../model/types";
|
||||||
import { uniqueId } from "../../tools/types";
|
import { uniqueId } from "../../tools/types";
|
||||||
@@ -20,39 +26,61 @@ import type {
|
|||||||
CommandContext,
|
CommandContext,
|
||||||
CommandResult,
|
CommandResult,
|
||||||
CommandState,
|
CommandState,
|
||||||
|
DraftShape,
|
||||||
Project,
|
Project,
|
||||||
ToolDraft,
|
|
||||||
Vec2,
|
Vec2,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
/** Default-Schrifthöhe einer Textspalte in Modell-Metern. */
|
/** Default-Schrifthöhe einer Textspalte in Modell-Metern (= Baseline-Versatz vom Rahmen-Top). */
|
||||||
const DEFAULT_TEXT_HEIGHT_M = 0.25;
|
const DEFAULT_TEXT_HEIGHT_M = 0.25;
|
||||||
/** Kleinste sinnvolle Spaltenbreite (Meter). */
|
/** Kleinste sinnvolle Rahmen-Breite/-Höhe (Meter). */
|
||||||
const MIN_WIDTH_M = 0.2;
|
const MIN_SIZE_M = 0.2;
|
||||||
|
|
||||||
interface TbPoint extends CommandState {
|
interface TbFirst extends CommandState {
|
||||||
phase: "point";
|
phase: "first";
|
||||||
}
|
}
|
||||||
interface TbWidth extends CommandState {
|
interface TbSecond extends CommandState {
|
||||||
phase: "width";
|
phase: "second";
|
||||||
at: Vec2;
|
a: Vec2;
|
||||||
cursor: Vec2 | null;
|
cursor: Vec2 | null;
|
||||||
}
|
}
|
||||||
type TbState = TbPoint | TbWidth;
|
type TbState = TbFirst | TbSecond;
|
||||||
|
|
||||||
/** Horizontale Spaltenbreite aus Anker + Cursor (Betrag der x-Differenz, geklemmt). */
|
/** Normalisiertes Rechteck aus zwei diagonalen Punkten (beliebige Zugrichtung). */
|
||||||
function widthOf(at: Vec2, cursor: Vec2): number {
|
function rectOf(a: Vec2, b: Vec2): { minX: number; maxX: number; minY: number; maxY: number } {
|
||||||
return Math.max(MIN_WIDTH_M, Math.abs(cursor.x - at.x));
|
return {
|
||||||
|
minX: Math.min(a.x, b.x),
|
||||||
|
maxX: Math.max(a.x, b.x),
|
||||||
|
minY: Math.min(a.y, b.y),
|
||||||
|
maxY: Math.max(a.y, b.y),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vorschau: waagrechte Breitenlinie am Anker (zeigt die Spaltenbreite). */
|
/** Breite/Höhe (geklemmt auf MIN_SIZE_M) + die Textanker-Position (oben-links, als Baseline). */
|
||||||
function widthDraft(at: Vec2, cursor: Vec2): ToolDraft {
|
function frameOf(a: Vec2, b: Vec2): { at: Vec2; width: number; heightM: number } {
|
||||||
const w = widthOf(at, cursor);
|
const r = rectOf(a, b);
|
||||||
const b: Vec2 = { x: at.x + w, y: at.y };
|
const width = Math.max(MIN_SIZE_M, r.maxX - r.minX);
|
||||||
|
const heightM = Math.max(MIN_SIZE_M, r.maxY - r.minY);
|
||||||
|
// `at` ist die Baseline-links der ERSTEN Zeile (s. drawing2d.ts/primitives.tsx);
|
||||||
|
// die Rahmen-Oberkante liegt eine Zeilenhöhe darüber → at.y = Rahmen-Top − Zeilenhöhe.
|
||||||
|
return { at: { x: r.minX, y: r.maxY - DEFAULT_TEXT_HEIGHT_M }, width, heightM };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vorschau: Rahmen-Rechteck zwischen den beiden Ecken. */
|
||||||
|
function frameDraft(a: Vec2, cursor: Vec2): { preview: DraftShape[]; vertices: Vec2[]; hud: { at: Vec2; text: string } } {
|
||||||
|
const r = rectOf(a, cursor);
|
||||||
|
const pts: Vec2[] = [
|
||||||
|
{ x: r.minX, y: r.minY },
|
||||||
|
{ x: r.maxX, y: r.minY },
|
||||||
|
{ x: r.maxX, y: r.maxY },
|
||||||
|
{ x: r.minX, y: r.maxY },
|
||||||
|
];
|
||||||
|
const w = Math.max(MIN_SIZE_M, r.maxX - r.minX);
|
||||||
|
const h = Math.max(MIN_SIZE_M, r.maxY - r.minY);
|
||||||
return {
|
return {
|
||||||
preview: [{ kind: "line", a: at, b }],
|
preview: [{ kind: "poly", pts, closed: true }],
|
||||||
vertices: [at, b],
|
vertices: [a, cursor],
|
||||||
hud: { at: b, text: `${w.toFixed(2)} m` },
|
hud: { at: cursor, text: `${w.toFixed(2)} × ${h.toFixed(2)} m` },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,45 +96,45 @@ function appendEmptyTextbox(p: Project, id: string, at: Vec2, width: number, ctx
|
|||||||
}
|
}
|
||||||
|
|
||||||
const idle = (): [CommandState, CommandResult] => [
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
{ phase: "point", lastPoint: null } as TbPoint,
|
{ phase: "first", lastPoint: null } as TbFirst,
|
||||||
{ draft: null, done: true },
|
{ draft: null, done: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const textboxCommand: Command = {
|
export const textboxCommand: Command = {
|
||||||
name: "textbox",
|
name: "textbox",
|
||||||
labelKey: "cmd.textbox.label",
|
labelKey: "cmd.textbox.label",
|
||||||
prompt: (s) => ((s as TbState).phase === "width" ? "cmd.textbox.width" : "cmd.textbox.point"),
|
prompt: (s) => ((s as TbState).phase === "second" ? "cmd.textbox.second" : "cmd.textbox.first"),
|
||||||
accepts: () => ["point"],
|
accepts: () => ["point"],
|
||||||
options: () => [],
|
options: () => [],
|
||||||
init: (): TbPoint => ({ phase: "point", lastPoint: null }),
|
init: (): TbFirst => ({ phase: "first", lastPoint: null }),
|
||||||
|
|
||||||
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||||
const s = state as TbState;
|
const s = state as TbState;
|
||||||
if (input.kind !== "point") return [s, { draft: null }];
|
if (input.kind !== "point") return [s, { draft: null }];
|
||||||
if (s.phase === "point") {
|
if (s.phase === "first") {
|
||||||
const ns: TbWidth = { phase: "width", at: input.point, cursor: input.point, lastPoint: input.point };
|
const ns: TbSecond = { phase: "second", a: input.point, cursor: input.point, lastPoint: input.point };
|
||||||
return [ns, { draft: widthDraft(input.point, input.point) }];
|
return [ns, { draft: { preview: [], vertices: [input.point] } }];
|
||||||
}
|
}
|
||||||
// phase === "width"
|
// phase === "second"
|
||||||
const width = widthOf(s.at, input.point);
|
const { at, width, heightM } = frameOf(s.a, input.point);
|
||||||
const id = uniqueId("dr2d");
|
const id = uniqueId("dr2d");
|
||||||
const at = s.at;
|
|
||||||
return [
|
return [
|
||||||
{ phase: "point", lastPoint: null } as TbPoint,
|
{ phase: "first", lastPoint: null } as TbFirst,
|
||||||
{
|
{
|
||||||
draft: null,
|
draft: null,
|
||||||
done: true,
|
done: true,
|
||||||
commit: (p) => appendEmptyTextbox(p, id, at, width, ctx),
|
commit: (p) => appendEmptyTextbox(p, id, at, width, ctx),
|
||||||
focusDrawingId: id,
|
focusDrawingId: id,
|
||||||
|
focusMinHeightM: heightM,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
onMove: (state, point): [CommandState, CommandResult] => {
|
onMove: (state, point): [CommandState, CommandResult] => {
|
||||||
const s = state as TbState;
|
const s = state as TbState;
|
||||||
if (s.phase !== "width") return [s, { draft: null }];
|
if (s.phase !== "second") return [s, { draft: null }];
|
||||||
const ns: TbWidth = { ...s, cursor: point };
|
const ns: TbSecond = { ...s, cursor: point };
|
||||||
return [ns, { draft: widthDraft(s.at, point) }];
|
return [ns, { draft: frameDraft(s.a, point) }];
|
||||||
},
|
},
|
||||||
|
|
||||||
onConfirm: (): [CommandState, CommandResult] => idle(),
|
onConfirm: (): [CommandState, CommandResult] => idle(),
|
||||||
|
|||||||
@@ -54,10 +54,10 @@ export interface EngineHost {
|
|||||||
isFloor(): boolean;
|
isFloor(): boolean;
|
||||||
/**
|
/**
|
||||||
* Optional: ein frisch erzeugtes Drawing2D-Element fokussieren (Passthrough
|
* Optional: ein frisch erzeugtes Drawing2D-Element fokussieren (Passthrough
|
||||||
* von `CommandResult.focusDrawingId`, z. B. um sofort den Inline-Text-Editor
|
* von `CommandResult.focusDrawingId`/`focusMinHeightM`, z. B. um sofort den
|
||||||
* zu öffnen). Fehlt der Host-Handler, ist das ein No-op.
|
* Inline-Text-Editor zu öffnen). Fehlt der Host-Handler, ist das ein No-op.
|
||||||
*/
|
*/
|
||||||
focusDrawing?(drawingId: string): void;
|
focusDrawing?(drawingId: string, minHeightM?: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Öffentlicher Schnappschuss für die Command-Line-UI (rein lesend). */
|
/** Öffentlicher Schnappschuss für die Command-Line-UI (rein lesend). */
|
||||||
@@ -482,7 +482,7 @@ export class CommandEngine {
|
|||||||
|
|
||||||
private applyResult(res: CommandResult): void {
|
private applyResult(res: CommandResult): void {
|
||||||
if (res.commit) this.host.commit(res.commit);
|
if (res.commit) this.host.commit(res.commit);
|
||||||
if (res.focusDrawingId) this.host.focusDrawing?.(res.focusDrawingId);
|
if (res.focusDrawingId) this.host.focusDrawing?.(res.focusDrawingId, res.focusMinHeightM);
|
||||||
if (res.draft) this.host.setDraft(res.draft);
|
if (res.draft) this.host.setDraft(res.draft);
|
||||||
else this.host.setDraft(null);
|
else this.host.setDraft(null);
|
||||||
if (res.done) {
|
if (res.done) {
|
||||||
|
|||||||
@@ -116,6 +116,13 @@ export interface CommandResult {
|
|||||||
* die Engine selbst wertet die ID nicht aus.
|
* die Engine selbst wertet die ID nicht aus.
|
||||||
*/
|
*/
|
||||||
focusDrawingId?: string;
|
focusDrawingId?: string;
|
||||||
|
/**
|
||||||
|
* Nur zusammen mit `focusDrawingId`: die vom Nutzer aufgezogene Rahmenhöhe
|
||||||
|
* (Meter) als MINDESTHöhe des Inline-Editors (InDesign-Textrahmen — wächst
|
||||||
|
* bei Bedarf über diese Höhe hinaus, schrumpft aber nicht darunter). Fehlt
|
||||||
|
* sie, bestimmt allein der Inhalt die Höhe (Text-Werkzeug ohne Rahmen).
|
||||||
|
*/
|
||||||
|
focusMinHeightM?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Befehls-Zustand ──────────────────────────────────────────────────────────
|
// ── Befehls-Zustand ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
+2
-4
@@ -1141,11 +1141,9 @@ export const de = {
|
|||||||
"cmd.arc.end": "Endpunkt (Endwinkel):",
|
"cmd.arc.end": "Endpunkt (Endwinkel):",
|
||||||
"cmd.text.label": "Text",
|
"cmd.text.label": "Text",
|
||||||
"cmd.text.point": "Ankerpunkt:",
|
"cmd.text.point": "Ankerpunkt:",
|
||||||
"cmd.text.enter": "Text eingeben:",
|
|
||||||
"cmd.textbox.label": "Textspalte",
|
"cmd.textbox.label": "Textspalte",
|
||||||
"cmd.textbox.point": "Ankerpunkt:",
|
"cmd.textbox.first": "Erste Ecke:",
|
||||||
"cmd.textbox.width": "Spaltenbreite (zweiter Punkt):",
|
"cmd.textbox.second": "Gegenecke (Breite × Höhe):",
|
||||||
"cmd.textbox.enter": "Text eingeben:",
|
|
||||||
// Editierbefehle (Tier 1): Move/Copy/Offset operieren auf der Auswahl.
|
// Editierbefehle (Tier 1): Move/Copy/Offset operieren auf der Auswahl.
|
||||||
"cmd.move.label": "Verschieben",
|
"cmd.move.label": "Verschieben",
|
||||||
"cmd.move.base": "Basispunkt:",
|
"cmd.move.base": "Basispunkt:",
|
||||||
|
|||||||
+2
-4
@@ -1130,11 +1130,9 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"cmd.arc.end": "End point (end angle):",
|
"cmd.arc.end": "End point (end angle):",
|
||||||
"cmd.text.label": "Text",
|
"cmd.text.label": "Text",
|
||||||
"cmd.text.point": "Anchor point:",
|
"cmd.text.point": "Anchor point:",
|
||||||
"cmd.text.enter": "Enter text:",
|
|
||||||
"cmd.textbox.label": "Text column",
|
"cmd.textbox.label": "Text column",
|
||||||
"cmd.textbox.point": "Anchor point:",
|
"cmd.textbox.first": "First corner:",
|
||||||
"cmd.textbox.width": "Column width (second point):",
|
"cmd.textbox.second": "Opposite corner (width × height):",
|
||||||
"cmd.textbox.enter": "Enter text:",
|
|
||||||
"cmd.move.label": "Move",
|
"cmd.move.label": "Move",
|
||||||
"cmd.move.base": "Base point:",
|
"cmd.move.base": "Base point:",
|
||||||
"cmd.move.target": "Target point:",
|
"cmd.move.target": "Target point:",
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export function useContextMenuState() {
|
|||||||
const [stampEditorRoomId, setStampEditorRoomId] = useState<string | null>(null);
|
const [stampEditorRoomId, setStampEditorRoomId] = useState<string | null>(null);
|
||||||
// Text-Inhalt-Editor: Id des per Doppelklick bearbeiteten Text-2D-Elements.
|
// Text-Inhalt-Editor: Id des per Doppelklick bearbeiteten Text-2D-Elements.
|
||||||
const [editTextId, setEditTextId] = useState<string | null>(null);
|
const [editTextId, setEditTextId] = useState<string | null>(null);
|
||||||
|
// Mindesthöhe (Meter) des Inline-Editors, wenn `editTextId` aus einem frisch
|
||||||
|
// aufgezogenen Textspalten-Rahmen kommt (s. textbox.ts focusMinHeightM);
|
||||||
|
// null = keine Mindesthöhe (Text-Werkzeug bzw. Doppelklick-Edit bestehender
|
||||||
|
// Elemente — dort bestimmt allein der Inhalt die Höhe).
|
||||||
|
const [editTextMinHeightM, setEditTextMinHeightM] = useState<number | null>(null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layerClipboard,
|
layerClipboard,
|
||||||
@@ -41,5 +46,7 @@ export function useContextMenuState() {
|
|||||||
setStampEditorRoomId,
|
setStampEditorRoomId,
|
||||||
editTextId,
|
editTextId,
|
||||||
setEditTextId,
|
setEditTextId,
|
||||||
|
editTextMinHeightM,
|
||||||
|
setEditTextMinHeightM,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -313,6 +313,12 @@ export interface PlanViewProps {
|
|||||||
* Ausschnitt.
|
* Ausschnitt.
|
||||||
*/
|
*/
|
||||||
editTextId?: string | null;
|
editTextId?: string | null;
|
||||||
|
/**
|
||||||
|
* Mindesthöhe (Meter) des Inline-Editors, wenn `editTextId` aus einem
|
||||||
|
* frisch aufgezogenen Textspalten-RAHMEN kommt (s. textbox.ts). `null`/
|
||||||
|
* undefined = keine Mindesthöhe, der Inhalt bestimmt die Höhe allein.
|
||||||
|
*/
|
||||||
|
editTextMinHeightM?: number | null;
|
||||||
/** Jede Änderung im Inline-Text-Editor (live, pro Tastendruck). */
|
/** Jede Änderung im Inline-Text-Editor (live, pro Tastendruck). */
|
||||||
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
/** Inline-Text-Editor schliessen (Backdrop-Klick/Escape). */
|
/** Inline-Text-Editor schliessen (Backdrop-Klick/Escape). */
|
||||||
@@ -445,6 +451,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
editTextId,
|
editTextId,
|
||||||
|
editTextMinHeightM,
|
||||||
onCommitTextEdit,
|
onCommitTextEdit,
|
||||||
onCloseTextEdit,
|
onCloseTextEdit,
|
||||||
onOpenSectionLevel,
|
onOpenSectionLevel,
|
||||||
@@ -2494,7 +2501,18 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
const screen = vbToClient(topAnchor.x, topAnchor.y);
|
const screen = vbToClient(topAnchor.x, topAnchor.y);
|
||||||
const FONT_PX_AT_12PT = 16; // 12pt @ 96dpi (RichTextEditor-Default, s. LayoutSheet.tsx).
|
const FONT_PX_AT_12PT = 16; // 12pt @ 96dpi (RichTextEditor-Default, s. LayoutSheet.tsx).
|
||||||
const scale = (prim.heightM * pxPerM) / FONT_PX_AT_12PT;
|
const scale = (prim.heightM * pxPerM) / FONT_PX_AT_12PT;
|
||||||
const widthPx = prim.wrapWidth ? (prim.wrapWidth * pxPerM) / scale : undefined;
|
// MIT wrapWidth (Textspalte): feste Breite, Editor bricht live um (nowrap
|
||||||
|
// AUS). OHNE (Text-Werkzeug): keine feste Breite — `.planview-inline-text
|
||||||
|
// .rt-surface` steht auf nowrap (s. styles.css), die Box wächst mit jedem
|
||||||
|
// Zeichen; eine Mindestbreite verhindert eine unsichtbare 0px-Box beim
|
||||||
|
// leeren Start (der eigentliche Grund, warum der Anker bisher unsichtbar
|
||||||
|
// blieb — `width:undefined` kollabierte auf shrink-to-fit von NICHTS).
|
||||||
|
const hasWrap = !!prim.wrapWidth;
|
||||||
|
const widthPx = hasWrap ? (prim.wrapWidth! * pxPerM) / scale : undefined;
|
||||||
|
const minWidthPx = hasWrap ? undefined : 160;
|
||||||
|
// Aufgezogener Textspalten-Rahmen (textbox.ts): dessen Höhe als
|
||||||
|
// Mindesthöhe des Editors — wächst bei mehr Text darüber hinaus.
|
||||||
|
const minHeightPx = editTextMinHeightM ? (editTextMinHeightM * pxPerM) / scale : undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -2502,11 +2520,13 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
onPointerDown={() => onCloseTextEdit?.()}
|
onPointerDown={() => onCloseTextEdit?.()}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="planview-inline-text"
|
className={"planview-inline-text" + (hasWrap ? "" : " planview-inline-text-free")}
|
||||||
style={{
|
style={{
|
||||||
left: screen.x,
|
left: screen.x,
|
||||||
top: screen.y,
|
top: screen.y,
|
||||||
width: widthPx,
|
width: widthPx,
|
||||||
|
minWidth: minWidthPx,
|
||||||
|
minHeight: minHeightPx,
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${scale})`,
|
||||||
transformOrigin: "0 0",
|
transformOrigin: "0 0",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function useMouseSelectionHandlers(opts: {
|
|||||||
>;
|
>;
|
||||||
contextMenuState: Pick<
|
contextMenuState: Pick<
|
||||||
ReturnType<typeof useContextMenuState>,
|
ReturnType<typeof useContextMenuState>,
|
||||||
"setMenu" | "setStampEditorRoomId" | "editTextId" | "setEditTextId"
|
"setMenu" | "setStampEditorRoomId" | "editTextId" | "setEditTextId" | "setEditTextMinHeightM"
|
||||||
>;
|
>;
|
||||||
}) {
|
}) {
|
||||||
const { project, setProject, activeLevelId, visibleCodes, categoryDisplay, selectionState, contextMenuState } = opts;
|
const { project, setProject, activeLevelId, visibleCodes, categoryDisplay, selectionState, contextMenuState } = opts;
|
||||||
@@ -80,7 +80,7 @@ export function useMouseSelectionHandlers(opts: {
|
|||||||
selectedSectionLineId,
|
selectedSectionLineId,
|
||||||
setSelectedSectionLineId,
|
setSelectedSectionLineId,
|
||||||
} = selectionState;
|
} = selectionState;
|
||||||
const { setMenu, setStampEditorRoomId, editTextId, setEditTextId } = contextMenuState;
|
const { setMenu, setStampEditorRoomId, editTextId, setEditTextId, setEditTextMinHeightM } = contextMenuState;
|
||||||
|
|
||||||
// ── Maus-Schema: Auswahl + Kontextmenü aus Grundriss / 3D ─────────────────
|
// ── Maus-Schema: Auswahl + Kontextmenü aus Grundriss / 3D ─────────────────
|
||||||
// Links-Klick im Grundriss: genau eine Wand wählen (Menge ersetzen) bzw.
|
// Links-Klick im Grundriss: genau eine Wand wählen (Menge ersetzen) bzw.
|
||||||
@@ -116,6 +116,10 @@ export function useMouseSelectionHandlers(opts: {
|
|||||||
setSelectedRoofIds([]);
|
setSelectedRoofIds([]);
|
||||||
setSelectedContextObjectIds([]);
|
setSelectedContextObjectIds([]);
|
||||||
setSelectedSectionLineId(null);
|
setSelectedSectionLineId(null);
|
||||||
|
// Bestehendes Element per Doppelklick: keine Rahmen-Mindesthöhe (nur bei
|
||||||
|
// frisch aufgezogener Textspalte relevant, s. textbox.ts focusMinHeightM)
|
||||||
|
// — sonst könnte eine vorige Textspalten-Erstellung hier nachwirken.
|
||||||
|
setEditTextMinHeightM(null);
|
||||||
setEditTextId(drawingId);
|
setEditTextId(drawingId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,6 +159,7 @@ export function useMouseSelectionHandlers(opts: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setEditTextId(null);
|
setEditTextId(null);
|
||||||
|
setEditTextMinHeightM(null);
|
||||||
};
|
};
|
||||||
// Kategorie-Code des getroffenen Elements (für die Sperr-Prüfung unten) —
|
// Kategorie-Code des getroffenen Elements (für die Sperr-Prüfung unten) —
|
||||||
// nur die Kanäle, deren Modell-Typ eine categoryCode trägt (ExtrudedSolid/
|
// nur die Kanäle, deren Modell-Typ eine categoryCode trägt (ExtrudedSolid/
|
||||||
|
|||||||
@@ -6793,6 +6793,16 @@ body {
|
|||||||
min-height: 1.4em;
|
min-height: 1.4em;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
/* Text-Werkzeug ohne Spaltenbreite: kein Wortumbruch — die Box wächst in der
|
||||||
|
Breite mit dem Inhalt (Enter erzeugt trotzdem weitere Zeilen), statt am
|
||||||
|
Editor-Rand umzubrechen wie die Textspalte. */
|
||||||
|
.planview-inline-text-free .rt-editor,
|
||||||
|
.planview-inline-text-free .rt-surface {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.planview-inline-text-free .rt-surface {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
/* HUD (Eigenschaften des gewählten VP) unten links. */
|
/* HUD (Eigenschaften des gewählten VP) unten links. */
|
||||||
.layout-sheet-hud {
|
.layout-sheet-hud {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export function Content({
|
|||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
editTextId,
|
editTextId,
|
||||||
|
editTextMinHeightM,
|
||||||
onCommitTextEdit,
|
onCommitTextEdit,
|
||||||
onCloseTextEdit,
|
onCloseTextEdit,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
@@ -182,6 +183,7 @@ export function Content({
|
|||||||
onStampEdit: (roomId: string) => void;
|
onStampEdit: (roomId: string) => void;
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
editTextId?: string | null;
|
editTextId?: string | null;
|
||||||
|
editTextMinHeightM?: number | null;
|
||||||
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
onCloseTextEdit?: () => void;
|
onCloseTextEdit?: () => void;
|
||||||
onPlanSelect: (sel: PlanSelection) => void;
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
@@ -307,6 +309,7 @@ export function Content({
|
|||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
editTextId={editTextId}
|
editTextId={editTextId}
|
||||||
|
editTextMinHeightM={editTextMinHeightM}
|
||||||
onCommitTextEdit={onCommitTextEdit}
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
onCloseTextEdit={onCloseTextEdit}
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onSelectLevel}
|
onOpenSectionLevel={onSelectLevel}
|
||||||
@@ -369,6 +372,7 @@ export function Content({
|
|||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
editTextId={editTextId}
|
editTextId={editTextId}
|
||||||
|
editTextMinHeightM={editTextMinHeightM}
|
||||||
onCommitTextEdit={onCommitTextEdit}
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
onCloseTextEdit={onCloseTextEdit}
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onSelectLevel}
|
onOpenSectionLevel={onSelectLevel}
|
||||||
@@ -473,6 +477,7 @@ export function Content({
|
|||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
editTextId={editTextId}
|
editTextId={editTextId}
|
||||||
|
editTextMinHeightM={editTextMinHeightM}
|
||||||
onCommitTextEdit={onCommitTextEdit}
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
onCloseTextEdit={onCloseTextEdit}
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onSelectLevel}
|
onOpenSectionLevel={onSelectLevel}
|
||||||
@@ -525,6 +530,7 @@ function LevelPlanView({
|
|||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
editTextId,
|
editTextId,
|
||||||
|
editTextMinHeightM,
|
||||||
onCommitTextEdit,
|
onCommitTextEdit,
|
||||||
onCloseTextEdit,
|
onCloseTextEdit,
|
||||||
onOpenSectionLevel,
|
onOpenSectionLevel,
|
||||||
@@ -570,6 +576,7 @@ function LevelPlanView({
|
|||||||
onStampEdit: (roomId: string) => void;
|
onStampEdit: (roomId: string) => void;
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
editTextId?: string | null;
|
editTextId?: string | null;
|
||||||
|
editTextMinHeightM?: number | null;
|
||||||
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
onCloseTextEdit?: () => void;
|
onCloseTextEdit?: () => void;
|
||||||
onOpenSectionLevel?: (id: string) => void;
|
onOpenSectionLevel?: (id: string) => void;
|
||||||
@@ -654,6 +661,7 @@ function LevelPlanView({
|
|||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
editTextId={editTextId}
|
editTextId={editTextId}
|
||||||
|
editTextMinHeightM={editTextMinHeightM}
|
||||||
onCommitTextEdit={onCommitTextEdit}
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
onCloseTextEdit={onCloseTextEdit}
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onOpenSectionLevel}
|
onOpenSectionLevel={onOpenSectionLevel}
|
||||||
@@ -713,6 +721,7 @@ function SectionPlanView({
|
|||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
editTextId,
|
editTextId,
|
||||||
|
editTextMinHeightM,
|
||||||
onCommitTextEdit,
|
onCommitTextEdit,
|
||||||
onCloseTextEdit,
|
onCloseTextEdit,
|
||||||
onOpenSectionLevel,
|
onOpenSectionLevel,
|
||||||
@@ -753,6 +762,7 @@ function SectionPlanView({
|
|||||||
onStampEdit: (roomId: string) => void;
|
onStampEdit: (roomId: string) => void;
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
editTextId?: string | null;
|
editTextId?: string | null;
|
||||||
|
editTextMinHeightM?: number | null;
|
||||||
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
onCloseTextEdit?: () => void;
|
onCloseTextEdit?: () => void;
|
||||||
onOpenSectionLevel?: (id: string) => void;
|
onOpenSectionLevel?: (id: string) => void;
|
||||||
@@ -863,6 +873,7 @@ function SectionPlanView({
|
|||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
editTextId={editTextId}
|
editTextId={editTextId}
|
||||||
|
editTextMinHeightM={editTextMinHeightM}
|
||||||
onCommitTextEdit={onCommitTextEdit}
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
onCloseTextEdit={onCloseTextEdit}
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onOpenSectionLevel}
|
onOpenSectionLevel={onOpenSectionLevel}
|
||||||
|
|||||||
Reference in New Issue
Block a user