1d939a939a
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.
128 lines
4.2 KiB
TypeScript
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(),
|
|
};
|