2D: Kreis/Bogen-Zeichenelemente vollständig anwählbar und editierbar
Seit der Umstellung auf echte SVG-Kreis-/Bogen-Primitive (drawingCircle/ drawingArc statt 64-Ecken-Polygon-Annäherung) wurde die Interaktionsschicht nie nachgezogen — betraf drei unabhängige Stellen mit derselben Ursache: - Einzelklick: pickDrawing() testete nur kind==="line", fiel sonst auf Text-/Bild-Bbox zurück. Neuer distToDrawingRing()-Zweig: Ring-Abstand in Bildschirm-Pixeln (zoom-unabhängig wie die Linien-Toleranz), bei gefüllten Kreisen zählt auch innerhalb als Treffer; Bögen zusätzlich auf ihre Winkel-Spanne begrenzt. grabbedBody() (Körper-Verschieben) profitiert automatisch mit, da es denselben Picker nutzt. - Marquee (Rahmenauswahl): marqueeHitDrawings() sammelte nur line/polygon — drawingCircle/drawingArc UND drawingText/drawingImage fehlten komplett (Text/Bild waren nie per Rahmen erfassbar, unabhängig von der Kreis-Frage). Kreis/Bogen werden für den Punkt-Test auf Ring-Stützpunkte abgetastet (Bogen nur innerhalb seiner Winkel-Spanne, nicht als Vollkreis), Text/Bild über ihre Bounding-Box. - Griffe: drawingVertices() hatte keinen Fall für circle/arc → leeres Array, selbst nach Fix von Klick+Marquee also unverschiebbar/nicht editierbar. Kreis bekommt einen Radius-Griff (Ost), Bogen Start-/End-Winkel-Griffe plus einen Radius-Griff auf halbem Bogen — moveGrip() entsprechend erweitert. 10 neue Tests (drawingVertices/moveGripOf für Kreis/Bogen, marqueeHitDrawings für alle vier zuvor fehlenden Primitive-Arten). tsc -b / vitest run (901/901) / npm run build grün. Der Einzelklick-Pfad selbst (PlanView.tsx- Closures) ist nicht automatisiert testbar — visuell in der Tauri-App prüfen.
This commit is contained in:
+35
-2
@@ -900,13 +900,39 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Abstand (Bildschirm-Pixel) eines Klicks vom Ring eines Kreis/Bogen-2D-
|
||||||
|
// Elements — bei gefüllten Kreisen zählt auch INNERHALB als Treffer (d=0).
|
||||||
|
// Bögen zusätzlich auf ihre Winkel-Spanne begrenzt (CCW, Modell-Radiant, s.
|
||||||
|
// toRenderScene.ts für dieselbe Normalisierung).
|
||||||
|
const distToDrawingRing = (
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
p: Extract<Primitive, { kind: "drawingCircle" | "drawingArc" }>,
|
||||||
|
): number => {
|
||||||
|
const m = rawModelAt(clientX, clientY);
|
||||||
|
const distFromCenter = Math.hypot(m.x - p.center.x, m.y - p.center.y);
|
||||||
|
if (p.kind === "drawingArc") {
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
const sweep = (((p.a1 - p.a0) % TAU) + TAU) % TAU || TAU;
|
||||||
|
const ang = Math.atan2(m.y - p.center.y, m.x - p.center.x);
|
||||||
|
const rel = (((ang - p.a0) % TAU) + TAU) % TAU;
|
||||||
|
// Kleiner Winkel-Toleranzsaum (in Bogenmass, radiusabhängig), damit die
|
||||||
|
// Enden eines kurzen Bogens nicht unerreichbar schmal werden.
|
||||||
|
const angTolRad = distFromCenter > 1e-6 ? LINE_PICK_PX / (currentPxPerMeter() * Math.max(distFromCenter, 0.05)) : Infinity;
|
||||||
|
if (rel > sweep + angTolRad && TAU - rel > angTolRad) return Infinity;
|
||||||
|
}
|
||||||
|
const filled = p.kind === "drawingCircle" && p.fill !== "none";
|
||||||
|
const dMeters = filled ? Math.max(0, distFromCenter - p.r) : Math.abs(distFromCenter - p.r);
|
||||||
|
return dMeters * currentPxPerMeter();
|
||||||
|
};
|
||||||
|
|
||||||
// Nächstgelegenes 2D-Zeichenelement (Drawing2D) zum Cursor per Linien-Nähe in
|
// Nächstgelegenes 2D-Zeichenelement (Drawing2D) zum Cursor per Linien-Nähe in
|
||||||
// Bildschirm-Pixeln (zoom-unabhängige Toleranz). Liefert die drawingId oder null.
|
// Bildschirm-Pixeln (zoom-unabhängige Toleranz). Liefert die drawingId oder null.
|
||||||
const pickDrawing = (clientX: number, clientY: number): string | null => {
|
const pickDrawing = (clientX: number, clientY: number): string | null => {
|
||||||
let bestId: string | null = null;
|
let bestId: string | null = null;
|
||||||
let bestDist = LINE_PICK_PX;
|
let bestDist = LINE_PICK_PX;
|
||||||
for (const p of plan.primitives) {
|
for (const p of plan.primitives) {
|
||||||
if (p.kind !== "line" || !p.drawingId) continue;
|
if (p.kind === "line" && p.drawingId) {
|
||||||
const a = vbToClient(toScreen(p.a).x, toScreen(p.a).y);
|
const a = vbToClient(toScreen(p.a).x, toScreen(p.a).y);
|
||||||
const b = vbToClient(toScreen(p.b).x, toScreen(p.b).y);
|
const b = vbToClient(toScreen(p.b).x, toScreen(p.b).y);
|
||||||
const d = pointSegDistPx({ x: clientX, y: clientY }, a, b);
|
const d = pointSegDistPx({ x: clientX, y: clientY }, a, b);
|
||||||
@@ -914,9 +940,16 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
bestDist = d;
|
bestDist = d;
|
||||||
bestId = p.drawingId;
|
bestId = p.drawingId;
|
||||||
}
|
}
|
||||||
|
} else if ((p.kind === "drawingCircle" || p.kind === "drawingArc") && p.drawingId) {
|
||||||
|
const d = distToDrawingRing(clientX, clientY, p);
|
||||||
|
if (d < bestDist) {
|
||||||
|
bestDist = d;
|
||||||
|
bestId = p.drawingId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bestId) return bestId;
|
if (bestId) return bestId;
|
||||||
// Kein Linien-Treffer: Text- dann Bild-Elemente prüfen (eigene Bbox-Logik).
|
// Kein Linien-/Kreis-/Bogen-Treffer: Text- dann Bild-Elemente prüfen (eigene Bbox-Logik).
|
||||||
return pickDrawingText(clientX, clientY) ?? pickDrawingImage(clientX, clientY);
|
return pickDrawingText(clientX, clientY) ?? pickDrawingImage(clientX, clientY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* `marqueeHitDrawings` — Rahmenauswahl von 2D-Zeichenelementen. Vorher fehlten
|
||||||
|
* drawingCircle/drawingArc/drawingText/drawingImage komplett (nur line/polygon
|
||||||
|
* abgedeckt) — Regressionsschutz für den Nachzieh-Fix (2D-Vervollständigung,
|
||||||
|
* Fund A).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { marqueeHitDrawings, type ModelRect } from "./geometry";
|
||||||
|
import type { Primitive } from "../generatePlan";
|
||||||
|
|
||||||
|
const encloseRect: ModelRect = { minX: -10, minY: -10, maxX: 10, maxY: 10 };
|
||||||
|
const missRect: ModelRect = { minX: 20, minY: 20, maxX: 30, maxY: 30 };
|
||||||
|
|
||||||
|
describe("marqueeHitDrawings — Kreis/Bogen/Text/Bild", () => {
|
||||||
|
it("Kreis vollständig im Rechteck → window-Treffer", () => {
|
||||||
|
const prims: Primitive[] = [
|
||||||
|
{
|
||||||
|
kind: "drawingCircle",
|
||||||
|
center: { x: 0, y: 0 },
|
||||||
|
r: 2,
|
||||||
|
fill: "none",
|
||||||
|
stroke: "#000",
|
||||||
|
weightMm: 0.25,
|
||||||
|
drawingId: "c1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(marqueeHitDrawings(prims, encloseRect, false)).toEqual(["c1"]);
|
||||||
|
expect(marqueeHitDrawings(prims, missRect, false)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bogen wird nur auf seiner Winkel-Spanne abgetastet (nicht als Vollkreis)", () => {
|
||||||
|
// Viertelbogen 0..PI/2 bei r=2 um den Ursprung: liegt komplett im 1. Quadranten.
|
||||||
|
const prims: Primitive[] = [
|
||||||
|
{
|
||||||
|
kind: "drawingArc",
|
||||||
|
center: { x: 0, y: 0 },
|
||||||
|
r: 2,
|
||||||
|
a0: 0,
|
||||||
|
a1: Math.PI / 2,
|
||||||
|
stroke: "#000",
|
||||||
|
weightMm: 0.25,
|
||||||
|
drawingId: "a1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// Rechteck deckt NUR den 1. Quadranten ab — ein Vollkreis-Sampling würde
|
||||||
|
// hier fälschlich als "nicht enclosed" durchfallen (Punkte im 3. Quadranten
|
||||||
|
// wären ausserhalb), der Viertelbogen muss aber als vollständig erfasst gelten.
|
||||||
|
const q1Rect: ModelRect = { minX: -0.1, minY: -0.1, maxX: 2.1, maxY: 2.1 };
|
||||||
|
expect(marqueeHitDrawings(prims, q1Rect, false)).toEqual(["a1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Text-Bbox wird erfasst (vorher: gar nicht per Marquee erfassbar)", () => {
|
||||||
|
const prims: Primitive[] = [
|
||||||
|
{
|
||||||
|
kind: "drawingText",
|
||||||
|
at: { x: 1, y: 1 },
|
||||||
|
text: "Hallo",
|
||||||
|
heightM: 0.3,
|
||||||
|
angle: 0,
|
||||||
|
color: "#000",
|
||||||
|
drawingId: "t1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(marqueeHitDrawings(prims, encloseRect, false)).toEqual(["t1"]);
|
||||||
|
expect(marqueeHitDrawings(prims, missRect, false)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bild-Bbox wird erfasst (vorher: gar nicht per Marquee erfassbar)", () => {
|
||||||
|
const prims: Primitive[] = [
|
||||||
|
{
|
||||||
|
kind: "drawingImage",
|
||||||
|
min: { x: -1, y: -1 },
|
||||||
|
max: { x: 1, y: 1 },
|
||||||
|
src: "data:image/png;base64,",
|
||||||
|
opacity: 1,
|
||||||
|
drawingId: "i1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(marqueeHitDrawings(prims, encloseRect, false)).toEqual(["i1"]);
|
||||||
|
expect(marqueeHitDrawings(prims, missRect, false)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("crossing-Modus: Kreis, der das Rechteck nur teilweise überlappt, zählt bereits; window-Modus nicht", () => {
|
||||||
|
// Zentrum ausserhalb, Radius reicht bei Winkel 0 klar ins Rechteck hinein
|
||||||
|
// (x=15+6=21, innerhalb [10,20]) — aber nicht der ganze Ring liegt drin.
|
||||||
|
const prims: Primitive[] = [
|
||||||
|
{
|
||||||
|
kind: "drawingCircle",
|
||||||
|
center: { x: 15, y: 0 },
|
||||||
|
r: 6,
|
||||||
|
fill: "none",
|
||||||
|
stroke: "#000",
|
||||||
|
weightMm: 0.25,
|
||||||
|
drawingId: "c2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rightRect: ModelRect = { minX: 10, minY: -10, maxX: 20, maxY: 10 };
|
||||||
|
expect(marqueeHitDrawings(prims, rightRect, true)).toEqual(["c2"]);
|
||||||
|
expect(marqueeHitDrawings(prims, rightRect, false)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -246,6 +246,34 @@ export function marqueeHit(
|
|||||||
* den Linien-/Polygon-Primitiven; crossing = irgendein Punkt/Kante im Rechteck,
|
* den Linien-/Polygon-Primitiven; crossing = irgendein Punkt/Kante im Rechteck,
|
||||||
* window = alle Punkte vollständig eingeschlossen.
|
* window = alle Punkte vollständig eingeschlossen.
|
||||||
*/
|
*/
|
||||||
|
/** Punkte auf einem Kreis-/Bogen-Ring (Modell-Meter), für Marquee-Zwecke abgetastet. */
|
||||||
|
function ringSamplePoints(center: Vec2, r: number, a0?: number, a1?: number): Vec2[] {
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
const full = a0 === undefined || a1 === undefined;
|
||||||
|
const start = full ? 0 : a0;
|
||||||
|
const sweep = full ? TAU : (((a1 - a0) % TAU) + TAU) % TAU || TAU;
|
||||||
|
const steps = full ? 32 : Math.max(4, Math.round((sweep / TAU) * 32));
|
||||||
|
const pts: Vec2[] = [];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const a = start + (sweep * i) / steps;
|
||||||
|
pts.push({ x: center.x + r * Math.cos(a), y: center.y + r * Math.sin(a) });
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounding-Box-Eckpunkte eines `drawingText`-Primitivs, analog zur Einzelklick-Bbox in PlanView.tsx. */
|
||||||
|
function drawingTextBboxPoints(p: Extract<Primitive, { kind: "drawingText" }>): Vec2[] {
|
||||||
|
const singleW = Math.max(1, p.text.length) * p.heightM * 0.6;
|
||||||
|
const w = p.wrapWidth && p.wrapWidth > 0 ? p.wrapWidth : singleW;
|
||||||
|
const lines = p.wrapWidth && p.wrapWidth > 0 ? Math.max(1, Math.ceil(singleW / p.wrapWidth)) : 1;
|
||||||
|
const pad = p.heightM * 0.6;
|
||||||
|
const minX = p.at.x - pad;
|
||||||
|
const maxX = p.at.x + w + pad;
|
||||||
|
const minY = p.at.y - lines * p.heightM * 1.25 - pad;
|
||||||
|
const maxY = p.at.y + p.heightM * 0.4 + pad;
|
||||||
|
return [{ x: minX, y: minY }, { x: maxX, y: minY }, { x: maxX, y: maxY }, { x: minX, y: maxY }];
|
||||||
|
}
|
||||||
|
|
||||||
export function marqueeHitDrawings(
|
export function marqueeHitDrawings(
|
||||||
prims: Primitive[],
|
prims: Primitive[],
|
||||||
rect: ModelRect,
|
rect: ModelRect,
|
||||||
@@ -260,6 +288,17 @@ export function marqueeHitDrawings(
|
|||||||
for (const p of prims) {
|
for (const p of prims) {
|
||||||
if (p.kind === "line" && p.drawingId) collect(p.drawingId, [p.a, p.b]);
|
if (p.kind === "line" && p.drawingId) collect(p.drawingId, [p.a, p.b]);
|
||||||
else if (p.kind === "polygon" && p.drawingId) collect(p.drawingId, p.pts);
|
else if (p.kind === "polygon" && p.drawingId) collect(p.drawingId, p.pts);
|
||||||
|
else if (p.kind === "drawingCircle" && p.drawingId) collect(p.drawingId, ringSamplePoints(p.center, p.r));
|
||||||
|
else if (p.kind === "drawingArc" && p.drawingId) collect(p.drawingId, ringSamplePoints(p.center, p.r, p.a0, p.a1));
|
||||||
|
else if (p.kind === "drawingText" && p.drawingId) collect(p.drawingId, drawingTextBboxPoints(p));
|
||||||
|
else if (p.kind === "drawingImage" && p.drawingId) {
|
||||||
|
collect(p.drawingId, [
|
||||||
|
p.min,
|
||||||
|
{ x: p.max.x, y: p.min.y },
|
||||||
|
p.max,
|
||||||
|
{ x: p.min.x, y: p.max.y },
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
for (const [id, pts] of byDrawing) {
|
for (const [id, pts] of byDrawing) {
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Editier-Griffe für Kreis/Bogen-2D-Elemente (`drawingVertices`/`moveGripOf`
|
||||||
|
* in `projectSlice.ts`). Vorher gab es dafür keine Griffe (leeres Array) —
|
||||||
|
* Regressionsschutz für den Nachzieh-Fix (2D-Vervollständigung, Fund A).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import { getState, setState } from "./appStore";
|
||||||
|
import { sampleProject } from "../model/sampleProject";
|
||||||
|
import { drawingVertices } from "./projectSlice";
|
||||||
|
import type { Drawing2D } from "../model/types";
|
||||||
|
|
||||||
|
const circleDrawing: Drawing2D = {
|
||||||
|
id: "d-circle",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "00",
|
||||||
|
geom: { shape: "circle", center: { x: 2, y: 3 }, r: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const arcDrawing: Drawing2D = {
|
||||||
|
id: "d-arc",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "00",
|
||||||
|
geom: { shape: "arc", center: { x: 0, y: 0 }, r: 2, a0: 0, a1: Math.PI / 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setState({
|
||||||
|
project: {
|
||||||
|
...sampleProject,
|
||||||
|
drawings2d: [circleDrawing, arcDrawing],
|
||||||
|
},
|
||||||
|
undoStack: [],
|
||||||
|
redoStack: [],
|
||||||
|
canUndo: false,
|
||||||
|
canRedo: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("drawingVertices — Kreis/Bogen", () => {
|
||||||
|
it("Kreis liefert einen Radius-Griff auf der Ost-Seite", () => {
|
||||||
|
const v = drawingVertices(circleDrawing);
|
||||||
|
expect(v).toEqual([{ x: 3, y: 3 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bogen liefert Start-/End-/Mittel-Winkel-Griffe", () => {
|
||||||
|
const v = drawingVertices(arcDrawing);
|
||||||
|
expect(v.length).toBe(3);
|
||||||
|
expect(v[0]).toEqual({ x: 2, y: 0 }); // a0 = 0
|
||||||
|
expect(v[1].x).toBeCloseTo(0, 9); // a1 = PI/2
|
||||||
|
expect(v[1].y).toBeCloseTo(2, 9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("moveGripOf — Kreis/Bogen", () => {
|
||||||
|
it("Kreis-Griff ziehen ändert nur den Radius, nicht das Zentrum", () => {
|
||||||
|
getState().moveGripOf("d-circle", null, 0, { x: 5, y: 3 }); // 3m vom Zentrum
|
||||||
|
const g = getState().project.drawings2d.find((d) => d.id === "d-circle")!.geom;
|
||||||
|
expect(g).toMatchObject({ shape: "circle", center: { x: 2, y: 3 }, r: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bogen-Start-Griff (Index 0) ändert a0, nicht r/a1", () => {
|
||||||
|
getState().moveGripOf("d-arc", null, 0, { x: 0, y: 2 }); // 90° = PI/2
|
||||||
|
const g = getState().project.drawings2d.find((d) => d.id === "d-arc")!.geom;
|
||||||
|
expect(g.shape).toBe("arc");
|
||||||
|
if (g.shape === "arc") {
|
||||||
|
expect(g.a0).toBeCloseTo(Math.PI / 2, 9);
|
||||||
|
expect(g.a1).toBeCloseTo(Math.PI / 2, 9); // unverändert
|
||||||
|
expect(g.r).toBeCloseTo(2, 9); // unverändert
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bogen-Radius-Griff (Index 2) ändert nur r, nicht a0/a1", () => {
|
||||||
|
getState().moveGripOf("d-arc", null, 2, { x: 4, y: 0 }); // gleiche Richtung, doppelter Radius
|
||||||
|
const g = getState().project.drawings2d.find((d) => d.id === "d-arc")!.geom;
|
||||||
|
expect(g.shape).toBe("arc");
|
||||||
|
if (g.shape === "arc") {
|
||||||
|
expect(g.a0).toBeCloseTo(0, 9);
|
||||||
|
expect(g.a1).toBeCloseTo(Math.PI / 2, 9);
|
||||||
|
expect(g.r).toBeCloseTo(4, 9);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1438,8 +1438,10 @@ function resizeGeom(
|
|||||||
// ── Editier-Griffe (verschoben aus App.tsx, 1:1) ─────────────────────────────
|
// ── Editier-Griffe (verschoben aus App.tsx, 1:1) ─────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editier-Griffe (Eckpunkte) eines 2D-Zeichenelements in Modell-Metern. Phase 4
|
* Editier-Griffe (Eckpunkte) eines 2D-Zeichenelements in Modell-Metern.
|
||||||
* unterstützt line/polyline/rect; circle/arc/text bekommen (noch) keine Griffe.
|
* line/polyline/rect: Eckpunkte. circle: EIN Radius-Griff (Ost, Index 0).
|
||||||
|
* arc: Start-/End-Winkel-Griff (Index 0/1, ändern a0/a1) + ein Radius-Griff
|
||||||
|
* auf halbem Bogen (Index 2, ändert r) — siehe {@link moveGrip}.
|
||||||
*/
|
*/
|
||||||
export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
|
export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
|
||||||
const g = d.geom;
|
const g = d.geom;
|
||||||
@@ -1453,6 +1455,17 @@ export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
|
|||||||
{ x: g.min.x, y: g.max.y },
|
{ x: g.min.x, y: g.max.y },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (g.shape === "circle") {
|
||||||
|
return [{ x: g.center.x + g.r, y: g.center.y }];
|
||||||
|
}
|
||||||
|
if (g.shape === "arc") {
|
||||||
|
const mid = (g.a0 + g.a1) / 2;
|
||||||
|
return [
|
||||||
|
{ x: g.center.x + g.r * Math.cos(g.a0), y: g.center.y + g.r * Math.sin(g.a0) },
|
||||||
|
{ x: g.center.x + g.r * Math.cos(g.a1), y: g.center.y + g.r * Math.sin(g.a1) },
|
||||||
|
{ x: g.center.x + g.r * Math.cos(mid), y: g.center.y + g.r * Math.sin(mid) },
|
||||||
|
];
|
||||||
|
}
|
||||||
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
|
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
|
||||||
if (g.shape === "text") return [g.at];
|
if (g.shape === "text") return [g.at];
|
||||||
return [];
|
return [];
|
||||||
@@ -1528,6 +1541,21 @@ function moveGrip(
|
|||||||
}
|
}
|
||||||
// Text: der einzige Griff (Index 0) sitzt am Ankerpunkt → verschiebt ihn.
|
// Text: der einzige Griff (Index 0) sitzt am Ankerpunkt → verschiebt ihn.
|
||||||
if (g.shape === "text") return { ...d, geom: { ...g, at: pt } };
|
if (g.shape === "text") return { ...d, geom: { ...g, at: pt } };
|
||||||
|
// Kreis: der einzige Griff (Index 0) ändert nur den Radius.
|
||||||
|
if (g.shape === "circle") {
|
||||||
|
const r = Math.max(1e-3, Math.hypot(pt.x - g.center.x, pt.y - g.center.y));
|
||||||
|
return { ...d, geom: { ...g, r } };
|
||||||
|
}
|
||||||
|
// Bogen: Griff 0/1 (Start-/End-Winkel, s. drawingVertices) ändert a0/a1;
|
||||||
|
// Griff 2 (Bogenmitte) ändert nur den Radius (Winkel bleiben fest).
|
||||||
|
if (g.shape === "arc") {
|
||||||
|
if (index === 2) {
|
||||||
|
const r = Math.max(1e-3, Math.hypot(pt.x - g.center.x, pt.y - g.center.y));
|
||||||
|
return { ...d, geom: { ...g, r } };
|
||||||
|
}
|
||||||
|
const ang = Math.atan2(pt.y - g.center.y, pt.x - g.center.x);
|
||||||
|
return { ...d, geom: index === 0 ? { ...g, a0: ang } : { ...g, a1: ang } };
|
||||||
|
}
|
||||||
return d;
|
return d;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user