From 4b0f23d1231939d063ea717adbeb84c1d48a4374 Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 11 Jul 2026 00:21:35 +0200 Subject: [PATCH] L/W-HUD in den Zeichenbefehlen (line/polyline/wall/rect/circle/arc) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alle punktbasierten Zeichenbefehle setzen den Cursor-HUD jetzt einheitlich über den gemeinsamen segmentHud()-Helper statt eigener Text-Formatierung: - line/polyline/wall: L/W-Kästchen für das lebende Segment ab dem letzten Punkt. - rect: 2-Punkt/Zentrum-Methode zeigt die Diagonale als L/W; die 3-Punkt- Methode zeigt die Basiskante als L/W bzw. Basisbreite+Höhe im Rise-Schritt. - circle/arc (Radius-Schritt): Radius als „R: …m"-Label statt L/W (kein Winkel sinnvoll); arc zeigt im Spannwinkel-Schritt Radius als L und Spannwinkel als W. Alle Werte jetzt mit 3 Nachkommastellen (vorher 2 bzw. 0), konsistent mit der VW-Konvention der Statusleiste. Tests: line.test.ts/wall.test.ts prüfen hud.length/angleDeg im onMove-Draft (inkl. negativer Winkel im Bereich (−180,180] und Segment-Wechsel bei mehrteiligen Wandzügen). --- src/commands/cmds/arc.ts | 6 +-- src/commands/cmds/circle.ts | 2 +- src/commands/cmds/line.test.ts | 87 ++++++++++++++++++++++++++++++++++ src/commands/cmds/line.ts | 11 +---- src/commands/cmds/polyline.ts | 11 +---- src/commands/cmds/rect.ts | 29 ++++-------- src/commands/cmds/wall.test.ts | 87 ++++++++++++++++++++++++++++++++++ src/commands/cmds/wall.ts | 11 +---- 8 files changed, 194 insertions(+), 50 deletions(-) create mode 100644 src/commands/cmds/line.test.ts create mode 100644 src/commands/cmds/wall.test.ts diff --git a/src/commands/cmds/arc.ts b/src/commands/cmds/arc.ts index 2c3fda0..77337b3 100644 --- a/src/commands/cmds/arc.ts +++ b/src/commands/cmds/arc.ts @@ -73,14 +73,14 @@ function circlePts(center: Vec2, r: number): Vec2[] { return pts; } -/** Vorschau (offener Bogen) + HUD (Spanne in Grad). */ +/** Vorschau (offener Bogen) + HUD (Radius als L, Spanne als W — L/W-Kästchen). */ function arcDraft(center: Vec2, r: number, a0: number, a1: number, at: Vec2 | null): ToolDraft { if (r < EPS) return { preview: [], vertices: [center] }; const preview: DraftShape[] = [{ kind: "poly", pts: arcPts(center, r, a0, a1), closed: false }]; const draft: ToolDraft = { preview, vertices: [center] }; if (at) { const deg = (sweepOf(a0, a1) * 180) / Math.PI; - draft.hud = { at, text: `${deg.toFixed(0)}°` }; + draft.hud = { at, length: r, angleDeg: deg }; } return draft; } @@ -148,7 +148,7 @@ export const arcCommand: Command = { draft: { preview: [{ kind: "poly", pts: circlePts(s.center, r), closed: true }], vertices: [s.center], - hud: { at: point, text: `r ${r.toFixed(2)} m` }, + hud: { at: point, text: `R: ${r.toFixed(3)}m` }, }, }, ]; diff --git a/src/commands/cmds/circle.ts b/src/commands/cmds/circle.ts index 5c17b4d..6e83325 100644 --- a/src/commands/cmds/circle.ts +++ b/src/commands/cmds/circle.ts @@ -68,7 +68,7 @@ function circleDraft(center: Vec2, r: number, at: Vec2 | null): ToolDraft { if (r < EPS) return { preview: [], vertices: [center] }; const preview: DraftShape[] = [{ kind: "poly", pts: circlePts(center, r), closed: true }]; const draft: ToolDraft = { preview, vertices: [center] }; - if (at) draft.hud = { at, text: `r ${r.toFixed(2)} m` }; + if (at) draft.hud = { at, text: `R: ${r.toFixed(3)}m` }; return draft; } diff --git a/src/commands/cmds/line.test.ts b/src/commands/cmds/line.test.ts new file mode 100644 index 0000000..6aa0122 --- /dev/null +++ b/src/commands/cmds/line.test.ts @@ -0,0 +1,87 @@ +/** + * Unit-Tests für das Cursor-HUD (Länge/Winkel) des `line`-Befehls: nach dem + * Startpunkt muss `onMove` im Draft `hud.length`/`hud.angleDeg` mit den + * korrekten Werten liefern (VW-Stil L/W-Kästchen, PlanView rendert es). + */ + +import { describe, it, expect } from "vitest"; +import { lineCommand } from "./line"; +import type { CommandContext, Project, Vec2 } from "../types"; + +/** Minimalprojekt — nur die Felder, die der Befehl/Context anfassen. */ +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(lastPoint: Vec2 | null): CommandContext { + const p = project(); + return { + project: p, + level: p.drawingLevels[0], + defaultCategoryCode: "20", + activeLineStyleId: "solid", + activeWallTypeId: "aw", + lastPoint, + selection: { wallIds: [], drawingId: null }, + }; +} + +describe("lineCommand — Cursor-HUD (Länge/Winkel)", () => { + it("liefert hud.length/angleDeg im onMove nach dem Startpunkt", () => { + const start: Vec2 = { x: 0, y: 0 }; + const [afterStart] = lineCommand.onInput( + lineCommand.init(), + { kind: "point", point: start }, + makeCtx(null), + ); + const [, res] = lineCommand.onMove(afterStart, { x: 3, y: 4 }, null, makeCtx(start)); + expect(res.draft?.hud?.length).toBeCloseTo(5, 9); + expect(res.draft?.hud?.angleDeg).toBeCloseTo((Math.atan2(4, 3) * 180) / Math.PI, 9); + }); + + it("liefert einen negativen Winkel (Bereich (−180,180]) für Segmente nach unten-links", () => { + const start: Vec2 = { x: 0, y: 0 }; + const [afterStart] = lineCommand.onInput( + lineCommand.init(), + { kind: "point", point: start }, + makeCtx(null), + ); + // Richtung (-1,-1) → -135° (wie im VW-Screenshot). + const [, res] = lineCommand.onMove(afterStart, { x: -2, y: -2 }, null, makeCtx(start)); + expect(res.draft?.hud?.angleDeg).toBeCloseTo(-135, 9); + expect(res.draft?.hud?.length).toBeCloseTo(Math.hypot(2, 2), 9); + }); + + it("zeigt KEIN HUD vor dem Startpunkt (phase=start)", () => { + const [, res] = lineCommand.onMove(lineCommand.init(), { x: 1, y: 1 }, null, makeCtx(null)); + expect(res.draft).toBeNull(); + }); +}); diff --git a/src/commands/cmds/line.ts b/src/commands/cmds/line.ts index 465093b..a8af1cf 100644 --- a/src/commands/cmds/line.ts +++ b/src/commands/cmds/line.ts @@ -10,7 +10,7 @@ // Tool. Live-Vorschau + HUD (Länge/Winkel) wie die bestehenden Tools. import type { Drawing2D } from "../../model/types"; -import { uniqueId } from "../../tools/types"; +import { segmentHud, uniqueId } from "../../tools/types"; import type { Command, CommandContext, @@ -61,14 +61,7 @@ function lineDraft(a: Vec2, cursor: Vec2 | null): ToolDraft { const preview: DraftShape[] = []; if (cursor && segLen(a, cursor) >= EPS) preview.push({ kind: "line", a, b: cursor }); const draft: ToolDraft = { preview, vertices: [a] }; - if (cursor && segLen(a, cursor) >= EPS) { - draft.hud = { - at: cursor, - text: `${segLen(a, cursor).toFixed(2)} m · ${Math.abs( - (segAngleDeg(a, cursor) + 360) % 360, - ).toFixed(0)}°`, - }; - } + if (cursor && segLen(a, cursor) >= EPS) draft.hud = segmentHud(a, cursor); return draft; } diff --git a/src/commands/cmds/polyline.ts b/src/commands/cmds/polyline.ts index 4b84178..258b276 100644 --- a/src/commands/cmds/polyline.ts +++ b/src/commands/cmds/polyline.ts @@ -6,7 +6,7 @@ // Akzeptiert je Punkt Maus-Picks UND getippte Koordinaten (0,0 · r3,0 · 5<45). import type { Drawing2D } from "../../model/types"; -import { uniqueId } from "../../tools/types"; +import { segmentHud, uniqueId } from "../../tools/types"; import type { Command, CommandContext, @@ -74,14 +74,7 @@ function polyDraft(points: Vec2[], cursor: Vec2 | null, closed = false): ToolDra const preview: DraftShape[] = [{ kind: "poly", pts, closed: showClosed }]; const draft: ToolDraft = { preview, vertices: points }; const last = points[points.length - 1]; - if (cursor && last && segLen(last, cursor) >= EPS) { - draft.hud = { - at: cursor, - text: `${segLen(last, cursor).toFixed(2)} m · ${Math.abs( - (segAngleDeg(last, cursor) + 360) % 360, - ).toFixed(0)}°`, - }; - } + if (cursor && last && segLen(last, cursor) >= EPS) draft.hud = segmentHud(last, cursor); return draft; } diff --git a/src/commands/cmds/rect.ts b/src/commands/cmds/rect.ts index 6e1c04b..9f1c247 100644 --- a/src/commands/cmds/rect.ts +++ b/src/commands/cmds/rect.ts @@ -17,7 +17,7 @@ // werden (die Engine löst `r…` relativ zu lastPoint auf). import type { Drawing2D } from "../../model/types"; -import { uniqueId } from "../../tools/types"; +import { segmentHud, uniqueId } from "../../tools/types"; import type { Command, CommandContext, @@ -196,51 +196,42 @@ function rectGeom(a: Vec2, b: Vec2): { min: Vec2; max: Vec2 } { // ── Vorschau ───────────────────────────────────────────────────────────────── -function corners4Draft(pts: Vec2[], hudAt: Vec2 | null, hudText: string): ToolDraft { +function corners4Draft(pts: Vec2[], hud: ToolDraft["hud"] | null): ToolDraft { const preview: DraftShape[] = [{ kind: "poly", pts, closed: true }]; const draft: ToolDraft = { preview, vertices: [pts[0]] }; - if (hudAt) draft.hud = { at: hudAt, text: hudText }; + if (hud) draft.hud = hud; return draft; } -/** Vorschau des 2-Punkt-Rechtecks (achsparallel). */ +/** Vorschau des 2-Punkt-Rechtecks (achsparallel) — HUD zeigt die Diagonale a→cursor. */ function cornerDraft(a: Vec2, cursor: Vec2 | null): ToolDraft { if (!cursor) return { preview: [], vertices: [a] }; const g = rectGeom(a, cursor); const pts: Vec2[] = [g.min, { x: g.max.x, y: g.min.y }, g.max, { x: g.min.x, y: g.max.y }]; - const w = g.max.x - g.min.x; - const h = g.max.y - g.min.y; - return corners4Draft(pts, cursor, `${w.toFixed(2)} × ${h.toFixed(2)} m`); + return corners4Draft(pts, segLen(a, cursor) >= EPS ? segmentHud(a, cursor) : null); } -/** Vorschau des Zentrum-Rechtecks (achsparallel). */ +/** Vorschau des Zentrum-Rechtecks (achsparallel) — HUD zeigt die Diagonale center→cursor. */ function centerDraft(center: Vec2, cursor: Vec2 | null): ToolDraft { if (!cursor) return { preview: [], vertices: [center] }; const g = centerGeom(center, cursor); const pts: Vec2[] = [g.min, { x: g.max.x, y: g.min.y }, g.max, { x: g.min.x, y: g.max.y }]; - const w = g.max.x - g.min.x; - const h = g.max.y - g.min.y; - return corners4Draft(pts, cursor, `${w.toFixed(2)} × ${h.toFixed(2)} m`); + return corners4Draft(pts, segLen(center, cursor) >= EPS ? segmentHud(center, cursor) : null); } /** Vorschau der Basiskante (3-Punkt, erster Teil). */ function baseDraft(a: Vec2, cursor: Vec2 | null): ToolDraft { if (!cursor || segLen(a, cursor) < EPS) return { preview: [{ kind: "line", a, b: a }], vertices: [a] }; - const draft: ToolDraft = { preview: [{ kind: "line", a, b: cursor }], vertices: [a] }; - draft.hud = { - at: cursor, - text: `${segLen(a, cursor).toFixed(2)} m · ${Math.abs((segAngleDeg(a, cursor) + 360) % 360).toFixed(0)}°`, - }; - return draft; + return { preview: [{ kind: "line", a, b: cursor }], vertices: [a], hud: segmentHud(a, cursor) }; } -/** Vorschau des gedrehten Rechtecks (3-Punkt, zweiter Teil). */ +/** Vorschau des gedrehten Rechtecks (3-Punkt, zweiter Teil) — HUD: Basisbreite + Höhe am Cursor. */ function riseDraft(a: Vec2, b: Vec2, cursor: Vec2 | null): ToolDraft { if (!cursor) return { preview: [{ kind: "line", a, b }], vertices: [a] }; const pts = rotatedCorners(a, b, cursor); const w = segLen(a, b); const h = Math.abs(riseFrom(a, b, cursor)); - return corners4Draft(pts, cursor, `${w.toFixed(2)} × ${h.toFixed(2)} m`); + return corners4Draft(pts, { at: cursor, text: `B: ${w.toFixed(3)}m H: ${h.toFixed(3)}m` }); } // ── Commit ─────────────────────────────────────────────────────────────────── diff --git a/src/commands/cmds/wall.test.ts b/src/commands/cmds/wall.test.ts new file mode 100644 index 0000000..7e1795a --- /dev/null +++ b/src/commands/cmds/wall.test.ts @@ -0,0 +1,87 @@ +/** + * Unit-Tests für das Cursor-HUD (Länge/Winkel) des `wall`-Befehls: nach dem + * Startpunkt muss `onMove` im Draft `hud.length`/`hud.angleDeg` relativ zum + * ZULETZT gesetzten Achspunkt liefern — auch nach mehreren Segmenten + * (Chaining), damit ein mehrteiliger Wandzug je Segment ein frisches HUD zeigt. + */ + +import { describe, it, expect } from "vitest"; +import { wallCommand } from "./wall"; +import type { CommandContext, Project, Vec2 } from "../types"; + +function project(): Project { + return { + id: "t", + name: "T", + lineStyles: [], + hatches: [], + components: [{ id: "c", name: "C", color: "#ccc", hatchId: "none", joinPriority: 10 }], + wallTypes: [ + { id: "aw", name: "AW", layers: [{ componentId: "c", thickness: 0.2 }] }, + ], + drawingLevels: [ + { + id: "eg", + name: "EG", + kind: "floor", + visible: true, + locked: false, + floorHeight: 2.6, + cutHeight: 1.0, + baseElevation: 0, + }, + ], + layers: [{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }], + walls: [], + doors: [], + openings: [], + ceilings: [], + stairs: [], + rooms: [], + drawings2d: [], + context: [], + }; +} + +function makeCtx(lastPoint: Vec2 | null): CommandContext { + const p = project(); + return { + project: p, + level: p.drawingLevels[0], + defaultCategoryCode: "20", + activeLineStyleId: "solid", + activeWallTypeId: "aw", + lastPoint, + selection: { wallIds: [], drawingId: null }, + }; +} + +describe("wallCommand — Cursor-HUD (Länge/Winkel)", () => { + it("liefert hud.length/angleDeg relativ zum Startpunkt", () => { + const start: Vec2 = { x: 0, y: 0 }; + const [afterStart] = wallCommand.onInput( + wallCommand.init(), + { kind: "point", point: start }, + makeCtx(null), + ); + const [, res] = wallCommand.onMove(afterStart, { x: 4, y: 0 }, null, makeCtx(start)); + expect(res.draft?.hud?.length).toBeCloseTo(4, 9); + expect(res.draft?.hud?.angleDeg).toBeCloseTo(0, 9); + }); + + it("HUD bezieht sich nach dem zweiten Punkt auf das NEUE (letzte) Segment", () => { + const p0: Vec2 = { x: 0, y: 0 }; + const p1: Vec2 = { x: 4, y: 0 }; + const [s1] = wallCommand.onInput(wallCommand.init(), { kind: "point", point: p0 }, makeCtx(null)); + const [s2] = wallCommand.onInput(s1, { kind: "point", point: p1 }, makeCtx(p0)); + // Nächstes Segment ab p1 nach oben (0,3) → Länge 3, Winkel 90°. + const [, res] = wallCommand.onMove(s2, { x: 4, y: 3 }, null, makeCtx(p1)); + expect(res.draft?.hud?.length).toBeCloseTo(3, 9); + expect(res.draft?.hud?.angleDeg).toBeCloseTo(90, 9); + }); + + it("zeigt KEIN HUD vor dem Startpunkt (phase=start)", () => { + const [, res] = wallCommand.onMove(wallCommand.init(), { x: 1, y: 1 }, null, makeCtx(null)); + expect(res.draft).toBeNull(); + }); +}); diff --git a/src/commands/cmds/wall.ts b/src/commands/cmds/wall.ts index 01965d7..059a967 100644 --- a/src/commands/cmds/wall.ts +++ b/src/commands/cmds/wall.ts @@ -18,7 +18,7 @@ import type { Wall, WallType } from "../../model/types"; import { wallTypeThickness } from "../../model/types"; import { wallCorners } from "../../model/geometry"; -import { uniqueId } from "../../tools/types"; +import { segmentHud, uniqueId } from "../../tools/types"; import type { Command, CommandContext, @@ -164,14 +164,7 @@ function wallDraft(points: Vec2[], cursor: Vec2 | null, thickness: number): Tool } const draft: ToolDraft = { preview, vertices: points }; const last = points[points.length - 1]; - if (cursor && last && segLen(last, cursor) >= EPS) { - draft.hud = { - at: cursor, - text: `${segLen(last, cursor).toFixed(2)} m · ${Math.abs( - (segAngleDeg(last, cursor) + 360) % 360, - ).toFixed(0)}°`, - }; - } + if (cursor && last && segLen(last, cursor) >= EPS) draft.hud = segmentHud(last, cursor); return draft; }