diff --git a/src/commands/cmds/rect.ts b/src/commands/cmds/rect.ts index 9f1c247..bf1c26c 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 { segmentHud, uniqueId } from "../../tools/types"; +import { deltaHud, segmentHud, uniqueId } from "../../tools/types"; import type { Command, CommandContext, @@ -208,7 +208,7 @@ 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 }]; - return corners4Draft(pts, segLen(a, cursor) >= EPS ? segmentHud(a, cursor) : null); + return corners4Draft(pts, segLen(a, cursor) >= EPS ? deltaHud(a, cursor) : null); } /** Vorschau des Zentrum-Rechtecks (achsparallel) — HUD zeigt die Diagonale center→cursor. */ @@ -216,7 +216,12 @@ 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 }]; - return corners4Draft(pts, segLen(center, cursor) >= EPS ? segmentHud(center, cursor) : null); + return corners4Draft( + pts, + segLen(center, cursor) >= EPS + ? { at: cursor, dx: 2 * (cursor.x - center.x), dy: 2 * (cursor.y - center.y) } + : null, + ); } /** Vorschau der Basiskante (3-Punkt, erster Teil). */ diff --git a/src/plan/PlanView.tsx b/src/plan/PlanView.tsx index 32a6a7e..fe70c86 100644 --- a/src/plan/PlanView.tsx +++ b/src/plan/PlanView.tsx @@ -2447,11 +2447,22 @@ function DraftHud({ locked: boolean; } const liveFor = (id: string): number | null => - id === "length" ? (hud.length ?? null) : id === "angle" ? (hud.angleDeg ?? null) : null; + id === "length" + ? (hud.length ?? null) + : id === "angle" + ? (hud.angleDeg ?? null) + : id === "width" + ? (hud.dx ?? null) + : id === "height" + ? (hud.dy ?? null) + : null; + // Feld-Beschriftung: Länge/Winkel als L/W, Rechteck-Masse als Δx/Δy (VW). + const labelFor = (id: string): string => + id === "length" ? "L" : id === "angle" ? "W" : id === "width" ? "Δx" : id === "height" ? "Δy" : id; const segs: Seg[] = []; if (hudFields && hudFields.fields.length > 0) { for (const f of hudFields.fields) { - const label = f.id === "length" ? "L" : f.id === "angle" ? "W" : f.id; + const label = labelFor(f.id); const unit = f.id === "angle" ? "°" : "m"; const typedHere = f.active && hudFields.typed.trim() !== ""; const value = typedHere @@ -2461,6 +2472,9 @@ function DraftHud({ : (liveFor(f.id)?.toFixed(3) ?? "—"); segs.push({ text: `${label}: ${value}${unit}`, active: f.active, locked: f.locked }); } + } else if (hud.dx != null && hud.dy != null) { + segs.push({ text: `Δx: ${hud.dx.toFixed(3)}m`, active: false, locked: false }); + segs.push({ text: `Δy: ${hud.dy.toFixed(3)}m`, active: false, locked: false }); } else if (hud.length != null && hud.angleDeg != null) { segs.push({ text: `L: ${hud.length.toFixed(3)}m`, active: false, locked: false }); segs.push({ text: `W: ${hud.angleDeg.toFixed(3)}°`, active: false, locked: false }); @@ -2541,12 +2555,27 @@ function AngleGuide({ const dy = b.y - a.y; const len = Math.hypot(dx, dy); const dir = len > 1e-6 ? { x: dx / len, y: dy / len } : { x: 1, y: 0 }; - const extend = 40 * vbPerPx; // Bildschirm-px, die die Führungslinie über den Cursor hinausragt + // Führungslinie WEIT über den Cursor hinaus (VW-Verhalten: die Referenz läuft + // quer über den sichtbaren Ausschnitt; der SVG-Clip schneidet den Rest ab). + const extend = 4000 * vbPerPx; const end = { x: b.x + dir.x * extend, y: b.y + dir.y * extend }; - const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; - // Senkrecht zur Linie versetzt, damit das Badge die Führungslinie nicht verdeckt. - const perp = { x: -dir.y, y: dir.x }; - const badgeAt = { x: mid.x + perp.x * 10 * vbPerPx, y: mid.y + perp.y * 10 * vbPerPx }; + // Winkelbogen (VW): gestrichelter Bogen von der HORIZONTALEN Referenz (0°) zur + // Strahlrichtung, Radius knapp innerhalb des Cursors. Screen-y läuft nach + // unten → der Screen-Winkel ist der negierte Modellwinkel; für den Bogenpfad + // rechnen wir direkt mit Screen-Vektoren. + const screenAng = Math.atan2(dy, dx); // (−π, π], y-down + const r = Math.min(len * 0.85, 160 * vbPerPx); + const arcStart = { x: a.x + r, y: a.y }; + const arcEnd = { x: a.x + r * Math.cos(screenAng), y: a.y + r * Math.sin(screenAng) }; + const sweep = screenAng > 0 ? 1 : 0; // y-down: positiver Screen-Winkel = im Uhrzeigersinn + const arcPath = `M ${arcStart.x} ${arcStart.y} A ${r} ${r} 0 0 ${sweep} ${arcEnd.x} ${arcEnd.y}`; + // Horizontale Referenz-Strichellinie (0°-Basis des Bogens), etwas über den + // Bogen hinaus. + const refEnd = { x: a.x + r + 60 * vbPerPx, y: a.y }; + // Badge aussen am Bogen bei halbem Winkel (VW-Position). + const halfAng = screenAng / 2; + const badgeR = r + 22 * vbPerPx; + const badgeAt = { x: a.x + badgeR * Math.cos(halfAng), y: a.y + badgeR * Math.sin(halfAng) }; const fontSize = 11 * vbPerPx; const padX = 5 * vbPerPx; const padY = 3 * vbPerPx; @@ -2563,6 +2592,17 @@ function AngleGuide({ y2={end.y} vectorEffect="non-scaling-stroke" /> + + {len > 24 * vbPerPx && Math.abs(screenAng) > 0.02 && ( + + )}