L/W-HUD in den Zeichenbefehlen (line/polyline/wall/rect/circle/arc)
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).
This commit is contained in:
@@ -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` },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+10
-19
@@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user