2D: Ellipse, Spline und Bezier als neue Zeichenwerkzeuge
Ellipse (zwei Bounding-Box-Ecken, achsparallel), Spline (Wegpunkte, geglättet via Catmull-Rom, wie Polylinie mit rundem statt geradem Verlauf) und Bezier (klassische manuelle Kontrollpunkt-Kette: Anker/Griff/Griff je Segment). Neue Drawing2DGeom-Formen + Mathe-Helfer (tools/curves.ts). Rendering tessellliert zu Linien-/Polygon-Primitiven (wie Polylinie/Rechteck) statt eigenem SVG-Primitiv — dadurch funktionieren DXF-/Print-SVG-Export und Hit-Testing ohne Änderung mit. Griff-Editieren (2D-PlanView + 3D-Viewport), Verschieben/Skalieren, Snapping (Ellipse-Mittelpunkt, Bezier/Spline-Konturen) und Georef-Rebase entsprechend erweitert.
This commit is contained in:
@@ -1407,6 +1407,28 @@ function resizeGeom(
|
||||
};
|
||||
return { ...g, center, r };
|
||||
}
|
||||
case "ellipse": {
|
||||
// bbox: rotation=0 angenommen (das Werkzeug erzeugt nur achsparallele
|
||||
// Ellipsen); rx/ry unabhängig skaliert, Mittelpunkt um den Anker verschoben.
|
||||
const box = {
|
||||
minX: g.center.x - g.rx,
|
||||
minY: g.center.y - g.ry,
|
||||
maxX: g.center.x + g.rx,
|
||||
maxY: g.center.y + g.ry,
|
||||
};
|
||||
const { ax, ay, sx, sy } = scaleAround(box, w, h, anchor);
|
||||
const center: Vec2 = {
|
||||
x: ax + (g.center.x - ax) * sx,
|
||||
y: ay + (g.center.y - ay) * sy,
|
||||
};
|
||||
return { ...g, center, rx: Math.max(1e-3, g.rx * sx), ry: Math.max(1e-3, g.ry * sy) };
|
||||
}
|
||||
case "bezier":
|
||||
case "spline": {
|
||||
const { ax, ay, sx, sy } = scaleAround(bbox2(g.pts), w, h, anchor);
|
||||
const f = (p: Vec2): Vec2 => ({ x: ax + (p.x - ax) * sx, y: ay + (p.y - ay) * sy });
|
||||
return { ...g, pts: g.pts.map(f) };
|
||||
}
|
||||
case "text": {
|
||||
// Text: nur die Einfügeposition um den Anker schieben (Höhe bleibt).
|
||||
const box = { minX: g.at.x, minY: g.at.y, maxX: g.at.x, maxY: g.at.y };
|
||||
@@ -1461,6 +1483,17 @@ export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
|
||||
{ x: g.center.x + g.r * Math.cos(mid), y: g.center.y + g.r * Math.sin(mid) },
|
||||
];
|
||||
}
|
||||
// Ellipse: zwei Griffe (Index 0 = rx-Achse, Index 1 = ry-Achse), gedreht um
|
||||
// `rotation` (0 beim heutigen Werkzeug) — siehe `moveGrip`.
|
||||
if (g.shape === "ellipse") {
|
||||
const c = Math.cos(g.rotation), s = Math.sin(g.rotation);
|
||||
return [
|
||||
{ x: g.center.x + g.rx * c, y: g.center.y + g.rx * s },
|
||||
{ x: g.center.x - g.ry * s, y: g.center.y + g.ry * c },
|
||||
];
|
||||
}
|
||||
// Bezier/Spline: alle Kontroll-/Wegpunkte als Griffe (wie polyline).
|
||||
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
|
||||
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
|
||||
if (g.shape === "text") return [g.at];
|
||||
return [];
|
||||
@@ -1518,7 +1551,7 @@ function moveGrip(
|
||||
if (g.shape === "line") {
|
||||
return { ...d, geom: index === 0 ? { ...g, a: pt } : { ...g, b: pt } };
|
||||
}
|
||||
if (g.shape === "polyline") {
|
||||
if (g.shape === "polyline" || g.shape === "bezier" || g.shape === "spline") {
|
||||
const pts = g.pts.map((p, i) => (i === index ? pt : p));
|
||||
return { ...d, geom: { ...g, pts } };
|
||||
}
|
||||
@@ -1551,6 +1584,17 @@ function moveGrip(
|
||||
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 } };
|
||||
}
|
||||
// Ellipse: Griff 0 (rx-Achse) bzw. 1 (ry-Achse) — der gezogene Punkt wird
|
||||
// ins lokale (gedrehte) Achsensystem projiziert, nur die jeweils
|
||||
// betroffene Halbachse ändert sich.
|
||||
if (g.shape === "ellipse") {
|
||||
const c = Math.cos(g.rotation), s = Math.sin(g.rotation);
|
||||
const dx = pt.x - g.center.x, dy = pt.y - g.center.y;
|
||||
const lx = dx * c + dy * s;
|
||||
const ly = -dx * s + dy * c;
|
||||
if (index === 0) return { ...d, geom: { ...g, rx: Math.max(1e-3, Math.abs(lx)) } };
|
||||
return { ...d, geom: { ...g, ry: Math.max(1e-3, Math.abs(ly)) } };
|
||||
}
|
||||
return d;
|
||||
}),
|
||||
};
|
||||
@@ -1590,7 +1634,11 @@ function moveElementBy(
|
||||
return { ...d, geom: { ...g, min: mv(g.min), max: mv(g.max) } };
|
||||
case "circle":
|
||||
case "arc":
|
||||
case "ellipse":
|
||||
return { ...d, geom: { ...g, center: mv(g.center) } };
|
||||
case "bezier":
|
||||
case "spline":
|
||||
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||
case "text":
|
||||
return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||
default:
|
||||
@@ -1723,7 +1771,7 @@ function moveEdge(
|
||||
// Linie hat nur eine Kante [0,1] → das ganze Segment verschieben.
|
||||
return { ...d, geom: { ...g, a: mv(g.a), b: mv(g.b) } };
|
||||
}
|
||||
if (g.shape === "polyline") {
|
||||
if (g.shape === "polyline" || g.shape === "spline") {
|
||||
const pts = movePolygonEdge(g.pts, aIndex, bIndex, delta, !!g.closed);
|
||||
return { ...d, geom: { ...g, pts } };
|
||||
}
|
||||
|
||||
@@ -494,7 +494,9 @@ function isClosedGeom(g: Drawing2DGeom): boolean {
|
||||
return (
|
||||
g.shape === "rect" ||
|
||||
g.shape === "circle" ||
|
||||
(g.shape === "polyline" && g.closed)
|
||||
g.shape === "ellipse" ||
|
||||
(g.shape === "polyline" && g.closed) ||
|
||||
(g.shape === "spline" && g.closed)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -528,6 +530,20 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox {
|
||||
acc(g.center.x - g.r, g.center.y - g.r);
|
||||
acc(g.center.x + g.r, g.center.y + g.r);
|
||||
break;
|
||||
case "ellipse": {
|
||||
// Präzise Bounding-Box unter Berücksichtigung der Drehung (rotation=0
|
||||
// beim heutigen Werkzeug, Feld existiert für spätere Quellen).
|
||||
const c = Math.cos(g.rotation), s = Math.sin(g.rotation);
|
||||
const hw = Math.hypot(g.rx * c, g.ry * s);
|
||||
const hh = Math.hypot(g.rx * s, g.ry * c);
|
||||
acc(g.center.x - hw, g.center.y - hh);
|
||||
acc(g.center.x + hw, g.center.y + hh);
|
||||
break;
|
||||
}
|
||||
case "bezier":
|
||||
case "spline":
|
||||
for (const p of g.pts) acc(p.x, p.y);
|
||||
break;
|
||||
case "text":
|
||||
acc(g.at.x, g.at.y);
|
||||
break;
|
||||
|
||||
@@ -290,6 +290,8 @@ export function useGripEditing(opts: {
|
||||
if (g.shape === "rect" || g.shape === "image") out.push(...ringEdges(verts, owner));
|
||||
else if (g.shape === "polyline" && g.closed) out.push(...ringEdges(verts, owner));
|
||||
else if (g.shape === "polyline" && !g.closed) out.push(...openEdges(verts, owner));
|
||||
else if (g.shape === "spline" && g.closed) out.push(...ringEdges(verts, owner));
|
||||
else if (g.shape === "spline" && !g.closed) out.push(...openEdges(verts, owner));
|
||||
else if (g.shape === "line") out.push(...openEdges(verts, owner));
|
||||
}
|
||||
if (effectiveDrawingIds.length === 0) {
|
||||
@@ -462,7 +464,7 @@ export function useGripEditing(opts: {
|
||||
? project.drawings2d.find((d) => d.id === owner.drawingId)
|
||||
: selectedDrawing;
|
||||
const g = ownerDrawing?.geom;
|
||||
if (g && (g.shape === "circle" || g.shape === "arc")) return g.center;
|
||||
if (g && (g.shape === "circle" || g.shape === "arc" || g.shape === "ellipse")) return g.center;
|
||||
const refIndex = neighborGripIndex(index);
|
||||
return refIndex != null ? grips[refIndex] : null;
|
||||
};
|
||||
@@ -505,7 +507,8 @@ export function useGripEditing(opts: {
|
||||
const d = project.drawings2d.find((x) => x.id === owner.drawingId);
|
||||
const g = d?.geom;
|
||||
if (!g) return none;
|
||||
if (g.shape === "polyline") return ringNeighborPts(g.pts, owner.localIndex, !!g.closed);
|
||||
if (g.shape === "polyline" || g.shape === "spline")
|
||||
return ringNeighborPts(g.pts, owner.localIndex, !!g.closed);
|
||||
if (g.shape === "rect" || g.shape === "image") {
|
||||
const corners = [g.min, { x: g.max.x, y: g.min.y }, g.max, { x: g.min.x, y: g.max.y }];
|
||||
return ringNeighborPts(corners, owner.localIndex, true);
|
||||
|
||||
@@ -49,6 +49,11 @@ function drawingsBBox(
|
||||
else if (g.shape === "circle" || g.shape === "arc") {
|
||||
acc(g.center.x - g.r, g.center.y - g.r);
|
||||
acc(g.center.x + g.r, g.center.y + g.r);
|
||||
} else if (g.shape === "ellipse") {
|
||||
acc(g.center.x - g.rx, g.center.y - g.ry);
|
||||
acc(g.center.x + g.rx, g.center.y + g.ry);
|
||||
} else if (g.shape === "bezier" || g.shape === "spline") {
|
||||
for (const p of g.pts) acc(p.x, p.y);
|
||||
} else if (g.shape === "text") acc(g.at.x, g.at.y);
|
||||
}
|
||||
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
||||
@@ -64,6 +69,9 @@ function translateDrawing(d: Drawing2D, dx: number, dy: number): Drawing2D {
|
||||
case "rect": return { ...d, geom: { ...g, min: mv(g.min), max: mv(g.max) } };
|
||||
case "circle": return { ...d, geom: { ...g, center: mv(g.center) } };
|
||||
case "arc": return { ...d, geom: { ...g, center: mv(g.center) } };
|
||||
case "ellipse": return { ...d, geom: { ...g, center: mv(g.center) } };
|
||||
case "bezier": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||
case "spline": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||
case "text": return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||
default: return d;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user