diff --git a/src/data/settings.cpp b/src/data/settings.cpp index da8b68d0..8d60d60d 100644 --- a/src/data/settings.cpp +++ b/src/data/settings.cpp @@ -153,13 +153,6 @@ IMPLEMENT_REFLECTION_NO_SCRIPT(StyleSheetSettings) { 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; @@ -174,8 +167,8 @@ Settings::Settings() , symbol_grid_size (30) , symbol_grid (true) , symbol_grid_snap (false) - , print_layout (LAYOUT_NO_SPACE) - , internal_scale (1.0) + , print_spacing (0.33) + , internal_scale (1.0) , internal_image_extension(true) #if USE_OLD_STYLE_UPDATE_CHECKER , updates_url (_("https://magicseteditor.boards.net/page/downloads")) @@ -263,9 +256,9 @@ IMPLEMENT_REFLECTION_NO_SCRIPT(Settings) { REFLECT(symbol_grid); REFLECT(symbol_grid_snap); REFLECT(default_game); - REFLECT(print_layout); - REFLECT(apprentice_location); - REFLECT(internal_scale); + REFLECT(print_spacing); + REFLECT(apprentice_location); + REFLECT(internal_scale); REFLECT(internal_image_extension); #if USE_OLD_STYLE_UPDATE_CHECKER REFLECT(updates_url); diff --git a/src/data/settings.hpp b/src/data/settings.hpp index 7d022df0..0802c503 100644 --- a/src/data/settings.hpp +++ b/src/data/settings.hpp @@ -112,13 +112,6 @@ public: DECLARE_REFLECTION(); }; -// ----------------------------------------------------------------------------- : Printing settings - -enum PageLayoutType -{ LAYOUT_NO_SPACE -, LAYOUT_EQUAL_SPACE -//, LAYOUT_CUSTOM -}; // ----------------------------------------------------------------------------- : Settings @@ -188,7 +181,7 @@ public: // --------------------------------------------------- : Printing - PageLayoutType print_layout; + double print_spacing; // --------------------------------------------------- : Special game stuff String apprentice_location; diff --git a/src/gui/print_window.cpp b/src/gui/print_window.cpp index 6d36b6f2..0aec84cd 100644 --- a/src/gui/print_window.cpp +++ b/src/gui/print_window.cpp @@ -14,40 +14,149 @@ #include #include #include -#include +#include +#include +#include DECLARE_POINTER_TYPE(PageLayout); // ----------------------------------------------------------------------------- : Layout -PageLayout::PageLayout() - : margin_left(0), margin_right(0), margin_top(0), margin_bottom(0) - , rows(0), cols(0), card_landscape(false) -{} - -void PageLayout::init(const StyleSheet& stylesheet, PageLayoutType type, const RealSize& page_size) { - this->page_size = page_size; - 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::init(const RealSize& page_size) { + this->page_size = page_size; + if (cards.empty()) return; + measure_cards(); + layout_cards(); + align_cards(); + center_cards(); } +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()); + double row_top = 0.0, row_height = 0.0, row_width = 0.0; + unordered_set 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()); + 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& 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 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& 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 @@ -70,11 +179,11 @@ private: double scale_x, scale_y; // priter pixel per mm 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 - void drawCard(DC& dc, const CardP& card, int card_nr); + /// Draw a card according to it's CardLayout info + void drawCard(DC& dc, const PrintJob::CardLayout& card_layout); }; CardsPrintout::CardsPrintout(PrintJobP const& job) @@ -93,88 +202,69 @@ bool CardsPrintout::HasPage(int page) { } void CardsPrintout::OnPreparePrinting() { - if (job->layout.empty()) { + if (job->empty()) { int 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) { DC& dc = *GetDC(); - // scale factors (pixels per mm) + // page size in millimeters int pw_mm, ph_mm; GetPageSizeMM(&pw_mm, &ph_mm); + // page size in pixels int pw_px, ph_px; dc.GetSize(&pw_px, &ph_px); + // scale factors (pixels per mm) scale_x = (double)pw_px / pw_mm; scale_y = (double)ph_px / ph_mm; - // print the cards that belong on this page - int start = (page - 1) * job->layout.cards_per_page(); - int end = min((int)job->cards.size(), start + job->layout.cards_per_page()); - for (int i = start ; i < end ; ++i) { - drawCard(dc, job->cards.at(i), i - start); + // print the cards that belong on this page + FOR_EACH(card_layout, job->page_layouts[page - 1]) { + drawCard(dc, card_layout); } return true; } -void CardsPrintout::drawCard(DC& dc, const CardP& card, int card_nr) { - const StyleSheet& stylesheet = job->set->stylesheetFor(card); - // determine dpi factor for this card - 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 +void CardsPrintout::drawCard(DC& dc, const PrintJob::CardLayout& card_layout) { + const StyleSheet& stylesheet = job->set->stylesheetFor(card_layout.card); + // draw card to its own buffer int w = int(stylesheet.card_width), h = int(stylesheet.card_height); // in pixels - if (is_rad90(rotation)) swap(w,h); - // Draw using text buffer + if (is_rad90(card_layout.rotation)) swap(w,h); wxBitmap buffer(w,h,32); wxMemoryDC bufferDC; bufferDC.SelectObject(buffer); clearDC(bufferDC,*wxWHITE_BRUSH); - RotatedDC rdc(bufferDC, rotation, stylesheet.getCardRect(), 1.0, QUALITY_AA, ROTATION_ATTACH_TOP_LEFT); - // render card to dc - viewer.setCard(card); + RotatedDC rdc(bufferDC, card_layout.rotation, stylesheet.getCardRect(), 1.0, QUALITY_AA, ROTATION_ATTACH_TOP_LEFT); + viewer.setCard(card_layout.card); viewer.draw(rdc, *wxWHITE); - // render buffer to device - dc.SetUserScale(scale_x / px_per_mm, scale_y / px_per_mm); 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 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 - ExportWindowBase wnd(parent, _TITLE_("select cards print"), set, choices); - wxCheckBox* space = new wxCheckBox(&wnd, wxID_ANY, _LABEL_("put space between cards")); - space->SetValue(settings.print_layout); + ExportWindowBase wnd(parent, _TITLE_("select cards print"), set, choices); + wxFloatingPointValidator validator(2, NULL, wxNUM_VAL_ZERO_AS_BLANK); + 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 wxSizer* s = new wxBoxSizer(wxVERTICAL); wxSizer* s2 = new wxBoxSizer(wxHORIZONTAL); wxSizer* s3 = wnd.Create(); s2->Add(s3, 1, wxEXPAND | wxALL, 8); 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); s->Add(s2, 1, wxEXPAND); 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) { return PrintJobP(); // cancel } else { - // make print job - PrintJobP job = make_intrusive(set); - job->layout_type = settings.print_layout = space->GetValue() ? LAYOUT_EQUAL_SPACE : LAYOUT_NO_SPACE; - job->cards = wnd.getSelection(); + // make print job + double spacing; + space->GetValue().ToDouble(&spacing); + settings.print_spacing = spacing; + PrintJobP job = make_intrusive(set, wnd.getSelection(), spacing); return job; } } diff --git a/src/gui/print_window.hpp b/src/gui/print_window.hpp index bdcf4a78..09438b6d 100644 --- a/src/gui/print_window.hpp +++ b/src/gui/print_window.hpp @@ -11,52 +11,60 @@ #include #include #include -#include #include +#include +#include DECLARE_POINTER_TYPE(Set); DECLARE_POINTER_TYPE(PrintJob); -class StyleSheet; -// ----------------------------------------------------------------------------- : Layout - -/// 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; } -}; +// ----------------------------------------------------------------------------- : Job class PrintJob : public IntrusivePtrBase { public: - PrintJob(SetP const& set) : set(set) {} - - // set and cards to print - SetP set; - vector 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; + PrintJob(const SetP& set, const vector& cards, double spacing) : set(set), cards(cards), spacing(spacing) { + default_size.width = set->stylesheet->card_width * 25.4 / set->stylesheet->card_dpi; + default_size.height = set->stylesheet->card_height * 25.4 / set->stylesheet->card_dpi; } + + SetP set; + vector 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 card_layouts; ///< Locations of the cards on the pages + vector> 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