rewrite print layout logic

allow for different size cards
This commit is contained in:
GenevensiS
2025-07-13 04:13:35 +02:00
parent 58ab8f3636
commit e4e5b6840e
4 changed files with 222 additions and 137 deletions
+5 -12
View File
@@ -153,13 +153,6 @@ IMPLEMENT_REFLECTION_NO_SCRIPT(StyleSheetSettings) {
REFLECT(card_spellcheck_enabled); REFLECT(card_spellcheck_enabled);
} }
// ----------------------------------------------------------------------------- : Printing
IMPLEMENT_REFLECTION_ENUM(PageLayoutType) {
VALUE_N("no space", LAYOUT_NO_SPACE);
VALUE_N("equal space", LAYOUT_EQUAL_SPACE);
}
// ----------------------------------------------------------------------------- : Settings // ----------------------------------------------------------------------------- : Settings
Settings settings; Settings settings;
@@ -174,8 +167,8 @@ Settings::Settings()
, symbol_grid_size (30) , symbol_grid_size (30)
, symbol_grid (true) , symbol_grid (true)
, symbol_grid_snap (false) , symbol_grid_snap (false)
, print_layout (LAYOUT_NO_SPACE) , print_spacing (0.33)
, internal_scale (1.0) , internal_scale (1.0)
, internal_image_extension(true) , internal_image_extension(true)
#if USE_OLD_STYLE_UPDATE_CHECKER #if USE_OLD_STYLE_UPDATE_CHECKER
, updates_url (_("https://magicseteditor.boards.net/page/downloads")) , updates_url (_("https://magicseteditor.boards.net/page/downloads"))
@@ -263,9 +256,9 @@ IMPLEMENT_REFLECTION_NO_SCRIPT(Settings) {
REFLECT(symbol_grid); REFLECT(symbol_grid);
REFLECT(symbol_grid_snap); REFLECT(symbol_grid_snap);
REFLECT(default_game); REFLECT(default_game);
REFLECT(print_layout); REFLECT(print_spacing);
REFLECT(apprentice_location); REFLECT(apprentice_location);
REFLECT(internal_scale); REFLECT(internal_scale);
REFLECT(internal_image_extension); REFLECT(internal_image_extension);
#if USE_OLD_STYLE_UPDATE_CHECKER #if USE_OLD_STYLE_UPDATE_CHECKER
REFLECT(updates_url); REFLECT(updates_url);
+1 -8
View File
@@ -112,13 +112,6 @@ public:
DECLARE_REFLECTION(); DECLARE_REFLECTION();
}; };
// ----------------------------------------------------------------------------- : Printing settings
enum PageLayoutType
{ LAYOUT_NO_SPACE
, LAYOUT_EQUAL_SPACE
//, LAYOUT_CUSTOM
};
// ----------------------------------------------------------------------------- : Settings // ----------------------------------------------------------------------------- : Settings
@@ -188,7 +181,7 @@ public:
// --------------------------------------------------- : Printing // --------------------------------------------------- : Printing
PageLayoutType print_layout; double print_spacing;
// --------------------------------------------------- : Special game stuff // --------------------------------------------------- : Special game stuff
String apprentice_location; String apprentice_location;
+171 -80
View File
@@ -14,40 +14,149 @@
#include <data/card.hpp> #include <data/card.hpp>
#include <data/stylesheet.hpp> #include <data/stylesheet.hpp>
#include <render/card/viewer.hpp> #include <render/card/viewer.hpp>
#include <wx/print.h> #include <wx/print.h>
#include <wx/valnum.h>
#include <unordered_set>
DECLARE_POINTER_TYPE(PageLayout); DECLARE_POINTER_TYPE(PageLayout);
// ----------------------------------------------------------------------------- : Layout // ----------------------------------------------------------------------------- : Layout
PageLayout::PageLayout() void PrintJob::init(const RealSize& page_size) {
: margin_left(0), margin_right(0), margin_top(0), margin_bottom(0) this->page_size = page_size;
, rows(0), cols(0), card_landscape(false) if (cards.empty()) return;
{} measure_cards();
layout_cards();
void PageLayout::init(const StyleSheet& stylesheet, PageLayoutType type, const RealSize& page_size) { align_cards();
this->page_size = page_size; center_cards();
margin_left = margin_right = margin_top = margin_bottom = 0;
card_size.width = stylesheet.card_width * 25.4 / stylesheet.card_dpi;
card_size.height = stylesheet.card_height * 25.4 / stylesheet.card_dpi;
card_dpi = stylesheet.card_dpi;
card_landscape = card_size.width > card_size.height;
cols = int(floor(page_size.width / card_size.width));
rows = int(floor(page_size.height / card_size.height));
// spacing
double hspace = (page_size.width - (cols * card_size.width ));
double vspace = (page_size.height - (rows * card_size.height));
if (type == LAYOUT_NO_SPACE) {
// no space between cards
card_spacing.width = card_spacing.height = 0;
margin_left = margin_right = hspace / 2;
margin_top = vspace * 1./3; margin_bottom = vspace * 2./3; // most printers have more margin at the bottom
} else {
// distribute whitespace evenly
margin_left = margin_right = card_spacing.width = hspace / (cols + 1);
margin_top = margin_bottom = card_spacing.height = vspace / (rows + 1);
}
} }
void PrintJob::measure_cards() {
FOR_EACH(card, cards) {
const StyleSheet& stylesheet = set->stylesheetFor(card);
RealSize size(stylesheet.card_width * 25.4 / stylesheet.card_dpi, stylesheet.card_height * 25.4 / stylesheet.card_dpi);
Radians rotation = 0.0;
bool rotated = abs(size.width - default_size.height) < abs(size.height - default_size.height); // try to align best to default card height
if (rotated) {
swap(size.width, size.height);
rotation = rad90;
}
CardLayout layout(card, size, rotation);
card_layouts.push_back(layout);
}
std::sort(card_layouts.begin(), card_layouts.end());
}
void PrintJob::layout_cards() {
page_layouts.push_back(vector<CardLayout>());
double row_top = 0.0, row_height = 0.0, row_width = 0.0;
unordered_set<int> already_laidout_cards;
while (true) {
// try to find a card that will fit on the current row
for (int i = 0; i < card_layouts.size(); ++i) {
if (already_laidout_cards.find(i) != already_laidout_cards.end()) continue;
if (card_layouts[i].size.width + row_width >= page_size.width) continue;
if (card_layouts[i].size.height + row_top >= page_size.height) continue;
// the card fits
card_layouts[i].pos.width = row_width;
card_layouts[i].pos.height = row_top;
page_layouts[page_layouts.size()-1].push_back(card_layouts[i]);
already_laidout_cards.insert(i);
if (already_laidout_cards.size() == card_layouts.size()) return;
// move to next spot on the row
row_width += card_layouts[i].size.width + spacing;
row_height = max(row_height, card_layouts[i].size.height + spacing);
goto continue_outer;
}
// no card fits
if (row_top == 0.0 && row_height == 0.0 && row_width == 0.0) {
// none of the remaining cards can fit on an empty page, return
page_layouts.pop_back();
queue_message(MESSAGE_WARNING, _ERROR_("cards bigger than page"));
return;
}
if (row_height == 0.0 && row_width == 0.0) {
// none of the remaining cards can fit on an empty row, create a new page
page_layouts.push_back(vector<CardLayout>());
row_top = 0.0;
continue;
}
// none of the remaining cards can fit on this row, create a new row
row_top += row_height;
row_width = row_height = 0.0;
continue_outer:;
}
}
void PrintJob::align_cards() {
// align cards that are at most this far appart in millimeters
double threshold_top = 0.2 * default_size.width;
// consider cards that are this close to already be aligned
double threshold_bottom = 0.05;
// for each page
for (int p = 0; p < page_layouts.size(); ++p) {
vector<CardLayout>& page_layout = page_layouts[p];
// for each card on the page
for (int max = 0, j = 0; j < page_layout.size(); ++max, ++j) {
if (max > 100) {
queue_message(MESSAGE_WARNING, _("DEBUG: large amount of iterations when aligning cards for print"));
break;
}
double x = page_layout[j].pos.width;
double y = page_layout[j].pos.height;
// if another card is almost aligned
for (int i = 0; i < page_layout.size(); ++i) {
if (i == j) continue;
double difference = page_layout[i].pos.width - x;
if (threshold_bottom < difference && difference <= threshold_top) {
// get the card, and all cards to the right on the same row
vector<int> cards;
cards.push_back(j);
for (int h = 0; h < page_layout.size(); ++h) {
if (h == j) continue;
double difference_x = page_layout[h].pos.width - x;
double difference_y = abs(page_layout[h].pos.height - y);
if (difference_y < threshold_bottom && difference_x > threshold_bottom) {
cards.push_back(h);
}
}
// check if all these cards can be moved to the right
bool can_move = true;
for (int h = 0; h < cards.size(); ++h) {
if (page_layout[cards[h]].pos.width + page_layout[cards[h]].size.width + difference > page_size.width) {
can_move = false;
break;
}
}
// move the cards
if (can_move) {
for (int h = 0; h < cards.size(); ++h) {
page_layout[cards[h]].pos.width += difference;
}
j = -1; // restart, new cards may be in range now
goto continue_outer;
}
}
}
continue_outer:;
}
}
}
void PrintJob::center_cards() {
for (int p = 0; p < page_layouts.size(); ++p) {
vector<CardLayout>& page_layout = page_layouts[p];
RealSize page_margin(0.0, 0.0);
for (int i = 0; i < page_layout.size(); ++i) {
double width = page_layout[i].pos.width + page_layout[i].size.width;
double height = page_layout[i].pos.height + page_layout[i].size.height;
if (page_margin.width < width) page_margin.width = width;
if (page_margin.height < height) page_margin.height = height;
}
page_margin.width = (page_size.width - page_margin.width) / 2;
page_margin.height = (page_size.height - page_margin.height) / 2;
for (int i = 0; i < page_layout.size(); ++i) {
page_layout[i].pos.width += page_margin.width;
page_layout[i].pos.height += page_margin.height;
}
}
}
// ----------------------------------------------------------------------------- : Printout // ----------------------------------------------------------------------------- : Printout
@@ -70,11 +179,11 @@ private:
double scale_x, scale_y; // priter pixel per mm double scale_x, scale_y; // priter pixel per mm
int pageCount() { int pageCount() {
return job->num_pages(); return job->page_layouts.size();
} }
/// Draw a card, that is card_nr on this page, find the postion by asking the layout /// Draw a card according to it's CardLayout info
void drawCard(DC& dc, const CardP& card, int card_nr); void drawCard(DC& dc, const PrintJob::CardLayout& card_layout);
}; };
CardsPrintout::CardsPrintout(PrintJobP const& job) CardsPrintout::CardsPrintout(PrintJobP const& job)
@@ -93,88 +202,69 @@ bool CardsPrintout::HasPage(int page) {
} }
void CardsPrintout::OnPreparePrinting() { void CardsPrintout::OnPreparePrinting() {
if (job->layout.empty()) { if (job->empty()) {
int pw_mm, ph_mm; int pw_mm, ph_mm;
GetPageSizeMM(&pw_mm, &ph_mm); GetPageSizeMM(&pw_mm, &ph_mm);
job->layout.init(*job->set->stylesheet, job->layout_type, RealSize(pw_mm, ph_mm)); job->init(RealSize(pw_mm, ph_mm));
} }
} }
bool CardsPrintout::OnPrintPage(int page) { bool CardsPrintout::OnPrintPage(int page) {
DC& dc = *GetDC(); DC& dc = *GetDC();
// scale factors (pixels per mm) // page size in millimeters
int pw_mm, ph_mm; int pw_mm, ph_mm;
GetPageSizeMM(&pw_mm, &ph_mm); GetPageSizeMM(&pw_mm, &ph_mm);
// page size in pixels
int pw_px, ph_px; int pw_px, ph_px;
dc.GetSize(&pw_px, &ph_px); dc.GetSize(&pw_px, &ph_px);
// scale factors (pixels per mm)
scale_x = (double)pw_px / pw_mm; scale_x = (double)pw_px / pw_mm;
scale_y = (double)ph_px / ph_mm; scale_y = (double)ph_px / ph_mm;
// print the cards that belong on this page // print the cards that belong on this page
int start = (page - 1) * job->layout.cards_per_page(); FOR_EACH(card_layout, job->page_layouts[page - 1]) {
int end = min((int)job->cards.size(), start + job->layout.cards_per_page()); drawCard(dc, card_layout);
for (int i = start ; i < end ; ++i) {
drawCard(dc, job->cards.at(i), i - start);
} }
return true; return true;
} }
void CardsPrintout::drawCard(DC& dc, const CardP& card, int card_nr) { void CardsPrintout::drawCard(DC& dc, const PrintJob::CardLayout& card_layout) {
const StyleSheet& stylesheet = job->set->stylesheetFor(card); const StyleSheet& stylesheet = job->set->stylesheetFor(card_layout.card);
// determine dpi factor for this card // draw card to its own buffer
double px_per_mm = stylesheet.card_dpi / 25.4;
// determine position
int col = card_nr % job->layout.cols;
int row = card_nr / job->layout.cols;
RealPoint pos((job->layout.margin_left + (job->layout.card_size.width + job->layout.card_spacing.width) * col) / scale_x * px_per_mm
,(job->layout.margin_top + (job->layout.card_size.height + job->layout.card_spacing.height) * row) / scale_y * px_per_mm);
// determine rotation
Radians rotation = 0;
if ((stylesheet.card_width > stylesheet.card_height) != job->layout.card_landscape) {
rotation = rad90;
}
/*
// size of this particular card (in mm)
RealSize card_size( stylesheet.card_width * 25.4 / stylesheet.card_dpi
, stylesheet.card_height * 25.4 / stylesheet.card_dpi);
if (is_rad90(rotation)) swap(card_size.width, card_size.height);
// adjust card size, to center card in the available space (from job->layout.card_size)?
// TODO: deal with different sized cards in general
*/
// create buffers
int w = int(stylesheet.card_width), h = int(stylesheet.card_height); // in pixels int w = int(stylesheet.card_width), h = int(stylesheet.card_height); // in pixels
if (is_rad90(rotation)) swap(w,h); if (is_rad90(card_layout.rotation)) swap(w,h);
// Draw using text buffer
wxBitmap buffer(w,h,32); wxBitmap buffer(w,h,32);
wxMemoryDC bufferDC; wxMemoryDC bufferDC;
bufferDC.SelectObject(buffer); bufferDC.SelectObject(buffer);
clearDC(bufferDC,*wxWHITE_BRUSH); clearDC(bufferDC,*wxWHITE_BRUSH);
RotatedDC rdc(bufferDC, rotation, stylesheet.getCardRect(), 1.0, QUALITY_AA, ROTATION_ATTACH_TOP_LEFT); RotatedDC rdc(bufferDC, card_layout.rotation, stylesheet.getCardRect(), 1.0, QUALITY_AA, ROTATION_ATTACH_TOP_LEFT);
// render card to dc viewer.setCard(card_layout.card);
viewer.setCard(card);
viewer.draw(rdc, *wxWHITE); viewer.draw(rdc, *wxWHITE);
// render buffer to device
dc.SetUserScale(scale_x / px_per_mm, scale_y / px_per_mm);
bufferDC.SelectObject(wxNullBitmap); bufferDC.SelectObject(wxNullBitmap);
dc.DrawBitmap(buffer, int(scale_x * pos.x), int(scale_y * pos.y)); // draw card buffer to page dc
double px_per_mm = stylesheet.card_dpi / 25.4;
dc.SetUserScale(scale_x / px_per_mm, scale_y / px_per_mm);
dc.DrawBitmap(buffer, int(card_layout.pos.width * px_per_mm), int(card_layout.pos.height * px_per_mm));
} }
// ----------------------------------------------------------------------------- : PrintWindow // ----------------------------------------------------------------------------- : PrintWindow
PrintJobP make_print_job(Window* parent, const SetP& set, const ExportCardSelectionChoices& choices) { PrintJobP make_print_job(Window* parent, const SetP& set, const ExportCardSelectionChoices& choices) {
// Let the user choose cards // Let the user choose cards and spacing
// controls // controls
ExportWindowBase wnd(parent, _TITLE_("select cards print"), set, choices); ExportWindowBase wnd(parent, _TITLE_("select cards print"), set, choices);
wxCheckBox* space = new wxCheckBox(&wnd, wxID_ANY, _LABEL_("put space between cards")); wxFloatingPointValidator<float> validator(2, NULL, wxNUM_VAL_ZERO_AS_BLANK);
space->SetValue(settings.print_layout); validator.SetRange(0, 100);
wxTextCtrl* space = new wxTextCtrl(&wnd, wxID_ANY, _(""), wxDefaultPosition, wxDefaultSize, 0L, validator);
space->SetValue(wxString::Format(wxT("%lf"), settings.print_spacing));
// layout // layout
wxSizer* s = new wxBoxSizer(wxVERTICAL); wxSizer* s = new wxBoxSizer(wxVERTICAL);
wxSizer* s2 = new wxBoxSizer(wxHORIZONTAL); wxSizer* s2 = new wxBoxSizer(wxHORIZONTAL);
wxSizer* s3 = wnd.Create(); wxSizer* s3 = wnd.Create();
s2->Add(s3, 1, wxEXPAND | wxALL, 8); s2->Add(s3, 1, wxEXPAND | wxALL, 8);
wxSizer* s4 = new wxStaticBoxSizer(wxVERTICAL, &wnd, _TITLE_("settings")); wxSizer* s4 = new wxStaticBoxSizer(wxVERTICAL, &wnd, _TITLE_("settings"));
s4->Add(space, 1, wxALL | wxALIGN_TOP, 8); s4->Add(new wxStaticText(&wnd, -1, _LABEL_("spacing print")), 0, wxALL, 8);
s4->Add(space, 0, wxALL & ~wxTOP, 8);
s2->Add(s4, 1, wxEXPAND | (wxALL & ~wxLEFT), 8); s2->Add(s4, 1, wxEXPAND | (wxALL & ~wxLEFT), 8);
s->Add(s2, 1, wxEXPAND); s->Add(s2, 1, wxEXPAND);
s->Add(wnd.CreateButtonSizer(wxOK | wxCANCEL) , 0, wxEXPAND | wxALL, 8); s->Add(wnd.CreateButtonSizer(wxOK | wxCANCEL) , 0, wxEXPAND | wxALL, 8);
@@ -185,10 +275,11 @@ PrintJobP make_print_job(Window* parent, const SetP& set, const ExportCardSelect
if (wnd.ShowModal() != wxID_OK) { if (wnd.ShowModal() != wxID_OK) {
return PrintJobP(); // cancel return PrintJobP(); // cancel
} else { } else {
// make print job // make print job
PrintJobP job = make_intrusive<PrintJob>(set); double spacing;
job->layout_type = settings.print_layout = space->GetValue() ? LAYOUT_EQUAL_SPACE : LAYOUT_NO_SPACE; space->GetValue().ToDouble(&spacing);
job->cards = wnd.getSelection(); settings.print_spacing = spacing;
PrintJobP job = make_intrusive<PrintJob>(set, wnd.getSelection(), spacing);
return job; return job;
} }
} }
+45 -37
View File
@@ -11,52 +11,60 @@
#include <util/prec.hpp> #include <util/prec.hpp>
#include <util/reflect.hpp> #include <util/reflect.hpp>
#include <util/real_point.hpp> #include <util/real_point.hpp>
#include <data/settings.hpp>
#include <gui/card_select_window.hpp> #include <gui/card_select_window.hpp>
#include <data/set.hpp>
#include <data/stylesheet.hpp>
DECLARE_POINTER_TYPE(Set); DECLARE_POINTER_TYPE(Set);
DECLARE_POINTER_TYPE(PrintJob); DECLARE_POINTER_TYPE(PrintJob);
class StyleSheet;
// ----------------------------------------------------------------------------- : Layout // ----------------------------------------------------------------------------- : Job
/// Layout of a page of cards
class PageLayout {
public:
// layout
RealSize page_size; ///< Size of a page (in millimetres)
RealSize card_size; ///< Size of a card (in millimetres)
RealSize card_spacing; ///< Spacing between cards (in millimetres)
double card_dpi; ///< Dots per inch of the default stylesheet
double margin_left, margin_right, margin_top, margin_bottom; ///< Page margins (in millimetres)
int rows, cols; ///< Number of rows/columns of cards
bool card_landscape; ///< Are cards rotated to landscape orientation?
PageLayout();
void init(const StyleSheet& stylesheet, PageLayoutType layout_type, const RealSize& page_size);
/// Is this layout uninitialized?
inline bool empty() const { return cards_per_page() == 0; }
/// The number of cards per page
inline int cards_per_page() const { return rows * cols; }
};
class PrintJob : public IntrusivePtrBase<PrintJob> { class PrintJob : public IntrusivePtrBase<PrintJob> {
public: public:
PrintJob(SetP const& set) : set(set) {} PrintJob(const SetP& set, const vector<CardP>& cards, double spacing) : set(set), cards(cards), spacing(spacing) {
default_size.width = set->stylesheet->card_width * 25.4 / set->stylesheet->card_dpi;
// set and cards to print default_size.height = set->stylesheet->card_height * 25.4 / set->stylesheet->card_dpi;
SetP set;
vector<CardP> cards;
// printing options
PageLayoutType layout_type;
PageLayout layout;
inline int num_pages() const {
int cards_per_page = max(1,layout.cards_per_page());
return ((int)cards.size() + cards_per_page - 1) / cards_per_page;
} }
SetP set;
vector<CardP> cards; ///< Cards selected by the user for print
RealSize default_size; ///< Size of a card with the default stylesheet in millimetres
double spacing; ///< Spacing between cards in millimetres
struct CardLayout {
CardLayout(const CardP& card, const RealSize& size, Radians rotation)
: card(card), size(size), rotation(rotation) {}
bool operator<(const CardLayout& that) const {
return size.width > that.size.width; // put the widest cards first
}
CardP card;
RealSize size;
Radians rotation;
RealSize pos;
};
void init(const RealSize& page_size);
RealSize page_size; ///< Size of a page in millimetres
vector<CardLayout> card_layouts; ///< Locations of the cards on the pages
vector<vector<CardLayout>> page_layouts; ///< The CardLayout grouped by page
/// Is this job uninitialized?
inline bool empty() const { return page_layouts.empty(); }
private:
// calculate the width and height of each card in millimeters
void measure_cards();
// calculate where the cards go on the pages
void layout_cards();
// if two cards are almost aligned, align them
void align_cards();
// center the cards on the middle of each page
void center_cards();
}; };
// ----------------------------------------------------------------------------- : Printing // ----------------------------------------------------------------------------- : Printing