3d2d4d6321
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
// Line — erster Befehl der Command-Engine (portiert lineTool aus
|
|
// src/tools/tools.ts). Zwei Punkte → Drawing2D{shape:"line"}.
|
|
//
|
|
// Schritte:
|
|
// 1) „Start of line:" → Punkt (Klick ODER getippt: 0,0 / r3,0 / 3<45)
|
|
// 2) „End of line:" → Punkt → commit Drawing2D line → done
|
|
//
|
|
// Akzeptiert in JEDEM Punkt-Schritt sowohl Maus-Picks (gesnappt) als auch
|
|
// getippte Koordinaten — das ist die Kern-Vereinheitlichung gegenüber dem alten
|
|
// Tool. Live-Vorschau + HUD (Länge/Winkel) wie die bestehenden Tools.
|
|
|
|
import type { Drawing2D } from "../../model/types";
|
|
import { uniqueId } from "../../tools/types";
|
|
import type {
|
|
Command,
|
|
CommandContext,
|
|
CommandField,
|
|
CommandResult,
|
|
CommandState,
|
|
DraftShape,
|
|
ToolDraft,
|
|
Vec2,
|
|
} from "../types";
|
|
|
|
const EPS = 1e-6;
|
|
const DEG = Math.PI / 180;
|
|
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;
|
|
|
|
/** Tab-Felder des End-Schritts: erst Länge, dann Winkel (§2.7). */
|
|
const LINE_FIELDS: CommandField[] = [
|
|
{ id: "length", labelKey: "cmd.field.length" },
|
|
{ id: "angle", labelKey: "cmd.field.angle" },
|
|
];
|
|
|
|
/**
|
|
* Endpunkt aus gelockten Feldern (length/angle) + Cursor: ungelockte Werte
|
|
* folgen dem Cursor relativ zum Startpunkt `a`.
|
|
*/
|
|
function lineEndFromFields(a: Vec2, locks: Record<string, number>, cursor: Vec2 | null): Vec2 {
|
|
const ref = cursor ?? a;
|
|
const length = "length" in locks ? locks.length : segLen(a, ref);
|
|
const angleDeg = "angle" in locks ? locks.angle : segAngleDeg(a, ref);
|
|
const ang = angleDeg * DEG;
|
|
return { x: a.x + Math.cos(ang) * length, y: a.y + Math.sin(ang) * length };
|
|
}
|
|
|
|
interface LineIdle extends CommandState {
|
|
phase: "start";
|
|
}
|
|
interface LineDrawing extends CommandState {
|
|
phase: "end";
|
|
a: Vec2;
|
|
cursor: Vec2 | null;
|
|
}
|
|
type LineState = LineIdle | LineDrawing;
|
|
|
|
/** Vorschau (Rubber-Band) + HUD (Länge·Winkel) für das lebende Segment. */
|
|
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)}°`,
|
|
};
|
|
}
|
|
return draft;
|
|
}
|
|
|
|
/** Hängt eine 2D-Linie ans Projekt (immutabel). Farbe/Strich erbt die Kategorie. */
|
|
function appendLine(p: import("../types").Project, a: Vec2, b: Vec2, ctx: CommandContext) {
|
|
const d: Drawing2D = {
|
|
id: uniqueId("dr2d"),
|
|
type: "drawing2d",
|
|
levelId: ctx.level.id,
|
|
categoryCode: ctx.defaultCategoryCode,
|
|
geom: { shape: "line", a, b },
|
|
};
|
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
|
}
|
|
|
|
export const lineCommand: Command = {
|
|
name: "line",
|
|
labelKey: "cmd.line.label",
|
|
prompt: (s) => ((s as LineState).phase === "end" ? "cmd.line.end" : "cmd.line.start"),
|
|
accepts: () => ["point", "number"],
|
|
options: () => [],
|
|
init: (): LineIdle => ({ phase: "start", lastPoint: null }),
|
|
|
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
|
const s = state as LineState;
|
|
// Beide Punkt-Schritte erwarten einen aufgelösten Punkt; die Engine reicht
|
|
// getippte Koordinaten bereits als {kind:"point"} herein.
|
|
if (input.kind !== "point") return [s, { draft: s.phase === "end" ? lineDraft(s.a, s.cursor) : null }];
|
|
const pt = input.point;
|
|
if (s.phase !== "end") {
|
|
const next: LineDrawing = { phase: "end", a: pt, cursor: pt, lastPoint: pt };
|
|
return [next, { draft: lineDraft(pt, pt) }];
|
|
}
|
|
// Zweiter Punkt: Null-Strecke verwerfen, sonst committen.
|
|
if (segLen(s.a, pt) < EPS) {
|
|
return [{ phase: "start", lastPoint: null }, { draft: null, done: true }];
|
|
}
|
|
const a = s.a;
|
|
return [
|
|
{ phase: "start", lastPoint: null },
|
|
{ draft: null, done: true, commit: (p) => appendLine(p, a, pt, ctx) },
|
|
];
|
|
},
|
|
|
|
onMove: (state, point): [CommandState, CommandResult] => {
|
|
const s = state as LineState;
|
|
if (s.phase !== "end") return [s, { draft: null }];
|
|
const next: LineDrawing = { ...s, cursor: point };
|
|
return [next, { draft: lineDraft(s.a, point) }];
|
|
},
|
|
|
|
// Line braucht genau zwei Punkte; Enter/Rechtsklick im offenen Entwurf bricht ab.
|
|
onConfirm: (): [CommandState, CommandResult] => [
|
|
{ phase: "start", lastPoint: null },
|
|
{ draft: null, done: true },
|
|
],
|
|
onCancel: (): [CommandState, CommandResult] => [
|
|
{ phase: "start", lastPoint: null },
|
|
{ draft: null, done: true },
|
|
],
|
|
|
|
// Tab-Feld-Zyklus nur im End-Schritt: Länge + Winkel relativ zum Startpunkt.
|
|
fields: (state) => ((state as LineState).phase === "end" ? LINE_FIELDS : []),
|
|
pointFromFields: (state, locks, cursor) => {
|
|
const s = state as LineState;
|
|
if (s.phase !== "end") return null;
|
|
return lineEndFromFields(s.a, locks, cursor);
|
|
},
|
|
fieldValues: (state, _locks, cursor): Record<string, number> => {
|
|
const s = state as LineState;
|
|
if (s.phase !== "end" || !cursor) return {};
|
|
return {
|
|
length: segLen(s.a, cursor),
|
|
angle: ((segAngleDeg(s.a, cursor) % 360) + 360) % 360,
|
|
};
|
|
},
|
|
};
|