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:
+28
-18
@@ -796,6 +796,25 @@ export default function App() {
|
|||||||
setDraft: (d) => cmdLatest.current.setDraft(d),
|
setDraft: (d) => cmdLatest.current.setDraft(d),
|
||||||
commit: (mutate) => cmdLatest.current.setProject(mutate),
|
commit: (mutate) => cmdLatest.current.setProject(mutate),
|
||||||
isFloor: () => cmdLatest.current.activeLevel.kind === "floor",
|
isFloor: () => cmdLatest.current.activeLevel.kind === "floor",
|
||||||
|
// Frisch über `text`/`textbox` erzeugtes Element sofort inline editieren
|
||||||
|
// (kein Lookup nötig — der Commit hat es bereits angehängt, s. engine.ts
|
||||||
|
// applyResult-Reihenfolge commit→focusDrawing). Auswahl-Löschen wie
|
||||||
|
// onEditDrawingText (useMouseSelectionHandlers.ts), damit kein anderes
|
||||||
|
// Element gleichzeitig markiert bleibt.
|
||||||
|
focusDrawing: (id) => {
|
||||||
|
setSelectedWallIds([]);
|
||||||
|
setSelectedCeilingIds([]);
|
||||||
|
setSelectedOpeningIds([]);
|
||||||
|
setSelectedStairIds([]);
|
||||||
|
setSelectedRoomIds([]);
|
||||||
|
setSelectedExtrudedSolidIds([]);
|
||||||
|
setSelectedColumnIds([]);
|
||||||
|
setSelectedRoofIds([]);
|
||||||
|
setSelectedContextObjectIds([]);
|
||||||
|
setSelectedSectionLineId(null);
|
||||||
|
setSelectedDrawingId(id);
|
||||||
|
setEditTextId(id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
engineRef.current = new CommandEngine(host);
|
engineRef.current = new CommandEngine(host);
|
||||||
}
|
}
|
||||||
@@ -2226,6 +2245,7 @@ export default function App() {
|
|||||||
onOpenStampEditor,
|
onOpenStampEditor,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
commitTextEdit,
|
commitTextEdit,
|
||||||
|
closeTextEdit,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
onPlanMarquee,
|
onPlanMarquee,
|
||||||
onPlanContextMenu,
|
onPlanContextMenu,
|
||||||
@@ -2715,6 +2735,9 @@ export default function App() {
|
|||||||
onStampMove={moveRoomStamp}
|
onStampMove={moveRoomStamp}
|
||||||
onStampEdit={onOpenStampEditor}
|
onStampEdit={onOpenStampEditor}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
editTextId={editTextId}
|
||||||
|
onCommitTextEdit={commitTextEdit}
|
||||||
|
onCloseTextEdit={closeTextEdit}
|
||||||
onPlanSelect={onPlanSelect}
|
onPlanSelect={onPlanSelect}
|
||||||
onPlanMarquee={onPlanMarquee}
|
onPlanMarquee={onPlanMarquee}
|
||||||
onSegmentCut={onSegmentCut}
|
onSegmentCut={onSegmentCut}
|
||||||
@@ -3022,24 +3045,11 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Rich-Text-Editor: per Doppelklick auf ein Text-2D-Element geöffnet (wie
|
{/* Inline-Text-Editor für Drawing2D-Text (`editTextId`) rendert PlanView
|
||||||
beim Raum-Stempel) — mehrere Absätze, gemischte Formatierung pro
|
jetzt direkt auf der Zeichenfläche (kein Dialog mehr, Nutzer-Wunsch —
|
||||||
Zeichen (InDesign-/Affinity-artig). Alt-Elemente ohne `doc` starten
|
gleiches Prinzip wie der Layout-Blatt-Inline-Editor). Ausgelöst über
|
||||||
aus ihrem bisherigen String + einheitlichen `marks`. */}
|
`text`/`textbox` (sofort nach Platzieren, s. host.focusDrawing) oder
|
||||||
{editTextId &&
|
Doppelklick auf ein bestehendes Element (onEditDrawingText). */}
|
||||||
(() => {
|
|
||||||
const d = project.drawings2d.find((x) => x.id === editTextId);
|
|
||||||
if (!d || d.geom.shape !== "text") return null;
|
|
||||||
const g = d.geom;
|
|
||||||
const doc = g.doc ?? docFromText(g.text, g.marks ?? {});
|
|
||||||
return (
|
|
||||||
<TextEditorDialog
|
|
||||||
value={doc}
|
|
||||||
onApply={commitTextEdit}
|
|
||||||
onCancel={() => setEditTextId(null)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* Text-Editor (schwebendes Fenster): Doppelklick auf einen Raumstempel bzw.
|
{/* Text-Editor (schwebendes Fenster): Doppelklick auf einen Raumstempel bzw.
|
||||||
Button in der Objekt-Info öffnet ihn. „Übernehmen" schreibt das
|
Button in der Objekt-Info öffnet ihn. „Übernehmen" schreibt das
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* `textCommand` — committet seit der Umstellung auf Inline-Editing (kein
|
||||||
|
* Dialog/keine Befehlszeilen-Eingabe mehr, Nutzer-Wunsch) SOFORT ein leeres
|
||||||
|
* Text-Element nach dem Ankerpunkt und meldet `focusDrawingId`, damit die UI
|
||||||
|
* (PlanView) direkt den Inline-Rich-Text-Editor öffnet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { textCommand } from "./text";
|
||||||
|
import type { CommandContext, Project } from "../types";
|
||||||
|
|
||||||
|
function project(): Project {
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [],
|
||||||
|
hatches: [],
|
||||||
|
components: [],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [
|
||||||
|
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
|
||||||
|
],
|
||||||
|
layers: [{ code: "20", name: "Zeichnung", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
stairs: [],
|
||||||
|
rooms: [],
|
||||||
|
drawings2d: [],
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCtx(p: Project): CommandContext {
|
||||||
|
return {
|
||||||
|
project: p,
|
||||||
|
level: p.drawingLevels[0],
|
||||||
|
defaultCategoryCode: "20",
|
||||||
|
activeLineStyleId: "solid",
|
||||||
|
activeWallTypeId: "aw",
|
||||||
|
lastPoint: null,
|
||||||
|
selection: { wallIds: [], drawingId: null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("textCommand", () => {
|
||||||
|
it("committet sofort ein leeres Text-Element und meldet focusDrawingId", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
const [, result] = textCommand.onInput(
|
||||||
|
textCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 3, y: 4 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
expect(result.done).toBe(true);
|
||||||
|
expect(result.commit).toBeDefined();
|
||||||
|
expect(result.focusDrawingId).toBeDefined();
|
||||||
|
|
||||||
|
const next = result.commit!(p);
|
||||||
|
expect(next.drawings2d.length).toBe(1);
|
||||||
|
const d = next.drawings2d[0];
|
||||||
|
expect(d.id).toBe(result.focusDrawingId); // dieselbe ID wie im Commit erzeugt
|
||||||
|
expect(d.geom.shape).toBe("text");
|
||||||
|
if (d.geom.shape === "text") {
|
||||||
|
expect(d.geom.text).toBe("");
|
||||||
|
expect(d.geom.at).toEqual({ x: 3, y: 4 });
|
||||||
|
expect(d.geom.width).toBeUndefined(); // Text-Werkzeug: KEIN Wortumbruch
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+25
-35
@@ -1,21 +1,16 @@
|
|||||||
// Text — modellverankerter Einzeltext als 2D-Element. Schritte:
|
// Text — modellverankerter Einzeltext als 2D-Element. Schritte:
|
||||||
// 1) „Ankerpunkt:" → Punkt (Position im Modell)
|
// 1) „Ankerpunkt:" → Punkt (Position im Modell)
|
||||||
// 2) „Text:" → getippte Zeile (das Label) → commit Drawing2D {shape:"text"}
|
// Committet SOFORT ein leeres Text-Element und meldet `focusDrawingId` —
|
||||||
//
|
// die UI (PlanView) öffnet daraufhin direkt den Inline-Rich-Text-Editor AUF
|
||||||
// Der Text-Schritt nimmt Freitext an (accepts:["text"]) — die Engine reicht die
|
// der Zeichenfläche (kein Dialog, keine Befehlszeilen-Eingabe). Ohne
|
||||||
// komplette getippte Zeile 1:1 durch (auch Zahlen/Kommas). Gerendert wird der
|
// Spaltenbreite bricht der Text NICHT automatisch um: einzeilig, bis der
|
||||||
// Text in generatePlan als `drawingText` (SVG), analog zum DXF-Import.
|
// Nutzer selbst Enter drückt (siehe `textbox` fürs InDesign-artige
|
||||||
|
// Wortumbruch-Textfeld). Bleibt der Editor leer, verwirft `commitTextEdit`
|
||||||
|
// (App.tsx) das Element wieder.
|
||||||
|
|
||||||
import type { Drawing2D } from "../../model/types";
|
import type { Drawing2D } from "../../model/types";
|
||||||
import { uniqueId } from "../../tools/types";
|
import { uniqueId } from "../../tools/types";
|
||||||
import type {
|
import type { Command, CommandContext, CommandResult, CommandState, Project, Vec2 } from "../types";
|
||||||
Command,
|
|
||||||
CommandContext,
|
|
||||||
CommandResult,
|
|
||||||
CommandState,
|
|
||||||
Project,
|
|
||||||
Vec2,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
/** Default-Schrifthöhe eines frei platzierten Texts in Modell-Metern. */
|
/** Default-Schrifthöhe eines frei platzierten Texts in Modell-Metern. */
|
||||||
const DEFAULT_TEXT_HEIGHT_M = 0.25;
|
const DEFAULT_TEXT_HEIGHT_M = 0.25;
|
||||||
@@ -23,50 +18,45 @@ const DEFAULT_TEXT_HEIGHT_M = 0.25;
|
|||||||
interface TextIdle extends CommandState {
|
interface TextIdle extends CommandState {
|
||||||
phase: "point";
|
phase: "point";
|
||||||
}
|
}
|
||||||
interface TextLabel extends CommandState {
|
type TextState = TextIdle;
|
||||||
phase: "label";
|
|
||||||
at: Vec2;
|
|
||||||
}
|
|
||||||
type TextState = TextIdle | TextLabel;
|
|
||||||
|
|
||||||
function appendText(p: Project, at: Vec2, text: string, ctx: CommandContext): Project {
|
function appendEmptyText(p: Project, id: string, at: Vec2, ctx: CommandContext): Project {
|
||||||
const label = text.trim();
|
|
||||||
if (label === "") return p;
|
|
||||||
const d: Drawing2D = {
|
const d: Drawing2D = {
|
||||||
id: uniqueId("dr2d"),
|
id,
|
||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
levelId: ctx.level.id,
|
levelId: ctx.level.id,
|
||||||
categoryCode: ctx.defaultCategoryCode,
|
categoryCode: ctx.defaultCategoryCode,
|
||||||
geom: { shape: "text", at, text: label, height: DEFAULT_TEXT_HEIGHT_M, angle: 0 },
|
geom: { shape: "text", at, text: "", height: DEFAULT_TEXT_HEIGHT_M, angle: 0 },
|
||||||
};
|
};
|
||||||
return { ...p, drawings2d: [...p.drawings2d, d] };
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const idle = (): [CommandState, CommandResult] => [
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
{ phase: "point", lastPoint: null },
|
{ phase: "point", lastPoint: null } as TextIdle,
|
||||||
{ draft: null, done: true },
|
{ draft: null, done: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const textCommand: Command = {
|
export const textCommand: Command = {
|
||||||
name: "text",
|
name: "text",
|
||||||
labelKey: "cmd.text.label",
|
labelKey: "cmd.text.label",
|
||||||
prompt: (s) => ((s as TextState).phase === "label" ? "cmd.text.enter" : "cmd.text.point"),
|
prompt: () => "cmd.text.point",
|
||||||
accepts: (s) => ((s as TextState).phase === "label" ? ["text"] : ["point"]),
|
accepts: () => ["point"],
|
||||||
options: () => [],
|
options: () => [],
|
||||||
init: (): TextIdle => ({ phase: "point", lastPoint: null }),
|
init: (): TextIdle => ({ phase: "point", lastPoint: null }),
|
||||||
|
|
||||||
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||||
const s = state as TextState;
|
const s = state as TextState;
|
||||||
if (s.phase !== "label") {
|
if (input.kind !== "point") return [s, { draft: null }];
|
||||||
if (input.kind !== "point") return [s, { draft: null }];
|
const id = uniqueId("dr2d");
|
||||||
const ns: TextLabel = { phase: "label", at: input.point, lastPoint: input.point };
|
const at = input.point;
|
||||||
return [ns, { draft: null }];
|
|
||||||
}
|
|
||||||
if (input.kind !== "text") return [s, { draft: null }];
|
|
||||||
const at = s.at;
|
|
||||||
return [
|
return [
|
||||||
{ phase: "point", lastPoint: null },
|
{ phase: "point", lastPoint: null } as TextIdle,
|
||||||
{ draft: null, done: true, commit: (p) => appendText(p, at, input.text, ctx) },
|
{
|
||||||
|
draft: null,
|
||||||
|
done: true,
|
||||||
|
commit: (p) => appendEmptyText(p, id, at, ctx),
|
||||||
|
focusDrawingId: id,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* `textboxCommand` — committet seit der Umstellung auf Inline-Editing (kein
|
||||||
|
* Dialog/keine Befehlszeilen-Eingabe mehr, InDesign-artiges Textrahmen-
|
||||||
|
* Verhalten, Nutzer-Wunsch) SOFORT ein leeres Text-Element MIT Spaltenbreite
|
||||||
|
* nach Anker+Breite und meldet `focusDrawingId`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { textboxCommand } from "./textbox";
|
||||||
|
import type { CommandContext, Project } from "../types";
|
||||||
|
|
||||||
|
function project(): Project {
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [],
|
||||||
|
hatches: [],
|
||||||
|
components: [],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [
|
||||||
|
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
|
||||||
|
],
|
||||||
|
layers: [{ code: "20", name: "Zeichnung", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
stairs: [],
|
||||||
|
rooms: [],
|
||||||
|
drawings2d: [],
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCtx(p: Project): CommandContext {
|
||||||
|
return {
|
||||||
|
project: p,
|
||||||
|
level: p.drawingLevels[0],
|
||||||
|
defaultCategoryCode: "20",
|
||||||
|
activeLineStyleId: "solid",
|
||||||
|
activeWallTypeId: "aw",
|
||||||
|
lastPoint: null,
|
||||||
|
selection: { wallIds: [], drawingId: null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("textboxCommand", () => {
|
||||||
|
it("committet nach Anker+Breite sofort ein leeres Text-Element mit width und meldet focusDrawingId", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
|
||||||
|
const [afterPoint] = textboxCommand.onInput(
|
||||||
|
textboxCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 0, y: 0 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
const [, result] = textboxCommand.onInput(
|
||||||
|
afterPoint,
|
||||||
|
{ kind: "point", point: { x: 2, y: 0 } }, // Spaltenbreite = 2m
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
expect(result.done).toBe(true);
|
||||||
|
expect(result.commit).toBeDefined();
|
||||||
|
expect(result.focusDrawingId).toBeDefined();
|
||||||
|
|
||||||
|
const next = result.commit!(p);
|
||||||
|
expect(next.drawings2d.length).toBe(1);
|
||||||
|
const d = next.drawings2d[0];
|
||||||
|
expect(d.id).toBe(result.focusDrawingId);
|
||||||
|
expect(d.geom.shape).toBe("text");
|
||||||
|
if (d.geom.shape === "text") {
|
||||||
|
expect(d.geom.text).toBe("");
|
||||||
|
expect(d.geom.width).toBeCloseTo(2, 9); // InDesign-artiger Textrahmen: Breite gesetzt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
// Textspalte (Absatztext) — wie das Text-Werkzeug, aber mit einer Spaltenbreite:
|
// Textspalte (Absatztext) — wie das Text-Werkzeug, aber mit einer Spaltenbreite:
|
||||||
// der Text bricht beim Rendern wortweise auf diese Breite um. Schritte:
|
// der Text bricht beim Rendern UND beim Editieren wortweise auf diese Breite
|
||||||
|
// um (InDesign-artiges Textrahmen-Verhalten, Nutzer-Wunsch). Schritte:
|
||||||
// 1) „Ankerpunkt:" → Punkt (oben-links der Spalte)
|
// 1) „Ankerpunkt:" → Punkt (oben-links der Spalte)
|
||||||
// 2) „Spaltenbreite:" → Punkt (die horizontale Distanz zum Anker = Breite;
|
// 2) „Spaltenbreite:" → Punkt (die horizontale Distanz zum Anker = Breite;
|
||||||
// Live-Vorschau der Breitenlinie)
|
// Live-Vorschau der Breitenlinie)
|
||||||
// 3) „Text:" → getippte Zeile → commit Drawing2D {shape:"text", width}
|
// Committet danach SOFORT ein leeres Text-Element (mit `width`) und meldet
|
||||||
|
// `focusDrawingId` — die UI (PlanView) öffnet den Inline-Rich-Text-Editor
|
||||||
|
// direkt auf der Zeichenfläche, mit der Spaltenbreite als CSS-Breite (echter
|
||||||
|
// Wortumbruch beim Tippen, kein Dialog). Bleibt der Editor leer, verwirft
|
||||||
|
// `commitTextEdit` (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 Selektion,
|
||||||
// Verschieben und Griff automatisch vom Einzeltext; nur die Breite kommt hinzu.
|
// Verschieben und Griff automatisch vom Einzeltext; nur die Breite kommt hinzu.
|
||||||
@@ -33,12 +38,7 @@ interface TbWidth extends CommandState {
|
|||||||
at: Vec2;
|
at: Vec2;
|
||||||
cursor: Vec2 | null;
|
cursor: Vec2 | null;
|
||||||
}
|
}
|
||||||
interface TbLabel extends CommandState {
|
type TbState = TbPoint | TbWidth;
|
||||||
phase: "label";
|
|
||||||
at: Vec2;
|
|
||||||
width: number;
|
|
||||||
}
|
|
||||||
type TbState = TbPoint | TbWidth | TbLabel;
|
|
||||||
|
|
||||||
/** Horizontale Spaltenbreite aus Anker + Cursor (Betrag der x-Differenz, geklemmt). */
|
/** Horizontale Spaltenbreite aus Anker + Cursor (Betrag der x-Differenz, geklemmt). */
|
||||||
function widthOf(at: Vec2, cursor: Vec2): number {
|
function widthOf(at: Vec2, cursor: Vec2): number {
|
||||||
@@ -56,21 +56,13 @@ function widthDraft(at: Vec2, cursor: Vec2): ToolDraft {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendTextbox(
|
function appendEmptyTextbox(p: Project, id: string, at: Vec2, width: number, ctx: CommandContext): Project {
|
||||||
p: Project,
|
|
||||||
at: Vec2,
|
|
||||||
width: number,
|
|
||||||
text: string,
|
|
||||||
ctx: CommandContext,
|
|
||||||
): Project {
|
|
||||||
const label = text.trim();
|
|
||||||
if (label === "") return p;
|
|
||||||
const d: Drawing2D = {
|
const d: Drawing2D = {
|
||||||
id: uniqueId("dr2d"),
|
id,
|
||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
levelId: ctx.level.id,
|
levelId: ctx.level.id,
|
||||||
categoryCode: ctx.defaultCategoryCode,
|
categoryCode: ctx.defaultCategoryCode,
|
||||||
geom: { shape: "text", at, text: label, height: DEFAULT_TEXT_HEIGHT_M, angle: 0, width },
|
geom: { shape: "text", at, text: "", height: DEFAULT_TEXT_HEIGHT_M, angle: 0, width },
|
||||||
};
|
};
|
||||||
return { ...p, drawings2d: [...p.drawings2d, d] };
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||||
}
|
}
|
||||||
@@ -83,35 +75,30 @@ const idle = (): [CommandState, CommandResult] => [
|
|||||||
export const textboxCommand: Command = {
|
export const textboxCommand: Command = {
|
||||||
name: "textbox",
|
name: "textbox",
|
||||||
labelKey: "cmd.textbox.label",
|
labelKey: "cmd.textbox.label",
|
||||||
prompt: (s) => {
|
prompt: (s) => ((s as TbState).phase === "width" ? "cmd.textbox.width" : "cmd.textbox.point"),
|
||||||
const ts = s as TbState;
|
accepts: () => ["point"],
|
||||||
if (ts.phase === "label") return "cmd.textbox.enter";
|
|
||||||
if (ts.phase === "width") return "cmd.textbox.width";
|
|
||||||
return "cmd.textbox.point";
|
|
||||||
},
|
|
||||||
accepts: (s) => ((s as TbState).phase === "label" ? ["text"] : ["point"]),
|
|
||||||
options: () => [],
|
options: () => [],
|
||||||
init: (): TbPoint => ({ phase: "point", lastPoint: null }),
|
init: (): TbPoint => ({ phase: "point", 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 (s.phase === "point") {
|
if (s.phase === "point") {
|
||||||
if (input.kind !== "point") return [s, { draft: null }];
|
|
||||||
const ns: TbWidth = { phase: "width", at: input.point, cursor: input.point, lastPoint: input.point };
|
const ns: TbWidth = { phase: "width", at: input.point, cursor: input.point, lastPoint: input.point };
|
||||||
return [ns, { draft: widthDraft(input.point, input.point) }];
|
return [ns, { draft: widthDraft(input.point, input.point) }];
|
||||||
}
|
}
|
||||||
if (s.phase === "width") {
|
// phase === "width"
|
||||||
if (input.kind !== "point") return [s, { draft: s.cursor ? widthDraft(s.at, s.cursor) : null }];
|
const width = widthOf(s.at, input.point);
|
||||||
const width = widthOf(s.at, input.point);
|
const id = uniqueId("dr2d");
|
||||||
const ns: TbLabel = { phase: "label", at: s.at, width, lastPoint: input.point };
|
const at = s.at;
|
||||||
return [ns, { draft: null }];
|
|
||||||
}
|
|
||||||
// label
|
|
||||||
if (input.kind !== "text") return [s, { draft: null }];
|
|
||||||
const { at, width } = s;
|
|
||||||
return [
|
return [
|
||||||
{ phase: "point", lastPoint: null } as TbPoint,
|
{ phase: "point", lastPoint: null } as TbPoint,
|
||||||
{ draft: null, done: true, commit: (p) => appendTextbox(p, at, width, input.text, ctx) },
|
{
|
||||||
|
draft: null,
|
||||||
|
done: true,
|
||||||
|
commit: (p) => appendEmptyTextbox(p, id, at, width, ctx),
|
||||||
|
focusDrawingId: id,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export interface EngineHost {
|
|||||||
commit(mutate: (p: Project) => Project): void;
|
commit(mutate: (p: Project) => Project): void;
|
||||||
/** Ist die aktive Ebene ein Geschoss? (für floorOnly-Befehle). */
|
/** Ist die aktive Ebene ein Geschoss? (für floorOnly-Befehle). */
|
||||||
isFloor(): boolean;
|
isFloor(): boolean;
|
||||||
|
/**
|
||||||
|
* Optional: ein frisch erzeugtes Drawing2D-Element fokussieren (Passthrough
|
||||||
|
* von `CommandResult.focusDrawingId`, z. B. um sofort den Inline-Text-Editor
|
||||||
|
* zu öffnen). Fehlt der Host-Handler, ist das ein No-op.
|
||||||
|
*/
|
||||||
|
focusDrawing?(drawingId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Öffentlicher Schnappschuss für die Command-Line-UI (rein lesend). */
|
/** Öffentlicher Schnappschuss für die Command-Line-UI (rein lesend). */
|
||||||
@@ -476,6 +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.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) {
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ export interface CommandResult {
|
|||||||
commit?: (p: Project) => Project;
|
commit?: (p: Project) => Project;
|
||||||
/** true → Befehl ist fertig und kehrt in den Ruhezustand zurück. */
|
/** true → Befehl ist fertig und kehrt in den Ruhezustand zurück. */
|
||||||
done?: boolean;
|
done?: boolean;
|
||||||
|
/**
|
||||||
|
* Bei Abschluss: die Engine soll dieses neu erzeugte Drawing2D-Element
|
||||||
|
* sofort fokussieren (z. B. Inline-Text-Editor öffnen, wie `text`/`textbox`
|
||||||
|
* nach dem Platzieren). Reiner Passthrough an `EngineHost.focusDrawing`;
|
||||||
|
* die Engine selbst wertet die ID nicht aus.
|
||||||
|
*/
|
||||||
|
focusDrawingId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Befehls-Zustand ──────────────────────────────────────────────────────────
|
// ── Befehls-Zustand ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import {
|
|||||||
import type { HatchRender, Plan, Primitive } from "./generatePlan";
|
import type { HatchRender, Plan, Primitive } from "./generatePlan";
|
||||||
import type { EdgeGrip, Vec2 } from "../model/types";
|
import type { EdgeGrip, Vec2 } from "../model/types";
|
||||||
import type { ToolHandlers, ToolId, ToolMods } from "../tools/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 { useGlPlanRenderer } from "./useGlPlanRenderer";
|
||||||
import { useWasmPlanRenderer } from "./useWasmPlanRenderer";
|
import { useWasmPlanRenderer } from "./useWasmPlanRenderer";
|
||||||
import { DEFAULT_MARQUEE_COLOR, DEFAULT_SNAP_COLOR } from "../theme/accents";
|
import { DEFAULT_MARQUEE_COLOR, DEFAULT_SNAP_COLOR } from "../theme/accents";
|
||||||
@@ -301,6 +304,19 @@ export interface PlanViewProps {
|
|||||||
onStampEdit?: (roomId: string) => void;
|
onStampEdit?: (roomId: string) => void;
|
||||||
/** Doppelklick auf ein Text-2D-Element: Inhalt bearbeiten (Editor öffnen). */
|
/** Doppelklick auf ein Text-2D-Element: Inhalt bearbeiten (Editor öffnen). */
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
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
|
/** Doppelklick auf eine Schnittlinie: in deren 2D-Schnittansicht springen
|
||||||
* (Zeichnungsebene aktivieren). */
|
* (Zeichnungsebene aktivieren). */
|
||||||
onOpenSectionLevel?: (levelId: string) => void;
|
onOpenSectionLevel?: (levelId: string) => void;
|
||||||
@@ -428,6 +444,9 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
onStampMove,
|
onStampMove,
|
||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
|
editTextId,
|
||||||
|
onCommitTextEdit,
|
||||||
|
onCloseTextEdit,
|
||||||
onOpenSectionLevel,
|
onOpenSectionLevel,
|
||||||
activeTool,
|
activeTool,
|
||||||
toolInputActive,
|
toolInputActive,
|
||||||
@@ -2450,6 +2469,60 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</svg>
|
</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
|
{/* Lineale (oben + links, VW-Stil): Modell-Meter mit Ticks, Beschriftung
|
||||||
und Cursor-Marker — screen-fix über den sichtbaren Ausschnitt gelegt.
|
und Cursor-Marker — screen-fix über den sichtbaren Ausschnitt gelegt.
|
||||||
BEWUSST ein eigenes Sibling-SVG (nicht mehr Kind von `svgRef`): beim
|
BEWUSST ein eigenes Sibling-SVG (nicht mehr Kind von `svgRef`): beim
|
||||||
|
|||||||
@@ -121,19 +121,38 @@ export function useMouseSelectionHandlers(opts: {
|
|||||||
|
|
||||||
// Bearbeitetes Rich-Text-Dokument übernehmen (immutabel; nur bei Text-Geometrie).
|
// Bearbeitetes Rich-Text-Dokument übernehmen (immutabel; nur bei Text-Geometrie).
|
||||||
// `text` bleibt als Reintext-Spiegel bestehen (DXF-Export, Suche, Alt-Konsumenten).
|
// `text` bleibt als Reintext-Spiegel bestehen (DXF-Export, Suche, Alt-Konsumenten).
|
||||||
|
// LIVE bei jedem Tastendruck (Inline-Editor auf der Zeichenfläche, kein Dialog
|
||||||
|
// mehr) — schreibt AUCH einen leeren Zustand zurück (kein Guard mehr), damit
|
||||||
|
// Löschen bis auf null Zeichen sich sofort im Modell spiegelt; das eigentliche
|
||||||
|
// Aufräumen eines leer verlassenen NEUEN Elements übernimmt `closeTextEdit`.
|
||||||
const commitTextEdit = (doc: RichTextDoc) => {
|
const commitTextEdit = (doc: RichTextDoc) => {
|
||||||
const id = editTextId;
|
const id = editTextId;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const text = plainText(doc).trim();
|
setProject((p) => ({
|
||||||
if (text !== "") {
|
...p,
|
||||||
setProject((p) => ({
|
drawings2d: p.drawings2d.map((d) =>
|
||||||
...p,
|
d.id === id && d.geom.shape === "text"
|
||||||
drawings2d: p.drawings2d.map((d) =>
|
? { ...d, geom: { ...d.geom, doc, text: plainText(doc) } }
|
||||||
d.id === id && d.geom.shape === "text"
|
: d,
|
||||||
? { ...d, geom: { ...d.geom, doc, text: plainText(doc) } }
|
),
|
||||||
: d,
|
}));
|
||||||
),
|
};
|
||||||
}));
|
|
||||||
|
// Inline-Editor schliessen (Backdrop-Klick/Escape): bleibt das Element leer
|
||||||
|
// (frisch über `text`/`textbox` erzeugt, nie befüllt, oder komplett wieder
|
||||||
|
// geleert), wird es entfernt statt als leere Leiche im Modell zu bleiben.
|
||||||
|
const closeTextEdit = () => {
|
||||||
|
const id = editTextId;
|
||||||
|
if (id) {
|
||||||
|
setProject((p) => {
|
||||||
|
const d = p.drawings2d.find((x) => x.id === id);
|
||||||
|
// `text` ist immer der Reintext-Spiegel von `doc` (s. commitTextEdit) —
|
||||||
|
// reicht daher allein als Leer-Test, egal ob `doc` je gesetzt wurde.
|
||||||
|
if (d && d.geom.shape === "text" && d.geom.text.trim() === "") {
|
||||||
|
return { ...p, drawings2d: p.drawings2d.filter((x) => x.id !== id) };
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setEditTextId(null);
|
setEditTextId(null);
|
||||||
};
|
};
|
||||||
@@ -509,6 +528,7 @@ export function useMouseSelectionHandlers(opts: {
|
|||||||
onOpenStampEditor,
|
onOpenStampEditor,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
commitTextEdit,
|
commitTextEdit,
|
||||||
|
closeTextEdit,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
onPlanMarquee,
|
onPlanMarquee,
|
||||||
onPlanContextMenu,
|
onPlanContextMenu,
|
||||||
|
|||||||
@@ -6770,6 +6770,30 @@ body {
|
|||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline-Text-Editor auf der 2D-Zeichenfläche (PlanView) — dasselbe Prinzip
|
||||||
|
wie .layout-sheet-inline-text/-backdrop, nur `position:fixed` mit echten
|
||||||
|
Client-Pixeln (vbToClient), da PlanView kein eigenes mm-Koordinatensystem
|
||||||
|
hat wie das Layout-Blatt. */
|
||||||
|
.planview-inline-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
.planview-inline-text {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 7;
|
||||||
|
background: #ffffff;
|
||||||
|
outline: 1.5px solid #3b82f6;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.planview-inline-text .rt-editor {
|
||||||
|
min-height: inherit;
|
||||||
|
}
|
||||||
|
.planview-inline-text .rt-surface {
|
||||||
|
min-height: 1.4em;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* HUD (Eigenschaften des gewählten VP) unten links. */
|
/* HUD (Eigenschaften des gewählten VP) unten links. */
|
||||||
.layout-sheet-hud {
|
.layout-sheet-hud {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from "../model/types";
|
} from "../model/types";
|
||||||
import { generatePlan, generateSectionPlan } from "../plan/generatePlan";
|
import { generatePlan, generateSectionPlan } from "../plan/generatePlan";
|
||||||
import type { CategoryDisplayResolver } from "../plan/generatePlan";
|
import type { CategoryDisplayResolver } from "../plan/generatePlan";
|
||||||
|
import type { RichTextDoc } from "../text/richText";
|
||||||
import { computeSection } from "../plan/toSection";
|
import { computeSection } from "../plan/toSection";
|
||||||
import type { SectionOutput } from "../plan/toSection";
|
import type { SectionOutput } from "../plan/toSection";
|
||||||
import { generateElevationPlan } from "../plan/toElevation";
|
import { generateElevationPlan } from "../plan/toElevation";
|
||||||
@@ -91,6 +92,9 @@ export function Content({
|
|||||||
onStampMove,
|
onStampMove,
|
||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
|
editTextId,
|
||||||
|
onCommitTextEdit,
|
||||||
|
onCloseTextEdit,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
onPlanMarquee,
|
onPlanMarquee,
|
||||||
onSegmentCut,
|
onSegmentCut,
|
||||||
@@ -177,6 +181,9 @@ export function Content({
|
|||||||
onStampMove: (roomId: string, delta: Vec2) => void;
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
onStampEdit: (roomId: string) => void;
|
onStampEdit: (roomId: string) => void;
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
|
editTextId?: string | null;
|
||||||
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
|
onCloseTextEdit?: () => void;
|
||||||
onPlanSelect: (sel: PlanSelection) => void;
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
onPlanMarquee: (sel: MarqueeSelection) => void;
|
onPlanMarquee: (sel: MarqueeSelection) => void;
|
||||||
onSegmentCut: (drawingId: string, model: Vec2) => void;
|
onSegmentCut: (drawingId: string, model: Vec2) => void;
|
||||||
@@ -299,6 +306,9 @@ export function Content({
|
|||||||
onStampMove={onStampMove}
|
onStampMove={onStampMove}
|
||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
editTextId={editTextId}
|
||||||
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onSelectLevel}
|
onOpenSectionLevel={onSelectLevel}
|
||||||
onPlanSelect={onPlanSelect}
|
onPlanSelect={onPlanSelect}
|
||||||
onPlanMarquee={onPlanMarquee}
|
onPlanMarquee={onPlanMarquee}
|
||||||
@@ -358,6 +368,9 @@ export function Content({
|
|||||||
onStampMove={onStampMove}
|
onStampMove={onStampMove}
|
||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
editTextId={editTextId}
|
||||||
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onSelectLevel}
|
onOpenSectionLevel={onSelectLevel}
|
||||||
onPlanSelect={onPlanSelect}
|
onPlanSelect={onPlanSelect}
|
||||||
onPlanMarquee={onPlanMarquee}
|
onPlanMarquee={onPlanMarquee}
|
||||||
@@ -459,6 +472,9 @@ export function Content({
|
|||||||
onStampMove={onStampMove}
|
onStampMove={onStampMove}
|
||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
editTextId={editTextId}
|
||||||
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onSelectLevel}
|
onOpenSectionLevel={onSelectLevel}
|
||||||
onPlanSelect={onPlanSelect}
|
onPlanSelect={onPlanSelect}
|
||||||
onPlanMarquee={onPlanMarquee}
|
onPlanMarquee={onPlanMarquee}
|
||||||
@@ -508,6 +524,9 @@ function LevelPlanView({
|
|||||||
onStampMove,
|
onStampMove,
|
||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
|
editTextId,
|
||||||
|
onCommitTextEdit,
|
||||||
|
onCloseTextEdit,
|
||||||
onOpenSectionLevel,
|
onOpenSectionLevel,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
onPlanMarquee,
|
onPlanMarquee,
|
||||||
@@ -550,6 +569,9 @@ function LevelPlanView({
|
|||||||
onStampMove: (roomId: string, delta: Vec2) => void;
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
onStampEdit: (roomId: string) => void;
|
onStampEdit: (roomId: string) => void;
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
|
editTextId?: string | null;
|
||||||
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
|
onCloseTextEdit?: () => void;
|
||||||
onOpenSectionLevel?: (id: string) => void;
|
onOpenSectionLevel?: (id: string) => void;
|
||||||
onPlanSelect: (sel: PlanSelection) => void;
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
onPlanMarquee: (sel: MarqueeSelection) => void;
|
onPlanMarquee: (sel: MarqueeSelection) => void;
|
||||||
@@ -631,6 +653,9 @@ function LevelPlanView({
|
|||||||
onStampMove={onStampMove}
|
onStampMove={onStampMove}
|
||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
editTextId={editTextId}
|
||||||
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onOpenSectionLevel}
|
onOpenSectionLevel={onOpenSectionLevel}
|
||||||
onSelect={onPlanSelect}
|
onSelect={onPlanSelect}
|
||||||
onMarquee={onPlanMarquee}
|
onMarquee={onPlanMarquee}
|
||||||
@@ -687,6 +712,9 @@ function SectionPlanView({
|
|||||||
onStampMove,
|
onStampMove,
|
||||||
onStampEdit,
|
onStampEdit,
|
||||||
onEditDrawingText,
|
onEditDrawingText,
|
||||||
|
editTextId,
|
||||||
|
onCommitTextEdit,
|
||||||
|
onCloseTextEdit,
|
||||||
onOpenSectionLevel,
|
onOpenSectionLevel,
|
||||||
onPlanSelect,
|
onPlanSelect,
|
||||||
onPlanMarquee,
|
onPlanMarquee,
|
||||||
@@ -724,6 +752,9 @@ function SectionPlanView({
|
|||||||
onStampMove: (roomId: string, delta: Vec2) => void;
|
onStampMove: (roomId: string, delta: Vec2) => void;
|
||||||
onStampEdit: (roomId: string) => void;
|
onStampEdit: (roomId: string) => void;
|
||||||
onEditDrawingText?: (drawingId: string) => void;
|
onEditDrawingText?: (drawingId: string) => void;
|
||||||
|
editTextId?: string | null;
|
||||||
|
onCommitTextEdit?: (doc: RichTextDoc) => void;
|
||||||
|
onCloseTextEdit?: () => void;
|
||||||
onOpenSectionLevel?: (id: string) => void;
|
onOpenSectionLevel?: (id: string) => void;
|
||||||
onPlanSelect: (sel: PlanSelection) => void;
|
onPlanSelect: (sel: PlanSelection) => void;
|
||||||
onPlanMarquee: (sel: MarqueeSelection) => void;
|
onPlanMarquee: (sel: MarqueeSelection) => void;
|
||||||
@@ -831,6 +862,9 @@ function SectionPlanView({
|
|||||||
onStampMove={onStampMove}
|
onStampMove={onStampMove}
|
||||||
onStampEdit={onStampEdit}
|
onStampEdit={onStampEdit}
|
||||||
onEditDrawingText={onEditDrawingText}
|
onEditDrawingText={onEditDrawingText}
|
||||||
|
editTextId={editTextId}
|
||||||
|
onCommitTextEdit={onCommitTextEdit}
|
||||||
|
onCloseTextEdit={onCloseTextEdit}
|
||||||
onOpenSectionLevel={onOpenSectionLevel}
|
onOpenSectionLevel={onOpenSectionLevel}
|
||||||
onSelect={onPlanSelect}
|
onSelect={onPlanSelect}
|
||||||
onMarquee={onPlanMarquee}
|
onMarquee={onPlanMarquee}
|
||||||
|
|||||||
Reference in New Issue
Block a user