Zeichen-Feedback: Δx/Δy beim Rechteck, lange Führungslinien, Winkelbogen

VW-Feinschliff (Nutzer-Screenshots):
- Rechteck (2-Punkt + Zentrum) zeigt Δx/Δy vorzeichenbehaftet statt
  Diagonale+Winkel (deltaHud; Zentrum = volle Rechteckmasse). Die
  width/height-Tab-Felder erscheinen im HUD als Δx/Δy.
- Führungslinie des Winkelrasters läuft jetzt weit über den Cursor hinaus
  (quer über den Ausschnitt statt 40 px).
- Winkelbogen ergänzt: gestrichelter Bogen von der horizontalen 0°-Referenz
  (dezentes Lachsrot, VW-Anlehnung) zur Strahlrichtung, Badge sitzt am
  halben Winkel aussen am Bogen.
737/737 grün; headless verifiziert (Linie 45° + Rechteck-Δ).
This commit is contained in:
2026-07-11 01:44:32 +02:00
parent 7898a0d158
commit 242b850beb
4 changed files with 80 additions and 11 deletions
+8 -3
View File
@@ -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). */
+47 -7
View File
@@ -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"
/>
<line
className="plan-angle-ref-line"
x1={a.x}
y1={a.y}
x2={refEnd.x}
y2={refEnd.y}
vectorEffect="non-scaling-stroke"
/>
{len > 24 * vbPerPx && Math.abs(screenAng) > 0.02 && (
<path className="plan-angle-arc" d={arcPath} vectorEffect="non-scaling-stroke" />
)}
<rect
className="plan-angle-badge-box"
x={badgeAt.x - w / 2}
+15
View File
@@ -2066,6 +2066,21 @@ body {
stroke-dasharray: 5 4;
opacity: 0.8;
}
/* Horizontale 0°-Referenz des Winkelbogens (dezentes Lachsrot, VW-Anlehnung). */
.plan-svg .plan-angle-ref-line {
stroke: #cf8d7f;
stroke-width: 1;
stroke-dasharray: 7 5;
opacity: 0.8;
}
/* Gestrichelter Winkelbogen von der Referenz zur Strahlrichtung. */
.plan-svg .plan-angle-arc {
fill: none;
stroke: #8a8a8a;
stroke-width: 1;
stroke-dasharray: 5 4;
opacity: 0.8;
}
.plan-svg .plan-angle-badge-box {
fill: #2c2c2c;
fill-opacity: 0.94;
+10 -1
View File
@@ -164,7 +164,7 @@ export interface ToolDraft {
* einfaches Text-Label. Beide Formen sind additiv kombinierbar (text kann als
* Zusatzzeile neben length/angleDeg stehen).
*/
hud?: { at: Vec2; text?: string; length?: number; angleDeg?: number };
hud?: { at: Vec2; text?: string; length?: number; angleDeg?: number; dx?: number; dy?: number };
/** Live-Messwerte (nur beim Mess-Werkzeug gesetzt) fürs Objekt-Info-Panel. */
measure?: MeasurementReadout;
}
@@ -253,6 +253,15 @@ export function segmentHud(from: Vec2, to: Vec2): { at: Vec2; length: number; an
};
}
/**
* HUD für achsparallele Masse (Rechtecke, VW-Verhalten): Δx/Δy VORZEICHENBEHAFTET
* statt Diagonale+Winkel — die Diagonale eines Rechtecks interessiert beim
* Aufziehen nicht, die Kantenmasse schon.
*/
export function deltaHud(from: Vec2, to: Vec2): { at: Vec2; dx: number; dy: number } {
return { at: to, dx: to.x - from.x, dy: to.y - from.y };
}
// ── ID-Vergabe ───────────────────────────────────────────────────────────────
let idCounter = 0;