Files
MagicSetEditor2/src/util/rotation.cpp
T
2026-06-05 02:45:56 +02:00

418 lines
15 KiB
C++

//+----------------------------------------------------------------------------+
//| Description: Magic Set Editor - Program to make card games |
//| Copyright: (C) Twan van Laarhoven and the other MSE developers |
//| License: GNU General Public License 2 or later (see file COPYING) |
//+----------------------------------------------------------------------------+
// ----------------------------------------------------------------------------- : Includes
#include <util/prec.hpp>
#include <util/rotation.hpp>
#include <gfx/gfx.hpp>
#include <data/font.hpp>
#include <wx/rawbmp.h>
// ----------------------------------------------------------------------------- : Rotation
Rotation::Rotation(Radians angle, const RealRect& rect, double zoom, double stretch, RotationFlags flags)
: angle(constrain_radians(angle))
, size(rect.size())
, origin(rect.position())
, zoomX(zoom * stretch)
, zoomY(zoom)
{
if (stretch != 1.0) {
size.width /= stretch;
}
// set origin
if (flags & ROTATION_ATTACH_TOP_LEFT) {
origin -= boundingBoxCorner(size);
}
}
void Rotation::setStretch(double s) {
size.width *= s * getStretch();
zoomX = zoomY * s;
}
RealPoint Rotation::tr(const RealPoint& p) const {
double s = sin(angle), c = cos(angle);
double x = p.x * zoomX, y = p.y * zoomY;
return RealPoint(c * x + s * y + origin.x,
-s * x + c * y + origin.y);
}
RealPoint Rotation::trPixel(const RealPoint& p) const {
double s = sin(angle), c = cos(angle);
double x = p.x * zoomX + 0.5, y = p.y * zoomY + 0.5;
return RealPoint(c * x + s * y + origin.x - 0.5,
-s * x + c * y + origin.y - 0.5);
}
RealPoint Rotation::trNoZoom(const RealPoint& p) const {
double s = sin(angle), c = cos(angle);
double x = p.x, y = p.y;
return RealPoint(c * x + s * y + origin.x,
-s * x + c * y + origin.y);
}
RealPoint Rotation::trPixelNoZoom(const RealPoint& p) const {
double s = sin(angle), c = cos(angle);
double x = p.x + 0.5, y = p.y + 0.5;
return RealPoint(c * x + s * y + origin.x - 0.5,
-s * x + c * y + origin.y - 0.5);
}
RealSize Rotation::trSizeToBB(const RealSize& size) const {
if (is_straight(angle)) {
if (is_sideways(angle)) {
return RealSize(size.height * zoomY, size.width * zoomX);
} else {
return RealSize(size.width * zoomX, size.height * zoomY);
}
} else {
double s = sin(angle), c = cos(angle);
double x = size.width * zoomX, y = size.height * zoomY;
return RealSize(fabs(c * x) + fabs(s * y), fabs(s * x) + fabs(c * y));
}
}
RealRect Rotation::trRectToBB(const RealRect& r) const {
const bool special_case_optimization = true;
double x = r.x * zoomX, y = r.y * zoomY;
double w = r.width * zoomX, h = r.height * zoomY;
if (special_case_optimization && is_rad0(angle)) {
return RealRect(origin.x + x, origin.y + y, w, h);
} else if (special_case_optimization && is_rad180(angle)) {
return RealRect(origin.x - x - w, origin.y - y - h, w, h);
} else if (special_case_optimization && is_rad90(angle)) {
return RealRect(origin.x + y, origin.y - x - w, h, w);
} else if (special_case_optimization && is_rad270(angle)) {
return RealRect(origin.x - y - h, origin.y + x, h, w);
} else {
double s = sin(angle), c = cos(angle);
RealRect result(c * x + s * y + origin.x,
-s * x + c * y + origin.y,
0,0);
if (c > 0) {
result.width += c * w;
result.height += c * h;
} else {
result.x += c * w;
result.width -= c * w;
result.y += c * h;
result.height -= c * h;
}
if (s > 0) {
result.width += s * h;
result.y -= s * w;
result.height += s * w;
} else {
result.x += s * h;
result.width -= s * h;
result.height -= s * w;
}
return result;
}
}
wxRegion Rotation::trRectToRegion(const RealRect& r) const {
if (is_straight(angle)) {
return trRectToBB(r).toRect();
} else {
wxPoint points[4] = {trPixel(RealPoint(r.left(), r.top() ))
,trPixel(RealPoint(r.left(), r.bottom()))
,trPixel(RealPoint(r.right(), r.bottom()))
,trPixel(RealPoint(r.right(), r.top() ))};
return wxRegion(4,points);
}
}
RealPoint Rotation::trInv(const RealPoint& p) const {
double s = sin(angle), c = cos(angle);
double x = p.x - origin.x, y = p.y - origin.y;
return RealPoint((c * x - s * y) / zoomX,
(s * x + c * y) / zoomY);
}
RealSize Rotation::trInv(const RealSize& x) const {
double s = sin(angle), c = cos(angle);
return RealSize((c * x.width - s * x.height) / zoomX,
(s * x.width + c * x.height) / zoomY);
}
RealPoint Rotation::boundingBoxCorner(const RealSize& size) const {
// This function is a bit tricky,
// I derived it by drawing the four cases.
// Two succeeding cases must agree where they overlap (0,90,180,270 degrees)
double s = sin(angle), c = cos(angle);
double w = size.width * zoomX, h = size.height * zoomY;
if (angle <= rad90) return RealPoint(0, -w * s);
if (angle <= rad180) return RealPoint(w * c, h * c - w * s);
if (angle <= rad270) return RealPoint(w * c + h * s, h * c);
else return RealPoint(h * s, 0);
}
// ----------------------------------------------------------------------------- : Rotater
Rotater::Rotater(Rotation& rot, const Rotation& by)
: old(rot)
, rot(rot)
{
// apply rotation
rot.origin = rot.tr(by.origin);
rot.size = by.size;
rot.angle = constrain_radians(rot.angle + by.angle);
// zooming is not really correct if rot.zoomX != rot.zoomY
rot.zoomX *= by.zoomX;
rot.zoomY *= by.zoomY;
}
Rotater::~Rotater() {
rot = old; // restore
}
// ----------------------------------------------------------------------------- : RotatedDC
RotatedDC::RotatedDC(DC& dc, Radians angle, const RealRect& rect, double zoom, RenderQuality quality, RotationFlags flags)
: Rotation(angle, rect, zoom, 1.0, flags)
, dc(dc), quality(quality)
{}
RotatedDC::RotatedDC(DC& dc, const Rotation& rotation, RenderQuality quality)
: Rotation(rotation)
, dc(dc), quality(quality)
{}
// ----------------------------------------------------------------------------- : RotatedDC : Drawing
void RotatedDC::DrawText(const String& text, const RealPoint& pos, int blur_radius, Color stroke_color, int stroke_radius, double stretch) {
DrawText(text, pos, dc.GetTextForeground(), blur_radius, stroke_color, stroke_radius, stretch);
}
void RotatedDC::DrawText(const String& text, const RealPoint& pos, Color color, int blur_radius, Color stroke_color, int stroke_radius, double stretch) {
if (text.empty()) return;
if (color.Alpha() == 0) return;
if (quality >= QUALITY_AA) {
RealRect r(pos, GetTextExtent(text));
RealRect r_ext = trRectToBB(r);
RealPoint pos2 = tr(pos);
stretch *= getStretch();
if (fabs(stretch - 1) > 1e-6) {
r.width *= stretch;
RealRect r_ext2 = trRectToBB(r);
pos2.x += r_ext2.x - r_ext.x;
pos2.y += r_ext2.y - r_ext.y;
r_ext.x = r_ext2.x;
r_ext.y = r_ext2.y;
}
draw_resampled_text(dc, text, pos2, r_ext, angle, color, blur_radius, stroke_color, stroke_radius, stretch);
} else if (quality >= QUALITY_SUB_PIXEL) {
RealPoint p_ext = tr(pos)*text_scaling;
double usx,usy;
dc.GetUserScale(&usx, &usy);
dc.SetUserScale(usx/text_scaling, usy/text_scaling);
dc.SetTextForeground(color);
dc.DrawRotatedText(text, (int) p_ext.x, (int) p_ext.y, rad_to_deg(angle));
dc.SetUserScale(usx, usy);
} else {
RealPoint p_ext = tr(pos);
dc.SetTextForeground(color);
dc.DrawRotatedText(text, (int) p_ext.x, (int) p_ext.y, rad_to_deg(angle));
}
}
void RotatedDC::DrawTextWithShadowOrStroke(const String& text, const FontRef& font, const RealPoint& pos, double scale, double stretch) {
double s_scale = scale * dc.GetFont().GetPointSize() / text_scaling / 15.;
if (font.hasShadow() && !font.hasStroke()) {
RealSize shadow_displacement = trInvS(RealSize(font.shadow_displacement_x, font.shadow_displacement_y) * s_scale);
DrawText(text, pos + shadow_displacement, font.shadow_color, lround(font.shadow_blur * s_scale), Color(0,0,0), 0, stretch);
}
DrawText(text, pos, font.color, lround(font.stroke_blur * s_scale), font.stroke_color, lround(font.stroke_radius * s_scale), stretch);
}
void RotatedDC::DrawBitmap(const Bitmap& bitmap, const RealPoint& pos) {
if (is_rad0(angle)) {
RealPoint p_ext = tr(pos);
dc.DrawBitmap(bitmap, to_int(p_ext.x), to_int(p_ext.y), true);
} else {
DrawImage(bitmap.ConvertToImage(), pos);
}
}
void RotatedDC::DrawImage(const Image& image, const RealPoint& pos, ImageCombine combine) {
Image rotated = rotate_image(image, angle);
DrawPreRotatedImage(rotated, RealRect(pos,trInvS(RealSize(image))), combine);
}
void RotatedDC::DrawPreRotatedBitmap(const Bitmap& bitmap, const RealRect& rect) {
RealPoint p_ext = tr(rect.position()) + boundingBoxCorner(rect.size());
dc.DrawBitmap(bitmap, to_int(p_ext.x), to_int(p_ext.y), true);
}
void RotatedDC::DrawPreRotatedImage (const Image& image, const RealRect& rect, ImageCombine combine) {
RealPoint p_ext = tr(rect.position()) + boundingBoxCorner(rect.size());
draw_combine_image(dc, to_int(p_ext.x), to_int(p_ext.y), image, combine);
}
void RotatedDC::DrawLine (const RealPoint& p1, const RealPoint& p2) {
wxPoint p1_ext = tr(p1), p2_ext = tr(p2);
dc.DrawLine(p1_ext.x, p1_ext.y, p2_ext.x, p2_ext.y);
}
void RotatedDC::DrawRectangle(const RealRect& r) {
if (is_straight(angle)) {
wxRect r_ext = trRectToBB(r);
dc.DrawRectangle(r_ext.x, r_ext.y, r_ext.width, r_ext.height);
} else {
wxPoint points[4] = {trPixel(RealPoint(r.left(), r.top() ))
,trPixel(RealPoint(r.left(), r.bottom()))
,trPixel(RealPoint(r.right(), r.bottom()))
,trPixel(RealPoint(r.right(), r.top() ))};
dc.DrawPolygon(4,points);
}
}
void RotatedDC::DrawRoundedRectangle(const RealRect& r, double radius) {
if (is_straight(angle)) {
wxRect r_ext = trRectToBB(r);
dc.DrawRoundedRectangle(r_ext.x, r_ext.y, r_ext.width, r_ext.height, trS(radius));
} else {
// TODO
DrawRectangle(r);
}
}
void RotatedDC::DrawInvertRectangle(const RealRect& r) {
wxRect r_ext = trRectToBB(r);
wxBitmap bmp(r_ext.width, r_ext.height, 24);
wxMemoryDC memDC(bmp);
memDC.Blit(0, 0, r_ext.width, r_ext.height, &dc, r_ext.x, r_ext.y);
memDC.SelectObject(wxNullBitmap);
wxNativePixelData data(bmp);
if (!data) return; // Raw access not available on this platform/bitmap
wxNativePixelData::Iterator p(data);
for (int j = 0; j < r_ext.height; ++j) {
wxNativePixelData::Iterator rowStart = p;
for (int i = 0; i < r_ext.width; ++i) {
// Invert each channel
p.Red() = 255 - p.Red();
p.Green() = 255 - p.Green();
p.Blue() = 255 - p.Blue();
++p; // Move to next pixel in the row
}
p = rowStart;
p.OffsetY(data, 1); // Move to the next row
}
dc.DrawBitmap(bmp, RealPoint(r_ext.x, r_ext.y));
}
void RotatedDC::DrawCircle(const RealPoint& center, double radius) {
wxPoint p = tr(center);
dc.DrawCircle(p.x + 1, p.y + 1, int(trS(radius)));
}
void RotatedDC::DrawEllipse(const RealPoint& center, const RealSize& size) {
wxPoint c_ext = tr(center - size/2);
wxSize s_ext = trSizeToBB(size);
dc.DrawEllipse(c_ext.x, c_ext.y, s_ext.x, s_ext.y);
}
void RotatedDC::DrawEllipticArc(const RealPoint& center, const RealSize& size, Radians start, Radians end) {
wxPoint c_ext = tr(center - size/2);
wxSize s_ext = trSizeToBB(size);
dc.DrawEllipticArc(c_ext.x, c_ext.y, s_ext.x, s_ext.y, rad_to_deg(start + angle), rad_to_deg(end + angle));
}
void RotatedDC::DrawEllipticSpoke(const RealPoint& center, const RealSize& size, Radians angle) {
wxPoint c_ext = tr(center - size/2);
wxSize s_ext = trSizeToBB(size);
Radians rot_angle = angle + this->angle;
Radians sin_angle = sin(rot_angle), cos_angle = cos(rot_angle);
// position of center and of point on the boundary can vary because of rounding errors,
// this code matches DrawEllipticArc (at least on windows xp).
dc.DrawLine(
c_ext.x + int( 0.5 * (s_ext.x + cos_angle) ), // center
c_ext.y + int( 0.5 * (s_ext.y - sin_angle) ),
c_ext.x + int( 0.5 + 0.5 * (s_ext.x-1) * (1 + cos_angle) ), // boundary
c_ext.y + int( 0.5 + 0.5 * (s_ext.y-1) * (1 - sin_angle) )
);
}
// ----------------------------------------------------------------------------- : Forwarded properties
void RotatedDC::SetPen(const wxPen& pen) { dc.SetPen(pen); }
void RotatedDC::SetBrush(const wxBrush& brush) { dc.SetBrush(brush); }
void RotatedDC::SetTextForeground(const Color& color) { dc.SetTextForeground(color); }
void RotatedDC::SetLogicalFunction(wxRasterOperationMode function) { dc.SetLogicalFunction(function); }
void RotatedDC::SetFont(const wxFont& font) {
if (quality == QUALITY_LOW && zoomX == 1 && zoomY == 1) {
dc.SetFont(font);
} else {
wxFont scaled = font;
if (quality == QUALITY_LOW) {
scaled.SetPointSize((int) trY(font.GetPointSize()));
} else {
scaled.SetPointSize((int) (trY(font.GetPointSize()) * text_scaling));
}
dc.SetFont(scaled);
}
}
void RotatedDC::SetFont(const FontRef& font, double scale) {
dc.SetFont(font.toWxFont(trS(scale) * (quality == QUALITY_LOW ? 1 : text_scaling)));
}
double RotatedDC::getFontSizeStep() const {
if (quality == QUALITY_LOW) {
return 1;
} else {
return 1. / text_scaling;
}
}
RealSize RotatedDC::GetTextExtent(const String& text) const {
int w, h;
dc.GetTextExtent(text, &w, &h);
#ifdef __WXGTK__
// HACK: Some fonts don't get the descender height set correctly.
int charHeight = dc.GetCharHeight();
if (charHeight != h)
h += h - charHeight;
#endif
if (quality == QUALITY_LOW) {
return RealSize(w / zoomX, h / zoomY);
} else {
return RealSize(w / (zoomX * text_scaling), h / (zoomY * text_scaling));
}
}
double RotatedDC::GetCharHeight() const {
int h = dc.GetCharHeight();
#ifdef __WXGTK__
// See above HACK
int extent;
dc.GetTextExtent(_("H"), 0, &extent);
if (h != extent)
h = 2 * extent - h;
#endif
if (quality == QUALITY_LOW) {
return h / zoomY;
} else {
return h / (zoomY * text_scaling);
}
}
void RotatedDC::SetClippingRegion(const RealRect& rect) {
dc.SetDeviceClippingRegion(trRectToRegion(rect));
}
void RotatedDC::DestroyClippingRegion() {
dc.DestroyClippingRegion();
}
// ----------------------------------------------------------------------------- : Other
Bitmap RotatedDC::GetBackground(const RealRect& r) {
wxRect wr = trRectToBB(r);
Bitmap background(wr.width, wr.height);
wxMemoryDC mdc;
mdc.SelectObject(background);
mdc.Blit(0, 0, wr.width, wr.height, &dc, wr.x, wr.y);
mdc.SelectObject(wxNullBitmap);
return background;
}