I am writing a Labelling Software and I have encountered a very difficult mathematical problem.
I have objects which I can scale up, down, left, right, up-right, down-left etc. I have wrote all of the logic to handle all of those cases, even for grouped objects. The problem occurs when we have a rotation. Now, rotation for single objects works perfectly fine, the issue is within the logic when trying to proportionally scale all of the objects within the object that is being scaled while rotated.
I have written a bare-bone project which we can use to showcase this problem.
Let me show you an example.
Correct scaling behaviour of a single object, even with rotation:
Correct scaling behaviour of Parent and Children objects without rotation:
Wrong scaling behaviour of Parent and Children objects with rotation:
I've been boggling with this issue for the past couple of days, and I am much further now as you can see in the third animation that the scaling is at least correct when I scale it perfectly diagonally but it took longer than it should, and right now I am stuck because I have no idea how to imagine the calculation in my head for this to correctly behave when scaled in just one direction.
Although this is a bare-boned project, it has a bit going to itself due to the nature of it so I will add a link to the source code if anyone wants to run it for themselves, but here is some core code:
The Object Class:
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;
}
Point Rotation Utility Function:
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);
}
On Mouse Move Event resizing code:
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();
}
Here is a test project download link: https://uploadnow.io/f/GF2PQb7
To add an object, right click on the form.
In order to add a child object, right click inside already existing object.
In order to rotate an object, you have to click on it and use the scroll wheel.
Thank you for answers in advance!
childRects
list (or whatever name works for your code), and then drawing rectangles recursively, where you draw a group only by calling outer.draw(), have the transform the coordinate system by applying translations, rotation, and scaling, then draw itself, then call .draw() on each child, and then untransform the coordinate system. Because of how recursion works, this will "trickly down" the coordinate transform. How you do that, using which specific API, depends heavily on which specific drawing framework you're using of course. – Granduncle