Я пишу программу для маркировки и столкнулся с очень сложной математической проблемой.
У меня есть объекты, которые я могу масштабировать вверх, вниз, влево, вправо, вверх-вправо, вниз-влево и т. д. Я написал всю логику для обработки всех этих случаев, даже для сгруппированных объектов. Проблема возникает, когда у нас есть вращение. Теперь вращение для отдельных объектов работает отлично, проблема заключается в логике при попытке пропорционально масштабировать все объекты внутри объекта, который масштабируется во время вращения.
Я написал простой проект, который мы можем использовать для демонстрации этой проблемы.
Позвольте мне показать вам пример.
Правильное масштабирование отдельного объекта, даже при вращении:

Правильное масштабирование родительских и дочерних объектов без вращения:

Неправильное масштабирование родительских и дочерних объектов с вращением:

Я ломал голову над этой проблемой в течение последних нескольких дней, и сейчас я продвинулся гораздо дальше, как вы можете видеть в третьей анимации, что масштабирование, по крайней мере, правильное, когда я масштабирую его идеально по диагонали, но это заняло больше времени, чем следовало бы, и прямо сейчас я застрял, потому что понятия не имею, как представить расчет в своей голове, чтобы это правильно вело себя при масштабировании только в одном направлении.
Хотя это простой проект, из-за его характера он немного самостоятелен, поэтому я добавлю ссылку на исходный код, если кто-то захочет запустить его самостоятельно, но вот некоторый основной код:
Класс объекта:
public Dictionary<OriginName, PointF> Vector4Points { get; private set; }
public List<ObjRectangle> Children = new List<ObjRectangle>();
public OriginName GrabbedResizeOriginName;
private PointF _location;
public PointF Location
{
get
{
return _location;
}
set
{
PointF lastLocation = _location;
PointF moveDif = new PointF(value.X - lastLocation.X, value.Y - lastLocation.Y);
_location = value;
Origin = new PointF(Origin.X + moveDif.X, Origin.Y + moveDif.Y);
foreach(ObjRectangle objRectangle in Children)
{
objRectangle.MoveBy(moveDif);
}
Vector4Points = Get4VectorPoints();
}
}
private SizeF _size;
public SizeF Size
{
get
{
return _size;
}
set
{
float xDif = value.Width - _size.Width;
float yDif = value.Height - _size.Height;
switch (GrabbedResizeOriginName)
{
case OriginName.TopRight:
if (Rotation == 0f) // No rotation
Location = new PointF(Location.X, Location.Y - yDif);
else
{
PointF rotatedDelta = Utilities.RotatePoint(new PointF(0, -yDif), new PointF(0, 0), Rotation);
Location = new PointF(Location.X + rotatedDelta.X, Location.Y + rotatedDelta.Y);
}
break;
case OriginName.BottomLeft:
if (Rotation == 0f) // No rotation
Location = new PointF(Location.X - xDif, Location.Y);
else
{
PointF rotatedDelta = Utilities.RotatePoint(new PointF(-xDif, 0), new PointF(0, 0), Rotation);
Location = new PointF(Location.X + rotatedDelta.X, Location.Y + rotatedDelta.Y);
}
break;
case OriginName.TopLeft:
if (Rotation == 0f) // No rotation
Location = new PointF(Location.X - xDif, Location.Y - yDif);
else
{
PointF rotatedDelta = Utilities.RotatePoint(new PointF(-xDif, -yDif), new PointF(0, 0), Rotation);
Location = new PointF(Location.X + rotatedDelta.X, Location.Y + rotatedDelta.Y);
}
break;
default:
break;
}
_size = value;
Vector4Points = Get4VectorPoints();
}
}
public void MoveBy(PointF moveBy)
{
Location = new PointF(Location.X + moveBy.X, Location.Y + moveBy.Y);
}
public void ResizeParentAndChildren(SizeF newSize)
{
float scaleX = newSize.Width / Size.Width;
float scaleY = newSize.Height / Size.Height;
foreach (ObjRectangle child in Children)
{
child.GrabbedResizeOriginName = GrabbedResizeOriginName;
// Calculate the offset of the child from the parent's original origin
PointF childOffset = new PointF(child.Location.X - Location.X, child.Location.Y - Location.Y);
// Scale the offset based on the scaling factors
PointF scaledOffset = new PointF(childOffset.X * scaleX, childOffset.Y * scaleY);
// Calculate the new position of the child relative to the new origin of the parent
PointF newChildPosition = new PointF(Location.X + scaledOffset.X, Location.Y + scaledOffset.Y);
// Scale the size of the child
child.Size = new SizeF(child.Size.Width * scaleX, child.Size.Height * scaleY);
// Set the new position of the child
child.Location = newChildPosition;
}
Size = newSize;
}
public Dictionary<OriginName, PointF> Get4VectorPoints()
{
Dictionary<OriginName, PointF> points = new Dictionary<OriginName, PointF>
{
{ OriginName.TopLeft, Utilities.RotatePoint(new PointF(Location.X, Location.Y), Origin, Rotation) },
{ OriginName.TopRight, Utilities.RotatePoint(new PointF(Location.X + Size.Width, Location.Y), Origin, Rotation) },
{ OriginName.BottomRight, Utilities.RotatePoint(new PointF(Location.X + Size.Width, Location.Y + Size.Height), Origin, Rotation) },
{ OriginName.BottomLeft, Utilities.RotatePoint(new PointF(Location.X, Location.Y + Size.Height), Origin, Rotation) }
};
return points;
}
Утилита вращения точки:
public static PointF RotatePoint(PointF point, PointF origin, double angleDegrees)
{
// Convert angle from degrees to radians
double angleRadians = angleDegrees * Math.PI / 180.0;
// Translate point so that origin is at (0, 0)
double translatedX = point.X - origin.X;
double translatedY = point.Y - origin.Y;
// Perform rotation
double rotatedX = translatedX * Math.Cos(angleRadians) - translatedY * Math.Sin(angleRadians);
double rotatedY = translatedX * Math.Sin(angleRadians) + translatedY * Math.Cos(angleRadians);
// Translate point back to its original position
rotatedX += origin.X;
rotatedY += origin.Y;
return new PointF((float)rotatedX, (float)rotatedY);
}
Код изменения размера события перемещения мыши:
if (_resizingObject)
{
PointF rotatedDelta = Utilities.RotatePoint(mouseDelta, new PointF(0, 0), -geometryContainer.SelectedObject.Rotation);
float deltaX = rotatedDelta.X;
float deltaY = rotatedDelta.Y;
switch (geometryContainer.SelectedObject.GrabbedResizeOriginName)
{
case OriginName.TopLeft:
deltaX = -deltaX;
deltaY = -deltaY;
break;
case OriginName.TopRight:
deltaY = -deltaY;
break;
case OriginName.BottomLeft:
deltaX = -deltaX;
break;
default:
break;
}
geometryContainer.SelectedObject.ResizeParentAndChildren(new SizeF(geometryContainer.SelectedObject.Size.Width + deltaX, geometryContainer.SelectedObject.Size.Height + deltaY));
Invalidate();
}
Вот ссылка для скачивания тестового проекта: https://uploadnow.io/f/GF2PQb7
Чтобы добавить объект, щелкните правой кнопкой мыши по форме.
Чтобы добавить дочерний объект, щелкните правой кнопкой мыши внутри уже существующего объекта.
Чтобы повернуть объект, вам нужно щелкнуть по нему и использовать колесо прокрутки.
Спасибо за ответы заранее!
@Mike'Pomax'Kamermans, ну ладно, но как мне это сделать?
Присвоив прямоугольнику список childRects (или любое другое имя, подходящее для вашего кода), а затем рекурсивно рисуя прямоугольники, где вы рисуете группу только с помощью вызова external.draw(), преобразуйте систему координат, применяя переводы, поворот и масштабировать, затем нарисовать себя, затем вызвать .draw() для каждого дочернего элемента, а затем отменить преобразование системы координат. Из-за того, как работает рекурсия, это «затруднит» преобразование координат. Как вы это сделаете, используя какой конкретный API, конечно, во многом зависит от того, какую конкретную среду рисования вы используете.
(Я не знаю C#, поэтому я могу написать вам ответ, но он будет на другом языке, чтобы продемонстрировать концепцию, а не ответ на C#, дающий вам точный код для копирования и вставки - идея состоит в том, что вы отслеживаете «какое бы ни было текущее преобразование», либо преобразуя систему координат, если у вас есть API, который позволяет вам это делать, либо самостоятельно отслеживая матрицу преобразования и обязательно преобразуя «реальные координаты» в их преобразованные эквиваленты и рисуя с их помощью вместо)
@Mike'Pomax'Kamermans Если бы вы могли это сделать, я был бы очень признателен! Ничего страшного, если это будет на разных языках, главное, чтобы я смог понять эту концепцию.
Сделанный. Вам нужно запустить этот пример в «полностраничном» режиме, поскольку встроенные размеры исполняемого фрагмента слишком малы, чтобы правильно отображать группу вложенных прямоугольников, если мы также хотим иметь возможность их масштабировать.





Вместо того, чтобы пытаться выяснить вложенные преобразования для каждого элемента отдельно, гораздо проще работать с группами и «преобразовать вашу систему координат» (или использовать одноэлементный преобразователь, который поддерживает матрицу, которую вы используете для преобразования ваших «реальных координат» в « нарисовать координаты»). Если вы моделируете группы элементов, используя отношение дерева, то каждый узел может обновить текущее преобразование координат на основе параметров локального преобразования, нарисовать себя, а затем приказать всем своим дочерним узлам сделать то же самое (которые будут применять свои собственные преобразования поверх текущий), а затем в качестве последнего шага отмените их локальное преобразование, чтобы система координат вернулась к тому состоянию, которое было до того, как мы начали рисовать.
В JS (с использованием нотации классов, поэтому реализация на C# должна быть почти тривиальной) с кодом, который предполагает, что у вас есть что-то, что может отслеживать преобразования с помощью отдельных функций translate, rotate и scale:
class DrawableElement {
// ...first some boring boilerplate...
ox = 0; // translation
oy = 0;
sx = 1; // scale
sy = 1;
angle = 0; // rotation
children = [];
constructor(x, y) {
this.x = x;
this.y = y;
}
setRotation(a) {
this.angle = a;
}
setScale(x, y) {
if (x) this.sx = x;
if (y) this.sy = y;
}
// Then, the parts that matter:
addChild(child) {
child.setParent(this);
this.children.push(child);
}
setParent(parent) {
this.parent = parent;
// Set our parent's position as negative offset,
// so we can draw ourselves relative to the parent.
this.ox = -parent.x;
this.oy = -parent.y;
}
// This is the part that really matters, as this
// is the part that handles where/how things get drawn:
draw() {
// first, update the coordinate system
this.applyTransform();
// then draw ourselves
this.drawSelf();
// then draw our children without resetting the
// coordinate system, so that their transforms go
// "on top of" the transform that's already in effect:
this.children.forEach(r => r.draw());
// then undo (only) our coordinate transform
this.reverseTransform();
}
applyTransform() {
// note that the order matters here. If we scale before
// rotation, for example, we'll end up skewing instead
// of scaling...
translate(this.x + this.ox, this.y + this.oy);
rotate(this.angle);
scale(this.sx, this.sy)
}
reverseTransform() {
// and of course order matters here, too.
scale(1/this.sx, 1/this.sy);
// note that the above *can* lead to rounding errors
// doing funny things, which a transformer class that
// can cache and restore transformation matrices won't
// be susceptible to, at the cost of "a bit more memory".
rotate(-this.angle);
translate(-(this.x + this.ox), -(this.y + this.oy));
}
}
А затем, поскольку мы хотим рисовать прямоугольники, мы создаем расширение этого класса:
class Rectangle extends DrawableElement {
constructor(x, y, w = 0, h = 0) {
super(x,y);
this.w = w;
this.h = h;
}
drawSelf() {
// because we're applying a transform that
// puts the coordinate system's (0,0) on our
// (x,y), we draw relative to (0,0). Handy!
setStroke(`black`);
setFill(`#3332`);
rect(0, 0, this.w, this.h);
// draw that upper-left corner, too
setFill(`red`);
point(0, 0);
}
}
Хитрость теперь заключается в том, чтобы «группировать» (выполнено в приведенном выше пути кода addChild) и «разгруппировать» (не реализовано в этом примере) объекты, чтобы преобразования применялись «к группе», а не к отдельным рисуемым элементам.
(именно поэтому такие приложения, как Illustrator, Inkscape, Blender и т. д., поддерживают группировку и разгруппировку).
Объединим все это в работоспособную демонстрацию:
function sourceCode() {
class DrawableElement {
ox = 0;
oy = 0;
sx = 1;
sy = 1;
angle = 0;
children = [];
constructor(x, y) {
this.x = x;
this.y = y;
}
setRotation(a) {
this.angle = a;
}
setScale(x, y) {
if (x) this.sx = x;
if (y) this.sy = y;
}
addChild(child) {
child.setParent(this);
this.children.push(child);
}
setParent(parent) {
this.parent = parent;
this.ox = -parent.x;
this.oy = -parent.y;
}
draw() {
this.applyTransform();
this.drawSelf();
this.children.forEach(r => r.draw());
this.reverseTransform();
}
applyTransform() {
translate(this.x + this.ox, this.y + this.oy);
rotate(this.angle);
scale(this.sx, this.sy)
}
reverseTransform() {
scale(1 / this.sx, 1 / this.sy);
rotate(-this.angle);
translate(-(this.x + this.ox), -(this.y + this.oy));
}
}
class Rectangle extends DrawableElement {
constructor(x, y, w = 0, h = 0) {
super(x, y);
this.w = w;
this.h = h;
}
drawSelf() {
setStroke(`black`);
setFill(`#3332`);
rect(0, 0, this.w, this.h);
setFill(`red`);
point(0, 0);
}
}
const W = 600, H = 400;
// Let's group some rects:
const r1 = new Rectangle(150, 50, W - 200, H - 200);
const r2 = new Rectangle(200, 100, 100, 100);
const r3 = new Rectangle(320, 190, 200, 40);
const r4 = new Rectangle(210, 110, 70, 50);
// we'll make r1 the "outer group":
r1.addChild(r2);
r1.addChild(r3);
// and we'll make r2 a small "inner group":
r2.addChild(r4);
function setup() {
setSize(W, H);
setBorder(1, `black`);
setGrid(20, `grey`);
addSlider(`rotation`, { value: 0, min: 0, max: TAU, step: TAU / 100, transform: (r) => updateAngle(r) });
addSlider(`scale_x`, { max: 2, step: 0.01, transform: (x) => updateScale(x, undefined) });
addSlider(`scale_y`, { max: 2, step: 0.01, transform: (y) => updateScale(undefined, y) });
}
function updateAngle(r) {
r1.setRotation(r);
}
function updateScale(x, y) {
r1.setScale(x, y);
}
function draw() {
clear(`white`);
r1.draw();
}
}
// load the code once the custom element loader is done:
customElements.whenDefined(`graphics-element`).then(() => {
document.getElementById(`graphics`).loadFromFunction(sourceCode);
});<script src = "https://cdnjs.cloudflare.com/ajax/libs/graphics-element/5.0.0/graphics-element.js" type = "module"></script>
<link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/graphics-element/5.0.0/graphics-element.min.css" />
<graphics-element id = "graphics" title = "grouped transforms"></graphics-element>Обратите внимание, что этот код использует «полное» преобразование координат, поэтому вы также увидите такие вещи, как опорные точки, которые обычно представляют собой круги, вместо этого масштабируются до эллипсов. В то время как для таких вещей, как фон, вы этого хотите, для таких вещей, как точки, вы часто этого не делаете, поэтому обычно у вас также есть две отдельные функции, такие как screenToWorld и worldToScreen, которые вы можете использовать для преобразования непреобразованной пары (x,y) в преобразованную пары (и наоборот), чтобы вы могли рисовать непреобразованный контент по преобразованной координате, например:
...
drawSelf() {
...
// draw the upper-left corner as an untransformed circle,
// centered on where (0,0) is right now:
const { x, y } = worldToScreen(0,0);
setFill(`red`);
drawPointAtScreenCoordinate(x, y); // bypass the transform matrix
}
...
Реализация этих функций обычно идет рука об руку с реализациями преобразования системы координат, поэтому, если вам нужно выполнить этот код самостоятельно, у вас в основном есть синглтон CoordinateTransformer, который кодирует матрицу преобразования 3x3, которую можно обновить с помощью translate, Функции rotate, scale и skew (и обычно некоторые прямые setMatrix тоже), с функцией screenToWorld, которая просто применяет текущую матрицу к переданным значениям координат, а также функцией worldToScreen, которая применяет обратную матрицу к переданным значениям координат. .
(и инвертировать матрицу преобразования относительно просто - также обратите внимание: если вы не понимаете, почему нам нужно то, что выглядит как 3D-матрица, для работы с 2D-координатами, см. этот вопрос)
Просто чтобы убедиться, что я правильно это понимаю. Дочерние объекты в этом примере все еще сохраняют свои реальные позиции, верно? Если я откажусь от них и изменю происхождение, они останутся на том же месте? Или есть еще какие-то расчеты? Я спрашиваю, потому что это заставляет меня перепроектировать весь код позиционирования, если я хочу пройти путь «Поворот преобразования». По крайней мере, я думаю, что я прав?
Правильно, потому что вам нужен какой-то способ восстановить их исходное положение, когда вы их разгруппируете. Таким образом, каждый элемент имеет свою реальную позицию, а также смещение, равное нулю, если «просто элемент», или ненулевое значение, когда элемент является дочерним элементом другого элемента. Если вы не хотите перепроектировать все, третий вариант — заставить каждый элемент кодировать свою собственную матрицу преобразования «полной цепочки», а затем создать код, который будет обновлять каждый из них индивидуально по мере группировки и разгруппировки. Но это будет (почти) столько же работы.
В тот момент, когда вы группируете объекты, их координаты перестают быть мировыми координатами и вместо этого становятся локальными координатами их родителя (на самом деле «мир» — это просто еще один родитель, просто он оказывается самым высоким). Таким образом, когда вы преобразуете родителя, вам будет гораздо проще обновлять дочерние элементы. Таким образом, проблема «вычисления масштаба с вращением» становится «сначала повернуть рабочую систему координат, чтобы она соответствовала родительской, а затем масштабировать всех дочерних элементов в виде тупой ограничивающей рамки»