Files
DOSSIER-STANDALONE/src/commands/cmds/textbox.ts
T
karim 1d939a939a Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)
Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md ✅-Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
2026-07-09 00:57:29 +02:00

128 lines
4.2 KiB
TypeScript

// Textspalte (Absatztext) — wie das Text-Werkzeug, aber mit einer Spaltenbreite:
// der Text bricht beim Rendern wortweise auf diese Breite um. Schritte:
// 1) „Ankerpunkt:" → Punkt (oben-links der Spalte)
// 2) „Spaltenbreite:" → Punkt (die horizontale Distanz zum Anker = Breite;
// Live-Vorschau der Breitenlinie)
// 3) „Text:" → getippte Zeile → commit Drawing2D {shape:"text", width}
//
// Reuse des bestehenden {shape:"text"}-Elements (mit `width`) — so erben Selektion,
// Verschieben und Griff automatisch vom Einzeltext; nur die Breite kommt hinzu.
import type { Drawing2D } from "../../model/types";
import { uniqueId } from "../../tools/types";
import type {
Command,
CommandContext,
CommandResult,
CommandState,
Project,
ToolDraft,
Vec2,
} from "../types";
/** Default-Schrifthöhe einer Textspalte in Modell-Metern. */
const DEFAULT_TEXT_HEIGHT_M = 0.25;
/** Kleinste sinnvolle Spaltenbreite (Meter). */
const MIN_WIDTH_M = 0.2;
interface TbPoint extends CommandState {
phase: "point";
}
interface TbWidth extends CommandState {
phase: "width";
at: Vec2;
cursor: Vec2 | null;
}
interface TbLabel extends CommandState {
phase: "label";
at: Vec2;
width: number;
}
type TbState = TbPoint | TbWidth | TbLabel;
/** Horizontale Spaltenbreite aus Anker + Cursor (Betrag der x-Differenz, geklemmt). */
function widthOf(at: Vec2, cursor: Vec2): number {
return Math.max(MIN_WIDTH_M, Math.abs(cursor.x - at.x));
}
/** Vorschau: waagrechte Breitenlinie am Anker (zeigt die Spaltenbreite). */
function widthDraft(at: Vec2, cursor: Vec2): ToolDraft {
const w = widthOf(at, cursor);
const b: Vec2 = { x: at.x + w, y: at.y };
return {
preview: [{ kind: "line", a: at, b }],
vertices: [at, b],
hud: { at: b, text: `${w.toFixed(2)} m` },
};
}
function appendTextbox(
p: Project,
at: Vec2,
width: number,
text: string,
ctx: CommandContext,
): Project {
const label = text.trim();
if (label === "") return p;
const d: Drawing2D = {
id: uniqueId("dr2d"),
type: "drawing2d",
levelId: ctx.level.id,
categoryCode: ctx.defaultCategoryCode,
geom: { shape: "text", at, text: label, height: DEFAULT_TEXT_HEIGHT_M, angle: 0, width },
};
return { ...p, drawings2d: [...p.drawings2d, d] };
}
const idle = (): [CommandState, CommandResult] => [
{ phase: "point", lastPoint: null } as TbPoint,
{ draft: null, done: true },
];
export const textboxCommand: Command = {
name: "textbox",
labelKey: "cmd.textbox.label",
prompt: (s) => {
const ts = s as TbState;
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: () => [],
init: (): TbPoint => ({ phase: "point", lastPoint: null }),
onInput: (state, input, ctx): [CommandState, CommandResult] => {
const s = state as TbState;
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 };
return [ns, { draft: widthDraft(input.point, input.point) }];
}
if (s.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 ns: TbLabel = { phase: "label", at: s.at, width, lastPoint: input.point };
return [ns, { draft: null }];
}
// label
if (input.kind !== "text") return [s, { draft: null }];
const { at, width } = s;
return [
{ phase: "point", lastPoint: null } as TbPoint,
{ draft: null, done: true, commit: (p) => appendTextbox(p, at, width, input.text, ctx) },
];
},
onMove: (state, point): [CommandState, CommandResult] => {
const s = state as TbState;
if (s.phase !== "width") return [s, { draft: null }];
const ns: TbWidth = { ...s, cursor: point };
return [ns, { draft: widthDraft(s.at, point) }];
},
onConfirm: (): [CommandState, CommandResult] => idle(),
onCancel: (): [CommandState, CommandResult] => idle(),
};