492e1f811a
Das Mess-Werkzeug wird von der reinen Strecke zum Polygonzug: - Jeder Klick setzt einen weiteren Stützpunkt; angezeigt werden letztes Segment (Länge · Winkel), aufsummierte Pfadlänge und — ab 3 Ecken — die umschlossene Fläche (Gauss). - Die Messwerte erscheinen dauerhaft im Objekt-Info-Panel (neuer Abschnitt „Messung"), nicht nur flüchtig am Cursor-HUD. Kanal: ToolDraft.measure → PanelHost.measurement → ObjectInfoPanel. - Rechtsklick/Enter beendet den aktuellen Pfad und armiert sofort einen neuen (mehrere Messungen nacheinander); Rechtsklick auf leerem Pfad bzw. Esc beendet das Werkzeug. +4 Tests (Segment/Winkel, Summe/Fläche, Ketten-Verhalten). tsc sauber.
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
// Messen — schnelles, unverbindliches Mess-Werkzeug (kein Element wird erzeugt).
|
|
// Ein Mess-Pfad ist ein Polygonzug: jeder Klick setzt einen weiteren Stützpunkt.
|
|
// • 1 Punkt gesetzt → Live-Strecke (Länge · Winkel) zum Cursor.
|
|
// • ≥ 2 Punkte gesetzt → zusätzlich aufsummierte Pfadlänge; sobald der Zug
|
|
// (inkl. Cursor) ≥ 3 Ecken hat, zeigt er auch die umschlossene FLÄCHE.
|
|
// Rechtsklick/Enter beendet den aktuellen Pfad und armiert einen NEUEN — so misst
|
|
// man mehrere nacheinander. Ein weiterer Rechtsklick auf leerem Pfad (bzw. Esc)
|
|
// beendet das Werkzeug. Rein visuell: `commit` bleibt immer leer.
|
|
|
|
import type {
|
|
Command,
|
|
CommandResult,
|
|
CommandState,
|
|
ToolDraft,
|
|
Vec2,
|
|
} from "../types";
|
|
import type { MeasurementReadout } from "../../tools/types";
|
|
|
|
const segLen = (a: Vec2, b: Vec2): number => Math.hypot(b.x - a.x, b.y - a.y);
|
|
const segAngleDeg = (a: Vec2, b: Vec2): number =>
|
|
(Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
|
|
|
|
/** Aufsummierte Länge eines Polygonzugs (offene Kette). */
|
|
function pathLength(pts: Vec2[]): number {
|
|
let sum = 0;
|
|
for (let i = 1; i < pts.length; i++) sum += segLen(pts[i - 1], pts[i]);
|
|
return sum;
|
|
}
|
|
|
|
/** Vorzeichenlose Fläche (Gauss/Schuhband), Zug als geschlossen gedacht. */
|
|
function polygonArea(pts: Vec2[]): number {
|
|
if (pts.length < 3) return 0;
|
|
let a = 0;
|
|
for (let i = 0; i < pts.length; i++) {
|
|
const p = pts[i];
|
|
const q = pts[(i + 1) % pts.length];
|
|
a += p.x * q.y - q.x * p.y;
|
|
}
|
|
return Math.abs(a) / 2;
|
|
}
|
|
|
|
interface MeasureStart extends CommandState {
|
|
phase: "start";
|
|
}
|
|
interface MeasureDraw extends CommandState {
|
|
phase: "draw";
|
|
/** Bereits gesetzte Stützpunkte (≥ 1). */
|
|
points: Vec2[];
|
|
cursor: Vec2 | null;
|
|
}
|
|
type MeasureState = MeasureStart | MeasureDraw;
|
|
|
|
/** Messwerte aus gesetzten Punkten + aktuellem Cursor bilden. */
|
|
function readout(points: Vec2[], cursor: Vec2 | null): MeasurementReadout {
|
|
const chain = cursor ? [...points, cursor] : points;
|
|
const last = points[points.length - 1];
|
|
const segment = cursor && last ? segLen(last, cursor) : 0;
|
|
const angle = cursor && last ? ((segAngleDeg(last, cursor) % 360) + 360) % 360 : 0;
|
|
return {
|
|
segment,
|
|
total: pathLength(chain),
|
|
area: chain.length >= 3 ? polygonArea(chain) : null,
|
|
angle,
|
|
points: points.length,
|
|
};
|
|
}
|
|
|
|
/** Vorschau: gesetzter Zug + Live-Segment zum Cursor + HUD + Messwerte. */
|
|
function measureDraft(points: Vec2[], cursor: Vec2 | null): ToolDraft {
|
|
const chain = cursor ? [...points, cursor] : points;
|
|
const r = readout(points, cursor);
|
|
const hudText =
|
|
r.area != null
|
|
? `${r.segment.toFixed(3)} m · Σ ${r.total.toFixed(3)} m · ${r.area.toFixed(2)} m²`
|
|
: `${r.segment.toFixed(3)} m · ${r.angle.toFixed(1)}°`;
|
|
return {
|
|
preview: [{ kind: "poly", pts: chain, closed: chain.length >= 3 }],
|
|
vertices: points,
|
|
hud: cursor ? { at: cursor, text: hudText } : undefined,
|
|
measure: r,
|
|
};
|
|
}
|
|
|
|
const idle = (): [CommandState, CommandResult] => [
|
|
{ phase: "start", lastPoint: null } as MeasureStart,
|
|
{ draft: null, done: true },
|
|
];
|
|
|
|
export const measureCommand: Command = {
|
|
name: "measure",
|
|
labelKey: "cmd.measure.label",
|
|
prompt: (s) => ((s as MeasureState).phase === "draw" ? "cmd.measure.next" : "cmd.measure.start"),
|
|
accepts: () => ["point"],
|
|
options: () => [],
|
|
init: (): MeasureStart => ({ phase: "start", lastPoint: null }),
|
|
|
|
onInput: (state, input): [CommandState, CommandResult] => {
|
|
const s = state as MeasureState;
|
|
if (input.kind !== "point") {
|
|
const draft =
|
|
s.phase === "draw" ? measureDraft(s.points, s.cursor) : null;
|
|
return [s, { draft }];
|
|
}
|
|
// Klick hängt einen Stützpunkt an den Pfad (bzw. beginnt einen neuen).
|
|
const points = s.phase === "draw" ? [...s.points, input.point] : [input.point];
|
|
const ns: MeasureDraw = {
|
|
phase: "draw",
|
|
points,
|
|
cursor: input.point,
|
|
lastPoint: input.point,
|
|
};
|
|
return [ns, { draft: measureDraft(points, input.point) }];
|
|
},
|
|
|
|
onMove: (state, point): [CommandState, CommandResult] => {
|
|
const s = state as MeasureState;
|
|
if (s.phase !== "draw") return [s, { draft: null }];
|
|
const ns: MeasureDraw = { ...s, cursor: point };
|
|
return [ns, { draft: measureDraft(s.points, point) }];
|
|
},
|
|
|
|
// Rechtsklick/Enter: laufenden Pfad beenden und einen NEUEN armieren (mehrere
|
|
// Messungen nacheinander). Auf leerem Pfad = Werkzeug beenden.
|
|
onConfirm: (state): [CommandState, CommandResult] => {
|
|
const s = state as MeasureState;
|
|
if (s.phase === "draw" && s.points.length > 0) {
|
|
return [{ phase: "start", lastPoint: null } as MeasureStart, { draft: null }];
|
|
}
|
|
return idle();
|
|
},
|
|
// Esc: Werkzeug ganz beenden.
|
|
onCancel: (): [CommandState, CommandResult] => idle(),
|
|
};
|