Werkzeuge: Bogen-Werkzeug (arcCommand, 3-Klick CCW) + Toolbar/Icon/i18n verdrahtet
This commit is contained in:
@@ -213,6 +213,7 @@ const TOOL_COMMAND: Partial<Record<ToolId, string>> = {
|
|||||||
polyline: "polyline",
|
polyline: "polyline",
|
||||||
rect: "rect",
|
rect: "rect",
|
||||||
circle: "circle",
|
circle: "circle",
|
||||||
|
arc: "arc",
|
||||||
};
|
};
|
||||||
/** Befehlsname für ein gekoppeltes Werkzeug (oder null, wenn keiner). */
|
/** Befehlsname für ein gekoppeltes Werkzeug (oder null, wenn keiner). */
|
||||||
function coupledCommandFor(id: ToolId): string | null {
|
function coupledCommandFor(id: ToolId): string | null {
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Arc — Mittelpunkt + Startpunkt + Endpunkt. Schritte:
|
||||||
|
// 1) „Mittelpunkt:" → Punkt
|
||||||
|
// 2) „Startpunkt:" → Punkt (setzt Radius r = |P1−Center| UND Startwinkel a0)
|
||||||
|
// 3) „Endpunkt:" → Punkt (setzt Endwinkel a1; Radius bleibt) → commit
|
||||||
|
//
|
||||||
|
// Der Bogen läuft CCW von a0 nach a1 (wie DXF-ARC und das `{shape:"arc"}`-
|
||||||
|
// Rendering in PlanView). Der Endpunkt wird nur für seinen WINKEL genutzt (auf
|
||||||
|
// den Kreis mit Radius r projiziert). In generatePlan/PlanView als glatter
|
||||||
|
// SVG-Bogen (`drawingArc`) gerendert.
|
||||||
|
|
||||||
|
import type { Drawing2D } from "../../model/types";
|
||||||
|
import { uniqueId } from "../../tools/types";
|
||||||
|
import type {
|
||||||
|
Command,
|
||||||
|
CommandContext,
|
||||||
|
CommandResult,
|
||||||
|
CommandState,
|
||||||
|
DraftShape,
|
||||||
|
Project,
|
||||||
|
ToolDraft,
|
||||||
|
Vec2,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const EPS = 1e-6;
|
||||||
|
|
||||||
|
interface ArcCenter extends CommandState {
|
||||||
|
phase: "center";
|
||||||
|
}
|
||||||
|
interface ArcStart extends CommandState {
|
||||||
|
phase: "start";
|
||||||
|
center: Vec2;
|
||||||
|
cursor: Vec2 | null;
|
||||||
|
}
|
||||||
|
interface ArcEnd extends CommandState {
|
||||||
|
phase: "end";
|
||||||
|
center: Vec2;
|
||||||
|
r: number;
|
||||||
|
a0: number;
|
||||||
|
cursor: Vec2 | null;
|
||||||
|
}
|
||||||
|
type ArcState = ArcCenter | ArcStart | ArcEnd;
|
||||||
|
|
||||||
|
const dist = (a: Vec2, b: Vec2): number => Math.hypot(b.x - a.x, b.y - a.y);
|
||||||
|
const angle = (c: Vec2, p: Vec2): number => Math.atan2(p.y - c.y, p.x - c.x);
|
||||||
|
|
||||||
|
/** CCW-Spanne von a0 nach a1, normiert auf (0, 2π]. */
|
||||||
|
function sweepOf(a0: number, a1: number): number {
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
const s = ((a1 - a0) % TAU + TAU) % TAU;
|
||||||
|
return s < EPS ? TAU : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Punkte eines Bogens (CCW a0→a1) für die Vorschau. */
|
||||||
|
function arcPts(center: Vec2, r: number, a0: number, a1: number): Vec2[] {
|
||||||
|
const sweep = sweepOf(a0, a1);
|
||||||
|
const N = Math.max(2, Math.ceil(sweep / (Math.PI / 32)));
|
||||||
|
const pts: Vec2[] = [];
|
||||||
|
for (let i = 0; i <= N; i++) {
|
||||||
|
const t = a0 + (sweep * i) / N;
|
||||||
|
pts.push({ x: center.x + r * Math.cos(t), y: center.y + r * Math.sin(t) });
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Punkte einer vollen Kreis-Approximation (Radius-Vorschau im „start"-Schritt). */
|
||||||
|
function circlePts(center: Vec2, r: number): Vec2[] {
|
||||||
|
const N = 48;
|
||||||
|
const pts: Vec2[] = [];
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
const t = (i / N) * Math.PI * 2;
|
||||||
|
pts.push({ x: center.x + r * Math.cos(t), y: center.y + r * Math.sin(t) });
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vorschau (offener Bogen) + HUD (Spanne in Grad). */
|
||||||
|
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)}°` };
|
||||||
|
}
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendArc(p: Project, center: Vec2, r: number, a0: number, a1: number, ctx: CommandContext): Project {
|
||||||
|
if (r < EPS) return p;
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: uniqueId("dr2d"),
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: ctx.level.id,
|
||||||
|
categoryCode: ctx.defaultCategoryCode,
|
||||||
|
geom: { shape: "arc", center, r, a0, a1 },
|
||||||
|
};
|
||||||
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
|
{ phase: "center", lastPoint: null },
|
||||||
|
{ draft: null, done: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const arcCommand: Command = {
|
||||||
|
name: "arc",
|
||||||
|
labelKey: "cmd.arc.label",
|
||||||
|
prompt: (s) => {
|
||||||
|
const phase = (s as ArcState).phase;
|
||||||
|
return phase === "start" ? "cmd.arc.start" : phase === "end" ? "cmd.arc.end" : "cmd.arc.center";
|
||||||
|
},
|
||||||
|
accepts: () => ["point"],
|
||||||
|
options: () => [],
|
||||||
|
init: (): ArcCenter => ({ phase: "center", lastPoint: null }),
|
||||||
|
|
||||||
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||||
|
const s = state as ArcState;
|
||||||
|
if (input.kind !== "point") return [s, { draft: null }];
|
||||||
|
const pt = input.point;
|
||||||
|
if (s.phase === "center") {
|
||||||
|
const ns: ArcStart = { phase: "start", center: pt, cursor: pt, lastPoint: pt };
|
||||||
|
return [ns, { draft: { preview: [], vertices: [pt] } }];
|
||||||
|
}
|
||||||
|
if (s.phase === "start") {
|
||||||
|
const r = dist(s.center, pt);
|
||||||
|
if (r < EPS) return [s, { draft: null }];
|
||||||
|
const a0 = angle(s.center, pt);
|
||||||
|
const ns: ArcEnd = { phase: "end", center: s.center, r, a0, cursor: pt, lastPoint: pt };
|
||||||
|
return [ns, { draft: arcDraft(s.center, r, a0, a0, pt) }];
|
||||||
|
}
|
||||||
|
// Endpunkt: Endwinkel setzen, Bogen committen.
|
||||||
|
const a1 = angle(s.center, pt);
|
||||||
|
const { center, r, a0 } = s;
|
||||||
|
return [
|
||||||
|
{ phase: "center", lastPoint: null },
|
||||||
|
{ draft: null, done: true, commit: (p) => appendArc(p, center, r, a0, a1, ctx) },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
onMove: (state, point): [CommandState, CommandResult] => {
|
||||||
|
const s = state as ArcState;
|
||||||
|
if (s.phase === "start") {
|
||||||
|
const r = dist(s.center, point);
|
||||||
|
const ns: ArcStart = { ...s, cursor: point };
|
||||||
|
return [
|
||||||
|
ns,
|
||||||
|
{
|
||||||
|
draft: {
|
||||||
|
preview: [{ kind: "poly", pts: circlePts(s.center, r), closed: true }],
|
||||||
|
vertices: [s.center],
|
||||||
|
hud: { at: point, text: `r ${r.toFixed(2)} m` },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (s.phase === "end") {
|
||||||
|
const a1 = angle(s.center, point);
|
||||||
|
const ns: ArcEnd = { ...s, cursor: point };
|
||||||
|
return [ns, { draft: arcDraft(s.center, s.r, s.a0, a1, point) }];
|
||||||
|
}
|
||||||
|
return [s, { draft: null }];
|
||||||
|
},
|
||||||
|
|
||||||
|
onConfirm: (): [CommandState, CommandResult] => idle(),
|
||||||
|
onCancel: (): [CommandState, CommandResult] => idle(),
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ import { stairCommand } from "./cmds/stair";
|
|||||||
import { roomCommand } from "./cmds/room";
|
import { roomCommand } from "./cmds/room";
|
||||||
import { rectCommand } from "./cmds/rect";
|
import { rectCommand } from "./cmds/rect";
|
||||||
import { circleCommand } from "./cmds/circle";
|
import { circleCommand } from "./cmds/circle";
|
||||||
|
import { arcCommand } from "./cmds/arc";
|
||||||
import { moveCommand } from "./cmds/move";
|
import { moveCommand } from "./cmds/move";
|
||||||
import { mirrorCommand } from "./cmds/mirror";
|
import { mirrorCommand } from "./cmds/mirror";
|
||||||
import { joinCommand } from "./cmds/join";
|
import { joinCommand } from "./cmds/join";
|
||||||
@@ -35,6 +36,7 @@ export const COMMANDS: Record<string, Command> = {
|
|||||||
polyline: polylineCommand,
|
polyline: polylineCommand,
|
||||||
rect: rectCommand,
|
rect: rectCommand,
|
||||||
circle: circleCommand,
|
circle: circleCommand,
|
||||||
|
arc: arcCommand,
|
||||||
move: moveCommand,
|
move: moveCommand,
|
||||||
mirror: mirrorCommand,
|
mirror: mirrorCommand,
|
||||||
join: joinCommand,
|
join: joinCommand,
|
||||||
@@ -67,6 +69,8 @@ export const ALIASES: Record<string, string> = {
|
|||||||
pl: "polyline",
|
pl: "polyline",
|
||||||
rec: "rect",
|
rec: "rect",
|
||||||
c: "circle",
|
c: "circle",
|
||||||
|
a: "arc",
|
||||||
|
bogen: "arc",
|
||||||
m: "move",
|
m: "move",
|
||||||
s: "mirror",
|
s: "mirror",
|
||||||
spiegeln: "mirror",
|
spiegeln: "mirror",
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ export const de = {
|
|||||||
"tool.rect": "Rechteck",
|
"tool.rect": "Rechteck",
|
||||||
"tool.circle": "Kreis",
|
"tool.circle": "Kreis",
|
||||||
"tool.circle.hint": "Kreis: Mittelpunkt setzen, dann Radius",
|
"tool.circle.hint": "Kreis: Mittelpunkt setzen, dann Radius",
|
||||||
|
"tool.arc": "Bogen",
|
||||||
|
"tool.arc.hint": "Bogen: Mittelpunkt, Startpunkt, dann Endpunkt (CCW)",
|
||||||
"tool.select.hint": "Auswahl — Elemente klicken/aufziehen",
|
"tool.select.hint": "Auswahl — Elemente klicken/aufziehen",
|
||||||
"tool.window.hint": "Fenster: Wand wählen, dann Position setzen",
|
"tool.window.hint": "Fenster: Wand wählen, dann Position setzen",
|
||||||
"tool.door.hint": "Tür: Wand wählen, dann Position setzen",
|
"tool.door.hint": "Tür: Wand wählen, dann Position setzen",
|
||||||
@@ -709,6 +711,10 @@ export const de = {
|
|||||||
"cmd.circle.label": "Kreis",
|
"cmd.circle.label": "Kreis",
|
||||||
"cmd.circle.center": "Mittelpunkt:",
|
"cmd.circle.center": "Mittelpunkt:",
|
||||||
"cmd.circle.radius": "Radius:",
|
"cmd.circle.radius": "Radius:",
|
||||||
|
"cmd.arc.label": "Bogen",
|
||||||
|
"cmd.arc.center": "Mittelpunkt:",
|
||||||
|
"cmd.arc.start": "Startpunkt (Radius + Startwinkel):",
|
||||||
|
"cmd.arc.end": "Endpunkt (Endwinkel):",
|
||||||
// Editierbefehle (Tier 1): Move/Copy/Offset operieren auf der Auswahl.
|
// Editierbefehle (Tier 1): Move/Copy/Offset operieren auf der Auswahl.
|
||||||
"cmd.move.label": "Verschieben",
|
"cmd.move.label": "Verschieben",
|
||||||
"cmd.move.base": "Basispunkt:",
|
"cmd.move.base": "Basispunkt:",
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"tool.rect": "Rectangle",
|
"tool.rect": "Rectangle",
|
||||||
"tool.circle": "Circle",
|
"tool.circle": "Circle",
|
||||||
"tool.circle.hint": "Circle: set center, then radius",
|
"tool.circle.hint": "Circle: set center, then radius",
|
||||||
|
"tool.arc": "Arc",
|
||||||
|
"tool.arc.hint": "Arc: center, start point, then end point (CCW)",
|
||||||
"tool.select.hint": "Select — click/drag elements",
|
"tool.select.hint": "Select — click/drag elements",
|
||||||
"tool.window.hint": "Window: pick a wall, then set the position",
|
"tool.window.hint": "Window: pick a wall, then set the position",
|
||||||
"tool.door.hint": "Door: pick a wall, then set the position",
|
"tool.door.hint": "Door: pick a wall, then set the position",
|
||||||
@@ -699,6 +701,10 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"cmd.circle.label": "Circle",
|
"cmd.circle.label": "Circle",
|
||||||
"cmd.circle.center": "Center:",
|
"cmd.circle.center": "Center:",
|
||||||
"cmd.circle.radius": "Radius:",
|
"cmd.circle.radius": "Radius:",
|
||||||
|
"cmd.arc.label": "Arc",
|
||||||
|
"cmd.arc.center": "Center:",
|
||||||
|
"cmd.arc.start": "Start point (radius + start angle):",
|
||||||
|
"cmd.arc.end": "End point (end angle):",
|
||||||
"cmd.move.label": "Move",
|
"cmd.move.label": "Move",
|
||||||
"cmd.move.base": "Base point:",
|
"cmd.move.base": "Base point:",
|
||||||
"cmd.move.target": "Target point:",
|
"cmd.move.target": "Target point:",
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ function ToolIcon({ id }: { id: ToolId }) {
|
|||||||
<circle cx="8" cy="8" r="5.5" />
|
<circle cx="8" cy="8" r="5.5" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case "arc":
|
||||||
|
// Offener Halbbogen.
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<path d="M 2.5 12 A 5.5 5.5 0 0 1 13.5 12" fill="none" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,6 +541,19 @@ const circleTool: Tool = {
|
|||||||
onCancel: (s) => [s, { draft: null, done: true }],
|
onCancel: (s) => [s, { draft: null, done: true }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bogen: Platzhalter-Werkzeug, gekoppelt an den `arc`-Befehl (3 Klicks:
|
||||||
|
// Mitte → Start/Radius → Ende). Wie Kreis nicht floorOnly.
|
||||||
|
const arcTool: Tool = {
|
||||||
|
id: "arc",
|
||||||
|
labelKey: "tool.arc",
|
||||||
|
hintKey: () => "tool.arc.hint",
|
||||||
|
init: () => ({ phase: "idle" }),
|
||||||
|
onClick: (s) => [s, { draft: null }],
|
||||||
|
onMove: (s) => [s, { draft: null }],
|
||||||
|
onCommitGesture: (s) => [s, { draft: null, done: true }],
|
||||||
|
onCancel: (s) => [s, { draft: null, done: true }],
|
||||||
|
};
|
||||||
|
|
||||||
// ── Registry ─────────────────────────────────────────────────────────────────
|
// ── Registry ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const TOOLS: Record<ToolId, Tool> = {
|
const TOOLS: Record<ToolId, Tool> = {
|
||||||
@@ -555,6 +568,7 @@ const TOOLS: Record<ToolId, Tool> = {
|
|||||||
polyline: polylineTool,
|
polyline: polylineTool,
|
||||||
rect: rectTool,
|
rect: rectTool,
|
||||||
circle: circleTool,
|
circle: circleTool,
|
||||||
|
arc: arcTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Liefert das Werkzeug zur ID. */
|
/** Liefert das Werkzeug zur ID. */
|
||||||
@@ -575,4 +589,5 @@ export const TOOL_ORDER: ToolId[] = [
|
|||||||
"polyline",
|
"polyline",
|
||||||
"rect",
|
"rect",
|
||||||
"circle",
|
"circle",
|
||||||
|
"arc",
|
||||||
];
|
];
|
||||||
|
|||||||
+2
-1
@@ -19,7 +19,8 @@ export type ToolId =
|
|||||||
| "line"
|
| "line"
|
||||||
| "polyline"
|
| "polyline"
|
||||||
| "rect"
|
| "rect"
|
||||||
| "circle";
|
| "circle"
|
||||||
|
| "arc";
|
||||||
|
|
||||||
// ── Snapping ───────────────────────────────────────────────────────────────
|
// ── Snapping ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user