From be7428d66ecb0827676f230db86fad6300e1cc7d Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Tue, 1 Oct 2019 13:27:58 +0200 Subject: [PATCH] sla::Raster interface clarified and covered with tests. Also renamed sla::SupportTreeAlgorithm to SupportTreeBuildsteps. --- CMakeLists.txt | 2 +- src/agg/agg_rasterizer_scanline_aa.h | 2 +- src/libslic3r/CMakeLists.txt | 4 +- src/libslic3r/SLA/SLARaster.cpp | 317 +++++++++--------- src/libslic3r/SLA/SLARaster.hpp | 142 ++++---- src/libslic3r/SLA/SLARasterWriter.cpp | 37 +- src/libslic3r/SLA/SLARasterWriter.hpp | 56 ++-- src/libslic3r/SLA/SLASupportTree.cpp | 1 - src/libslic3r/SLA/SLASupportTreeBuilder.cpp | 4 +- ...rithm.cpp => SLASupportTreeBuildsteps.cpp} | 50 +-- ...rithm.hpp => SLASupportTreeBuildsteps.hpp} | 12 +- src/libslic3r/SLAPrint.cpp | 26 +- src/libslic3r/SLAPrint.hpp | 7 +- tests/sla_print/CMakeLists.txt | 3 +- tests/sla_print/sla_print_tests_main.cpp | 182 +++++++++- 15 files changed, 484 insertions(+), 361 deletions(-) rename src/libslic3r/SLA/{SLASupportTreeAlgorithm.cpp => SLASupportTreeBuildsteps.cpp} (97%) rename src/libslic3r/SLA/{SLASupportTreeAlgorithm.hpp => SLASupportTreeBuildsteps.hpp} (96%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c7fc12df..93339eb0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,7 @@ set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux") # Proposal for C++ unit tests and sandboxes option(SLIC3R_BUILD_SANDBOXES "Build development sandboxes" OFF) -option(SLIC3R_BUILD_TESTS "Build unit tests" OFF) +option(SLIC3R_BUILD_TESTS "Build unit tests" ON) # Print out the SLIC3R_* cache options get_cmake_property(_cache_vars CACHE_VARIABLES) diff --git a/src/agg/agg_rasterizer_scanline_aa.h b/src/agg/agg_rasterizer_scanline_aa.h index ffc2ddf94..4925ca209 100644 --- a/src/agg/agg_rasterizer_scanline_aa.h +++ b/src/agg/agg_rasterizer_scanline_aa.h @@ -156,7 +156,7 @@ namespace agg //------------------------------------------------------------------- template - void add_path(VertexSource& vs, unsigned path_id=0) + void add_path(VertexSource &&vs, unsigned path_id=0) { double x; double y; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 66602e06d..fe8b0f71d 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -179,8 +179,8 @@ add_library(libslic3r STATIC SLA/SLAPad.hpp SLA/SLAPad.cpp SLA/SLASupportTreeBuilder.hpp - SLA/SLASupportTreeAlgorithm.hpp - SLA/SLASupportTreeAlgorithm.cpp + SLA/SLASupportTreeBuildsteps.hpp + SLA/SLASupportTreeBuildsteps.cpp SLA/SLASupportTreeBuilder.cpp SLA/SLAConcurrency.hpp SLA/SLASupportTree.hpp diff --git a/src/libslic3r/SLA/SLARaster.cpp b/src/libslic3r/SLA/SLARaster.cpp index 32a88b1b5..091cadd23 100644 --- a/src/libslic3r/SLA/SLARaster.cpp +++ b/src/libslic3r/SLA/SLARaster.cpp @@ -5,6 +5,7 @@ #include "SLARaster.hpp" #include "libslic3r/ExPolygon.hpp" +#include "libslic3r/MTUtils.hpp" #include // For rasterizing @@ -32,25 +33,30 @@ inline const ClipperLib::Paths& holes(const ClipperLib::Polygon& p) { return p.H namespace sla { +const Raster::TMirroring Raster::NoMirror = {false, false}; +const Raster::TMirroring Raster::MirrorX = {true, false}; +const Raster::TMirroring Raster::MirrorY = {false, true}; +const Raster::TMirroring Raster::MirrorXY = {true, true}; + + +using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24; +using TRawRenderer = agg::renderer_base; +using TPixel = TPixelRenderer::color_type; +using TRawBuffer = agg::rendering_buffer; +using TBuffer = std::vector; + +using TRendererAA = agg::renderer_scanline_aa_solid; + class Raster::Impl { public: - using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24; - using TRawRenderer = agg::renderer_base; - using TPixel = TPixelRenderer::color_type; - using TRawBuffer = agg::rendering_buffer; - - using TBuffer = std::vector; - - using TRendererAA = agg::renderer_scanline_aa_solid; static const TPixel ColorWhite; static const TPixel ColorBlack; - using Format = Raster::Format; + using Format = Raster::RawData; private: Raster::Resolution m_resolution; -// Raster::PixelDim m_pxdim; Raster::PixelDim m_pxdim_scaled; // used for scaled coordinate polygons TBuffer m_buf; TRawBuffer m_rbuf; @@ -59,74 +65,49 @@ private: TRendererAA m_renderer; std::function m_gammafn; - std::array m_mirror; - Format m_fmt = Format::PNG; + Trafo m_trafo; inline void flipy(agg::path_storage& path) const { - path.flip_y(0, m_resolution.height_px); + path.flip_y(0, double(m_resolution.height_px)); } inline void flipx(agg::path_storage& path) const { - path.flip_x(0, m_resolution.width_px); + path.flip_x(0, double(m_resolution.width_px)); } public: - - inline Impl(const Raster::Resolution& res, const Raster::PixelDim &pd, - const std::array& mirror, double gamma = 1.0): - m_resolution(res), -// m_pxdim(pd), - m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm), - m_buf(res.pixels()), - m_rbuf(reinterpret_cast(m_buf.data()), - res.width_px, res.height_px, - int(res.width_px*TPixelRenderer::num_components)), - m_pixfmt(m_rbuf), - m_raw_renderer(m_pixfmt), - m_renderer(m_raw_renderer), - m_mirror(mirror) + inline Impl(const Raster::Resolution & res, + const Raster::PixelDim & pd, + const Trafo &trafo) + : m_resolution(res) + , m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm) + , m_buf(res.pixels()) + , m_rbuf(reinterpret_cast(m_buf.data()), + unsigned(res.width_px), + unsigned(res.height_px), + int(res.width_px * TPixelRenderer::num_components)) + , m_pixfmt(m_rbuf) + , m_raw_renderer(m_pixfmt) + , m_renderer(m_raw_renderer) + , m_trafo(trafo) { m_renderer.color(ColorWhite); - if(gamma > 0) m_gammafn = agg::gamma_power(gamma); + if (trafo.gamma > 0) m_gammafn = agg::gamma_power(trafo.gamma); else m_gammafn = agg::gamma_threshold(0.5); clear(); } - - inline Impl(const Raster::Resolution& res, - const Raster::PixelDim &pd, - Format fmt, - double gamma = 1.0): - Impl(res, pd, {false, false}, gamma) - { - switch (fmt) { - case Format::PNG: m_mirror = {false, true}; break; - case Format::RAW: m_mirror = {false, false}; break; - } - m_fmt = fmt; - } template void draw(const P &poly) { agg::rasterizer_scanline_aa<> ras; agg::scanline_p8 scanlines; ras.gamma(m_gammafn); - - auto&& path = to_path(contour(poly)); - if(m_mirror[X]) flipx(path); - if(m_mirror[Y]) flipy(path); - - ras.add_path(path); - - for(auto& h : holes(poly)) { - auto&& holepath = to_path(h); - if(m_mirror[X]) flipx(holepath); - if(m_mirror[Y]) flipy(holepath); - ras.add_path(holepath); - } - + ras.add_path(to_path(contour(poly))); + for(auto& h : holes(poly)) ras.add_path(to_path(h)); + agg::render_scanlines(ras, scanlines, m_renderer); } @@ -135,11 +116,16 @@ public: } inline TBuffer& buffer() { return m_buf; } + inline const TBuffer& buffer() const { return m_buf; } - inline Format format() const { return m_fmt; } inline const Raster::Resolution resolution() { return m_resolution; } - + inline const Raster::PixelDim pixdim() + { + return {SCALING_FACTOR / m_pxdim_scaled.w_mm, + SCALING_FACTOR / m_pxdim_scaled.h_mm}; + } + private: inline double getPx(const Point& p) { return p(0) * m_pxdim_scaled.w_mm; @@ -162,49 +148,67 @@ private: return p.Y * m_pxdim_scaled.h_mm; } - template agg::path_storage to_path(const PointVec& poly) + template agg::path_storage _to_path(const PointVec& v) { agg::path_storage path; - auto it = poly.begin(); + auto it = v.begin(); path.move_to(getPx(*it), getPy(*it)); + while(++it != v.end()) path.line_to(getPx(*it), getPy(*it)); + path.line_to(getPx(v.front()), getPy(v.front())); + + return path; + } + + template agg::path_storage _to_path_flpxy(const PointVec& v) + { + agg::path_storage path; + + auto it = v.begin(); + path.move_to(getPy(*it), getPx(*it)); + while(++it != v.end()) path.line_to(getPy(*it), getPx(*it)); + path.line_to(getPy(v.front()), getPx(v.front())); + + return path; + } + + template agg::path_storage to_path(const PointVec &v) + { + auto path = m_trafo.flipXY ? _to_path_flpxy(v) : _to_path(v); + + path.translate_all_paths(m_trafo.origin_x * m_pxdim_scaled.w_mm, + m_trafo.origin_y * m_pxdim_scaled.h_mm); + + if(m_trafo.mirror_x) flipx(path); + if(m_trafo.mirror_y) flipy(path); - while(++it != poly.end()) - path.line_to(getPx(*it), getPy(*it)); - - path.line_to(getPx(poly.front()), getPy(poly.front())); return path; } }; -const Raster::Impl::TPixel Raster::Impl::ColorWhite = Raster::Impl::TPixel(255); -const Raster::Impl::TPixel Raster::Impl::ColorBlack = Raster::Impl::TPixel(0); +const TPixel Raster::Impl::ColorWhite = TPixel(255); +const TPixel Raster::Impl::ColorBlack = TPixel(0); + +Raster::Raster() { reset(); } + +Raster::Raster(const Raster::Resolution &r, + const Raster::PixelDim & pd, + const Raster::Trafo & tr) +{ + reset(r, pd, tr); +} -template<> Raster::Raster() { reset(); }; Raster::~Raster() = default; -// Raster::Raster(Raster &&m) = default; -// Raster& Raster::operator=(Raster&&) = default; - -// FIXME: remove after migrating to higher version of windows compiler -Raster::Raster(Raster &&m): m_impl(std::move(m.m_impl)) {} -Raster& Raster::operator=(Raster &&m) { - m_impl = std::move(m.m_impl); return *this; -} +Raster::Raster(Raster &&m) = default; +Raster &Raster::operator=(Raster &&) = default; void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd, - Format fmt, double gamma) + const Trafo &trafo) { m_impl.reset(); - m_impl.reset(new Impl(r, pd, fmt, gamma)); -} - -void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd, - const std::array& mirror, double gamma) -{ - m_impl.reset(); - m_impl.reset(new Impl(r, pd, mirror, gamma)); + m_impl.reset(new Impl(r, pd, trafo)); } void Raster::reset() @@ -214,9 +218,16 @@ void Raster::reset() Raster::Resolution Raster::resolution() const { - if(m_impl) return m_impl->resolution(); + if (m_impl) return m_impl->resolution(); + + return Resolution{0, 0}; +} - return Resolution(0, 0); +Raster::PixelDim Raster::pixel_dimensions() const +{ + if (m_impl) return m_impl->pixdim(); + + return PixelDim{0., 0.}; } void Raster::clear() @@ -227,103 +238,83 @@ void Raster::clear() void Raster::draw(const ExPolygon &expoly) { + assert(m_impl); m_impl->draw(expoly); } void Raster::draw(const ClipperLib::Polygon &poly) { + assert(m_impl); m_impl->draw(poly); } -void Raster::save(std::ostream& stream, Format fmt) +uint8_t Raster::read_pixel(size_t x, size_t y) const { - assert(m_impl); - if(!stream.good()) return; - - switch(fmt) { - case Format::PNG: { - auto& b = m_impl->buffer(); - size_t out_len = 0; - void * rawdata = tdefl_write_image_to_png_file_in_memory( - b.data(), - int(resolution().width_px), - int(resolution().height_px), 1, &out_len); - - if(rawdata == nullptr) break; - - stream.write(static_cast(rawdata), - std::streamsize(out_len)); - - MZ_FREE(rawdata); - - break; - } - case Format::RAW: { - stream << "P5 " - << m_impl->resolution().width_px << " " - << m_impl->resolution().height_px << " " - << "255 "; - - auto sz = m_impl->buffer().size()*sizeof(Impl::TBuffer::value_type); - stream.write(reinterpret_cast(m_impl->buffer().data()), - std::streamsize(sz)); - } - } + assert (m_impl); + TPixel::value_type px; + m_impl->buffer()[y * resolution().width_px + x].get(px); + return px; } -void Raster::save(std::ostream &stream) +PNGImage & PNGImage::serialize(const Raster &raster) { - save(stream, m_impl->format()); + size_t s = 0; + m_buffer.clear(); + + void *rawdata = tdefl_write_image_to_png_file_in_memory( + get_internals(raster).buffer().data(), + int(raster.resolution().width_px), + int(raster.resolution().height_px), 1, &s); + + // On error, data() will return an empty vector. No other info can be + // retrieved from miniz anyway... + if (rawdata == nullptr) return *this; + + auto ptr = static_cast(rawdata); + + m_buffer.reserve(s); + std::copy(ptr, ptr + s, std::back_inserter(m_buffer)); + + MZ_FREE(rawdata); + return *this; } -RawBytes Raster::save(Format fmt) +std::ostream &operator<<(std::ostream &stream, const Raster::RawData &bytes) { - assert(m_impl); - - std::vector data; size_t s = 0; - - switch(fmt) { - case Format::PNG: { - void *rawdata = tdefl_write_image_to_png_file_in_memory( - m_impl->buffer().data(), - int(resolution().width_px), - int(resolution().height_px), 1, &s); - - if(rawdata == nullptr) break; - auto ptr = static_cast(rawdata); - - data.reserve(s); std::copy(ptr, ptr + s, std::back_inserter(data)); - - MZ_FREE(rawdata); - break; - } - case Format::RAW: { - auto header = std::string("P5 ") + - std::to_string(m_impl->resolution().width_px) + " " + - std::to_string(m_impl->resolution().height_px) + " " + "255 "; - - auto sz = m_impl->buffer().size()*sizeof(Impl::TBuffer::value_type); - s = sz + header.size(); - - data.reserve(s); - - auto buff = reinterpret_cast(m_impl->buffer().data()); - std::copy(header.begin(), header.end(), std::back_inserter(data)); - std::copy(buff, buff+sz, std::back_inserter(data)); - - break; - } - } - - return {std::move(data)}; + stream.write(reinterpret_cast(bytes.data()), + std::streamsize(bytes.size())); + + return stream; } -RawBytes Raster::save() +Raster::RawData::~RawData() = default; + +PPMImage & PPMImage::serialize(const Raster &raster) { - return save(m_impl->format()); + auto header = std::string("P5 ") + + std::to_string(raster.resolution().width_px) + " " + + std::to_string(raster.resolution().height_px) + " " + "255 "; + + const auto &impl = get_internals(raster); + auto sz = impl.buffer().size() * sizeof(TBuffer::value_type); + size_t s = sz + header.size(); + + m_buffer.clear(); + m_buffer.reserve(s); + + auto buff = reinterpret_cast(impl.buffer().data()); + std::copy(header.begin(), header.end(), std::back_inserter(m_buffer)); + std::copy(buff, buff+sz, std::back_inserter(m_buffer)); + + return *this; } +const Raster::Impl &Raster::RawData::get_internals(const Raster &raster) +{ + return *raster.m_impl; } -} + +} // namespace sla +} // namespace Slic3r #endif // SLARASTER_CPP diff --git a/src/libslic3r/SLA/SLARaster.hpp b/src/libslic3r/SLA/SLARaster.hpp index 8b27fd153..23b06c469 100644 --- a/src/libslic3r/SLA/SLARaster.hpp +++ b/src/libslic3r/SLA/SLARaster.hpp @@ -8,44 +8,14 @@ #include #include +#include + namespace ClipperLib { struct Polygon; } namespace Slic3r { - -class ExPolygon; - namespace sla { -// Raw byte buffer paired with its size. Suitable for compressed PNG data. -class RawBytes { - - std::vector m_buffer; -public: - - RawBytes() = default; - RawBytes(std::vector&& data): m_buffer(std::move(data)) {} - - size_t size() const { return m_buffer.size(); } - const uint8_t * data() { return m_buffer.data(); } - - RawBytes(const RawBytes&) = delete; - RawBytes& operator=(const RawBytes&) = delete; - - // ///////////////////////////////////////////////////////////////////////// - // FIXME: the following is needed for MSVC2013 compatibility - // ///////////////////////////////////////////////////////////////////////// - - // RawBytes(RawBytes&&) = default; - // RawBytes& operator=(RawBytes&&) = default; - - RawBytes(RawBytes&& mv) : m_buffer(std::move(mv.m_buffer)) {} - RawBytes& operator=(RawBytes&& mv) { - m_buffer = std::move(mv.m_buffer); - return *this; - } - - // ///////////////////////////////////////////////////////////////////////// -}; +class Raster; /** * @brief Raster captures an anti-aliased monochrome canvas where vectorial @@ -59,11 +29,29 @@ class Raster { class Impl; std::unique_ptr m_impl; public: - - /// Supported compression types - enum class Format { - RAW, //!> Uncompressed pixel data - PNG //!> PNG compression + + // Raw byte buffer paired with its size. Suitable for compressed image data. + class RawData + { + protected: + std::vector m_buffer; + const Impl& get_internals(const Raster& raster); + public: + RawData() = default; + RawData(std::vector&& data): m_buffer(std::move(data)) {} + virtual ~RawData(); + + RawData(const RawData &) = delete; + RawData &operator=(const RawData &) = delete; + + RawData(RawData &&) = default; + RawData &operator=(RawData &&) = default; + + size_t size() const { return m_buffer.size(); } + const uint8_t * data() const { return m_buffer.data(); } + + virtual RawData& serialize(const Raster &/*raster*/) { return *this; } + virtual std::string get_file_extension() const = 0; }; /// Type that represents a resolution in pixels. @@ -85,11 +73,36 @@ public: inline PixelDim(double px_width_mm = 0.0, double px_height_mm = 0.0): w_mm(px_width_mm), h_mm(px_height_mm) {} }; - - /// Constructor taking the resolution and the pixel dimension. - template Raster(Args...args) { - reset(std::forward(args)...); - } + + enum Orientation { roLandscape, roPortrait }; + + using TMirroring = std::array; + static const TMirroring NoMirror; + static const TMirroring MirrorX; + static const TMirroring MirrorY; + static const TMirroring MirrorXY; + + struct Trafo { + bool mirror_x = false, mirror_y = false, flipXY = false; + coord_t origin_x = 0, origin_y = .0; + + // If gamma is zero, thresholding will be performed which disables AA. + double gamma = 1.; + + // Portrait orientation will make sure the drawed polygons are rotated + // by 90 degrees. + Trafo(Orientation o = roLandscape, const TMirroring &mirror = NoMirror) + // XY flipping implicitly does an X mirror + : mirror_x(o == roPortrait ? !mirror[0] : mirror[0]) + , mirror_y(!mirror[1]) // Makes raster origin to be top left corner + , flipXY(o == roPortrait) + {} + }; + + Raster(); + Raster(const Resolution &r, + const PixelDim & pd, + const Trafo & tr = {}); Raster(const Raster& cpy) = delete; Raster& operator=(const Raster& cpy) = delete; @@ -98,17 +111,9 @@ public: ~Raster(); /// Reallocated everything for the given resolution and pixel dimension. - /// The third parameter is either the X, Y mirroring or a supported format - /// for which the correct mirroring will be configured. - void reset(const Resolution&, - const PixelDim&, - const std::array& mirror, - double gamma = 1.0); - void reset(const Resolution& r, - const PixelDim& pd, - Format o, - double gamma = 1.0); + const PixelDim& pd, + const Trafo &tr = {}); /** * Release the allocated resources. Drawing in this state ends in @@ -118,6 +123,7 @@ public: /// Get the resolution of the raster. Resolution resolution() const; + PixelDim pixel_dimensions() const; /// Clear the raster with black color. void clear(); @@ -125,25 +131,29 @@ public: /// Draw a polygon with holes. void draw(const ExPolygon& poly); void draw(const ClipperLib::Polygon& poly); - - // Saving the raster: - // It is possible to override the format given in the constructor but - // be aware that the mirroring will not be modified. + + uint8_t read_pixel(size_t w, size_t h) const; - /// Save the raster on the specified stream. - void save(std::ostream& stream, Format); - void save(std::ostream& stream); - - /// Save into a continuous byte stream which is returned. - RawBytes save(Format fmt); - RawBytes save(); + inline bool empty() const { return ! bool(m_impl); } + }; -// This prevents the duplicate default constructor warning on MSVC2013 -template<> Raster::Raster(); +class PNGImage: public Raster::RawData { +public: + PNGImage& serialize(const Raster &raster) override; + std::string get_file_extension() const override { return "png"; } +}; +class PPMImage: public Raster::RawData { +public: + PPMImage& serialize(const Raster &raster) override; + std::string get_file_extension() const override { return "ppm"; } +}; + +std::ostream& operator<<(std::ostream &stream, const Raster::RawData &bytes); } // sla } // Slic3r + #endif // SLARASTER_HPP diff --git a/src/libslic3r/SLA/SLARasterWriter.cpp b/src/libslic3r/SLA/SLARasterWriter.cpp index 4c2296a23..60f25ddf1 100644 --- a/src/libslic3r/SLA/SLARasterWriter.cpp +++ b/src/libslic3r/SLA/SLARasterWriter.cpp @@ -21,37 +21,12 @@ std::string RasterWriter::createIniContent(const std::string& projectname) const return out; } -void RasterWriter::flpXY(ClipperLib::Polygon &poly) -{ - for(auto& p : poly.Contour) std::swap(p.X, p.Y); - std::reverse(poly.Contour.begin(), poly.Contour.end()); - - for(auto& h : poly.Holes) { - for(auto& p : h) std::swap(p.X, p.Y); - std::reverse(h.begin(), h.end()); - } -} - -void RasterWriter::flpXY(ExPolygon &poly) -{ - for(auto& p : poly.contour.points) p = Point(p.y(), p.x()); - std::reverse(poly.contour.points.begin(), poly.contour.points.end()); - - for(auto& h : poly.holes) { - for(auto& p : h.points) p = Point(p.y(), p.x()); - std::reverse(h.points.begin(), h.points.end()); - } -} - -RasterWriter::RasterWriter(const Raster::Resolution &res, - const Raster::PixelDim &pixdim, - const std::array &mirror, - double gamma) - : m_res(res), m_pxdim(pixdim), m_mirror(mirror), m_gamma(gamma) -{ - // PNG raster will implicitly do an Y mirror - m_mirror[1] = !m_mirror[1]; -} +RasterWriter::RasterWriter(const Raster::Resolution &res, + const Raster::PixelDim & pixdim, + const Raster::Trafo & trafo, + double gamma) + : m_res(res), m_pxdim(pixdim), m_trafo(trafo), m_gamma(gamma) +{} void RasterWriter::save(const std::string &fpath, const std::string &prjname) { diff --git a/src/libslic3r/SLA/SLARasterWriter.hpp b/src/libslic3r/SLA/SLARasterWriter.hpp index e6a1fd0d1..c231655d2 100644 --- a/src/libslic3r/SLA/SLARasterWriter.hpp +++ b/src/libslic3r/SLA/SLARasterWriter.hpp @@ -15,7 +15,8 @@ namespace Slic3r { namespace sla { -// Implementation for PNG raster output +// API to write the zipped sla output layers and metadata. +// Implementation uses PNG raster output. // Be aware that if a large number of layers are allocated, it can very well // exhaust the available memory especially on 32 bit platform. // This class is designed to be used in parallel mode. Layers have an ID and @@ -25,10 +26,6 @@ namespace Slic3r { namespace sla { class RasterWriter { public: - enum Orientation { - roLandscape, - roPortrait - }; // Used for addressing parameters of set_statistics() struct PrintStatistics @@ -45,7 +42,7 @@ private: // A struct to bind the raster image data and its compressed bytes together. struct Layer { Raster raster; - RawBytes rawbytes; + PNGImage rawbytes; Layer() = default; @@ -61,22 +58,21 @@ private: // parallel. Later we can write every layer to the disk sequentially. std::vector m_layers_rst; Raster::Resolution m_res; - Raster::PixelDim m_pxdim; - std::array m_mirror; - double m_gamma; - + Raster::PixelDim m_pxdim; + Raster::Trafo m_trafo; + double m_gamma; + std::map m_config; std::string createIniContent(const std::string& projectname) const; - - static void flpXY(ClipperLib::Polygon& poly); - static void flpXY(ExPolygon& poly); public: - RasterWriter(const Raster::Resolution &res, - const Raster::PixelDim &pixdim, - const std::array &mirror, - double gamma = 1.); + + // SLARasterWriter is using Raster in custom mirroring mode + RasterWriter(const Raster::Resolution &res, + const Raster::PixelDim & pixdim, + const Raster::Trafo & trafo, + double gamma = 1.); RasterWriter(const RasterWriter& ) = delete; RasterWriter& operator=(const RasterWriter&) = delete; @@ -86,45 +82,31 @@ public: inline void layers(unsigned cnt) { if(cnt > 0) m_layers_rst.resize(cnt); } inline unsigned layers() const { return unsigned(m_layers_rst.size()); } - template void draw_polygon(const Poly& p, unsigned lyr, - Orientation o = roPortrait) + template void draw_polygon(const Poly& p, unsigned lyr) { assert(lyr < m_layers_rst.size()); - - switch (o) { - case roPortrait: { - Poly poly(p); - flpXY(poly); - m_layers_rst[lyr].raster.draw(poly); - break; - } - case roLandscape: - m_layers_rst[lyr].raster.draw(p); - break; - } + m_layers_rst[lyr].raster.draw(p); } inline void begin_layer(unsigned lyr) { if(m_layers_rst.size() <= lyr) m_layers_rst.resize(lyr+1); - m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_mirror, m_gamma); + m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_trafo); } inline void begin_layer() { m_layers_rst.emplace_back(); - m_layers_rst.front().raster.reset(m_res, m_pxdim, m_mirror, m_gamma); + m_layers_rst.front().raster.reset(m_res, m_pxdim, m_trafo); } inline void finish_layer(unsigned lyr_id) { assert(lyr_id < m_layers_rst.size()); - m_layers_rst[lyr_id].rawbytes = - m_layers_rst[lyr_id].raster.save(Raster::Format::PNG); + m_layers_rst[lyr_id].rawbytes.serialize(m_layers_rst[lyr_id].raster); m_layers_rst[lyr_id].raster.reset(); } inline void finish_layer() { if(!m_layers_rst.empty()) { - m_layers_rst.back().rawbytes = - m_layers_rst.back().raster.save(Raster::Format::PNG); + m_layers_rst.back().rawbytes.serialize(m_layers_rst.back().raster); m_layers_rst.back().raster.reset(); } } diff --git a/src/libslic3r/SLA/SLASupportTree.cpp b/src/libslic3r/SLA/SLASupportTree.cpp index 7bc003609..1faf20f34 100644 --- a/src/libslic3r/SLA/SLASupportTree.cpp +++ b/src/libslic3r/SLA/SLASupportTree.cpp @@ -8,7 +8,6 @@ #include "SLABoilerPlate.hpp" #include "SLASpatIndex.hpp" #include "SLASupportTreeBuilder.hpp" -#include "SLASupportTreeAlgorithm.hpp" #include #include diff --git a/src/libslic3r/SLA/SLASupportTreeBuilder.cpp b/src/libslic3r/SLA/SLASupportTreeBuilder.cpp index 445340010..9e1f8c1da 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuilder.cpp +++ b/src/libslic3r/SLA/SLASupportTreeBuilder.cpp @@ -1,5 +1,5 @@ #include "SLASupportTreeBuilder.hpp" -#include "SLASupportTreeAlgorithm.hpp" +#include "SLASupportTreeBuildsteps.hpp" namespace Slic3r { namespace sla { @@ -455,7 +455,7 @@ const TriangleMesh &SupportTreeBuilder::retrieve_mesh(MeshType meshtype) const bool SupportTreeBuilder::build(const SupportableMesh &sm) { ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm; - return SupportTreeAlgorithm::execute(*this, sm); + return SupportTreeBuildsteps::execute(*this, sm); } } diff --git a/src/libslic3r/SLA/SLASupportTreeAlgorithm.cpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp similarity index 97% rename from src/libslic3r/SLA/SLASupportTreeAlgorithm.cpp rename to src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp index 2f4a0c645..2404b2cdf 100644 --- a/src/libslic3r/SLA/SLASupportTreeAlgorithm.cpp +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp @@ -1,4 +1,4 @@ -#include "SLASupportTreeAlgorithm.hpp" +#include "SLASupportTreeBuildsteps.hpp" #include #include @@ -7,7 +7,7 @@ namespace Slic3r { namespace sla { -SupportTreeAlgorithm::SupportTreeAlgorithm(SupportTreeBuilder & builder, +SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm) : m_cfg(sm.cfg) , m_mesh(sm.emesh) @@ -29,12 +29,12 @@ SupportTreeAlgorithm::SupportTreeAlgorithm(SupportTreeBuilder & builder, } } -bool SupportTreeAlgorithm::execute(SupportTreeBuilder & builder, +bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, const SupportableMesh &sm) { if(sm.pts.empty()) return false; - SupportTreeAlgorithm alg(builder, sm); + SupportTreeBuildsteps alg(builder, sm); // Let's define the individual steps of the processing. We can experiment // later with the ordering and the dependencies between them. @@ -61,21 +61,21 @@ bool SupportTreeAlgorithm::execute(SupportTreeBuilder & builder, // Potentially clear up the shared data (not needed for now) }, - std::bind(&SupportTreeAlgorithm::filter, &alg), + std::bind(&SupportTreeBuildsteps::filter, &alg), - std::bind(&SupportTreeAlgorithm::add_pinheads, &alg), + std::bind(&SupportTreeBuildsteps::add_pinheads, &alg), - std::bind(&SupportTreeAlgorithm::classify, &alg), + std::bind(&SupportTreeBuildsteps::classify, &alg), - std::bind(&SupportTreeAlgorithm::routing_to_ground, &alg), + std::bind(&SupportTreeBuildsteps::routing_to_ground, &alg), - std::bind(&SupportTreeAlgorithm::routing_to_model, &alg), + std::bind(&SupportTreeBuildsteps::routing_to_model, &alg), - std::bind(&SupportTreeAlgorithm::interconnect_pillars, &alg), + std::bind(&SupportTreeBuildsteps::interconnect_pillars, &alg), - std::bind(&SupportTreeAlgorithm::routing_headless, &alg), + std::bind(&SupportTreeBuildsteps::routing_headless, &alg), - std::bind(&SupportTreeAlgorithm::merge_result, &alg), + std::bind(&SupportTreeBuildsteps::merge_result, &alg), [] () { // Done @@ -158,7 +158,7 @@ bool SupportTreeAlgorithm::execute(SupportTreeBuilder & builder, return pc == ABORT; } -EigenMesh3D::hit_result SupportTreeAlgorithm::pinhead_mesh_intersect( +EigenMesh3D::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect( const Vec3d &s, const Vec3d &dir, double r_pin, double r_back, double width) { static const size_t SAMPLES = 8; @@ -275,7 +275,7 @@ EigenMesh3D::hit_result SupportTreeAlgorithm::pinhead_mesh_intersect( return *mit; } -EigenMesh3D::hit_result SupportTreeAlgorithm::bridge_mesh_intersect( +EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect( const Vec3d &s, const Vec3d &dir, double r, bool ins_check) { static const size_t SAMPLES = 8; @@ -344,7 +344,7 @@ EigenMesh3D::hit_result SupportTreeAlgorithm::bridge_mesh_intersect( return *mit; } -bool SupportTreeAlgorithm::interconnect(const Pillar &pillar, +bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, const Pillar &nextpillar) { // We need to get the starting point of the zig-zag pattern. We have to @@ -437,7 +437,7 @@ bool SupportTreeAlgorithm::interconnect(const Pillar &pillar, return was_connected; } -bool SupportTreeAlgorithm::connect_to_nearpillar(const Head &head, +bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head, long nearpillar_id) { auto nearpillar = [this, nearpillar_id]() { @@ -514,7 +514,7 @@ bool SupportTreeAlgorithm::connect_to_nearpillar(const Head &head, return true; } -bool SupportTreeAlgorithm::search_pillar_and_connect(const Head &head) +bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &head) { PointIndex spindex = m_pillar_index.guarded_clone(); @@ -549,7 +549,7 @@ bool SupportTreeAlgorithm::search_pillar_and_connect(const Head &head) return nearest_id >= 0; } -void SupportTreeAlgorithm::create_ground_pillar(const Vec3d &jp, +void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, const Vec3d &sourcedir, double radius, long head_id) @@ -661,7 +661,7 @@ void SupportTreeAlgorithm::create_ground_pillar(const Vec3d &jp, m_pillar_index.guarded_insert(endp, unsigned(pillar_id)); } -void SupportTreeAlgorithm::filter() +void SupportTreeBuildsteps::filter() { // Get the points that are too close to each other and keep only the // first one @@ -806,7 +806,7 @@ void SupportTreeAlgorithm::filter() m_thr(); } -void SupportTreeAlgorithm::add_pinheads() +void SupportTreeBuildsteps::add_pinheads() { for (unsigned i : m_iheads) { m_thr(); @@ -822,7 +822,7 @@ void SupportTreeAlgorithm::add_pinheads() } } -void SupportTreeAlgorithm::classify() +void SupportTreeBuildsteps::classify() { // We should first get the heads that reach the ground directly PtIndices ground_head_indices; @@ -872,7 +872,7 @@ void SupportTreeAlgorithm::classify() m_cfg.max_bridges_on_pillar); } -void SupportTreeAlgorithm::routing_to_ground() +void SupportTreeBuildsteps::routing_to_ground() { const double pradius = m_cfg.head_back_radius_mm; @@ -946,7 +946,7 @@ void SupportTreeAlgorithm::routing_to_ground() } } -void SupportTreeAlgorithm::routing_to_model() +void SupportTreeBuildsteps::routing_to_model() { // We need to check if there is an easy way out to the bed surface. // If it can be routed there with a bridge shorter than @@ -1131,7 +1131,7 @@ void SupportTreeAlgorithm::routing_to_model() } } -void SupportTreeAlgorithm::interconnect_pillars() +void SupportTreeBuildsteps::interconnect_pillars() { // Now comes the algorithm that connects pillars with each other. // Ideally every pillar should be connected with at least one of its @@ -1337,7 +1337,7 @@ void SupportTreeAlgorithm::interconnect_pillars() } } -void SupportTreeAlgorithm::routing_headless() +void SupportTreeBuildsteps::routing_headless() { // For now we will just generate smaller headless sticks with a sharp // ending point that connects to the mesh surface. diff --git a/src/libslic3r/SLA/SLASupportTreeAlgorithm.hpp b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp similarity index 96% rename from src/libslic3r/SLA/SLASupportTreeAlgorithm.hpp rename to src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp index 716412a1c..b9f8db79d 100644 --- a/src/libslic3r/SLA/SLASupportTreeAlgorithm.hpp +++ b/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp @@ -142,7 +142,7 @@ IntegerOnly pairhash(I a, I b) return (DoubleI(g) << shift) + l; } -class SupportTreeAlgorithm { +class SupportTreeBuildsteps { const SupportConfig& m_cfg; const EigenMesh3D& m_mesh; const std::vector& m_support_pts; @@ -227,15 +227,9 @@ class SupportTreeAlgorithm { void create_ground_pillar(const Vec3d &jp, const Vec3d &sourcedir, double radius, - long head_id = ID_UNSET); - - SupportTreeAlgorithm(SupportTreeBuilder & builder, const SupportableMesh &sm); - + long head_id = ID_UNSET); public: - SupportTreeAlgorithm(const SupportTreeAlgorithm &) = delete; - SupportTreeAlgorithm(SupportTreeAlgorithm &&) = delete; - SupportTreeAlgorithm& operator=(const SupportTreeAlgorithm &) = delete; - SupportTreeAlgorithm& operator=(SupportTreeAlgorithm &&) = delete; + SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm); // Now let's define the individual steps of the support generation algorithm diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 9462e766c..2a1ae74d7 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -1176,9 +1176,8 @@ void SLAPrint::process() { ClipperPolygon poly; - // We need to reverse if flpXY OR is_lefthanded is true but - // not if both are true which is a logical inequality (XOR) - bool needreverse = /*flpXY !=*/ is_lefthanded; + // We need to reverse if is_lefthanded is true but + bool needreverse = is_lefthanded; // should be a move poly.Contour.reserve(polygon.contour.size() + 1); @@ -1401,11 +1400,9 @@ void SLAPrint::process() SpinMutex slck; - auto orientation = get_printer_orientation(); - // procedure to process one height level. This will run in parallel auto lvlfn = - [this, &slck, &printer, increment, &dstatus, &pst, orientation] + [this, &slck, &printer, increment, &dstatus, &pst] (unsigned level_id) { if(canceled()) return; @@ -1416,7 +1413,7 @@ void SLAPrint::process() printer.begin_layer(level_id); for(const ClipperLib::Polygon& poly : printlayer.transformed_slices()) - printer.draw_polygon(poly, level_id, orientation); + printer.draw_polygon(poly, level_id); // Finish the layer for later saving it. printer.finish_layer(level_id); @@ -1644,7 +1641,6 @@ sla::RasterWriter & SLAPrint::init_printer() sla::Raster::Resolution res; sla::Raster::PixelDim pxdim; std::array mirror; - double gamma; double w = m_printer_config.display_width.getFloat(); double h = m_printer_config.display_height.getFloat(); @@ -1654,20 +1650,18 @@ sla::RasterWriter & SLAPrint::init_printer() mirror[X] = m_printer_config.display_mirror_x.getBool(); mirror[Y] = m_printer_config.display_mirror_y.getBool(); - if (get_printer_orientation() == sla::RasterWriter::roPortrait) { + auto orientation = get_printer_orientation(); + if (orientation == sla::Raster::roPortrait) { std::swap(w, h); std::swap(pw, ph); - - // XY flipping implicitly does an X mirror - mirror[X] = !mirror[X]; } res = sla::Raster::Resolution{pw, ph}; pxdim = sla::Raster::PixelDim{w / pw, h / ph}; - - gamma = m_printer_config.gamma_correction.getFloat(); - - m_printer.reset(new sla::RasterWriter(res, pxdim, mirror, gamma)); + sla::Raster::Trafo tr{orientation, mirror}; + tr.gamma = m_printer_config.gamma_correction.getFloat(); + + m_printer.reset(new sla::RasterWriter(res, pxdim, tr)); m_printer->set_config(m_full_print_config); return *m_printer; } diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index c4eefffd7..2dc2a9040 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -461,12 +461,11 @@ private: sla::RasterWriter &init_printer(); - inline sla::RasterWriter::Orientation get_printer_orientation() const + inline sla::Raster::Orientation get_printer_orientation() const { auto ro = m_printer_config.display_orientation.getInt(); - return ro == sla::RasterWriter::roPortrait ? - sla::RasterWriter::roPortrait : - sla::RasterWriter::roLandscape; + return ro == sla::Raster::roPortrait ? sla::Raster::roPortrait : + sla::Raster::roLandscape; } friend SLAPrintObject; diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index b1a6b6de7..ba289892d 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -1,4 +1,5 @@ add_executable(sla_print_tests sla_print_tests_main.cpp) target_link_libraries(sla_print_tests test_common libslic3r ${Boost_LIBRARIES} ${TBB_LIBRARIES} ${Boost_LIBRARIES}) -gtest_discover_tests(sla_print_tests TEST_PREFIX sla_print.) +add_test(sla_print_tests sla_print_tests) +#gtest_discover_tests(sla_print_tests TEST_PREFIX sla_print.) diff --git a/tests/sla_print/sla_print_tests_main.cpp b/tests/sla_print/sla_print_tests_main.cpp index 73e193f5c..8e78322ce 100644 --- a/tests/sla_print/sla_print_tests_main.cpp +++ b/tests/sla_print/sla_print_tests_main.cpp @@ -1,5 +1,8 @@ #include +// Debug +#include + #include #include "libslic3r/libslic3r.h" @@ -8,8 +11,9 @@ #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/SLA/SLAPad.hpp" #include "libslic3r/SLA/SLASupportTreeBuilder.hpp" -#include "libslic3r/SLA/SLASupportTreeAlgorithm.hpp" +#include "libslic3r/SLA/SLASupportTreeBuildsteps.hpp" #include "libslic3r/SLA/SLAAutoSupports.hpp" +#include "libslic3r/SLA/SLARaster.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/SVG.hpp" @@ -86,7 +90,7 @@ void test_pad(const std::string & obj_filename, ASSERT_FALSE(out.model_contours.empty()); - // Create the pad geometry the model contours only + // Create the pad geometry for the model contours only Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg); check_validity(out.mesh); @@ -369,6 +373,180 @@ TEST(SLASupportGeneration, SupportsDoNotPierceModel) { test_support_model_collision(fname, supportcfg); } +TEST(SLARasterOutput, DefaultRasterShouldBeEmpty) { + sla::Raster raster; + ASSERT_TRUE(raster.empty()); +} + +TEST(SLARasterOutput, InitializedRasterShouldBeNONEmpty) { + // Default Prusa SL1 display parameters + sla::Raster::Resolution res{2560, 1440}; + sla::Raster::PixelDim pixdim{120. / res.width_px, 68. / res.height_px}; + + sla::Raster raster; + raster.reset(res, pixdim); + ASSERT_FALSE(raster.empty()); + ASSERT_EQ(raster.resolution().width_px, res.width_px); + ASSERT_EQ(raster.resolution().height_px, res.height_px); + ASSERT_DOUBLE_EQ(raster.pixel_dimensions().w_mm, pixdim.w_mm); + ASSERT_DOUBLE_EQ(raster.pixel_dimensions().h_mm, pixdim.h_mm); +} + +using TPixel = uint8_t; +static constexpr const TPixel FullWhite = 255; +static constexpr const TPixel FullBlack = 0; + +template constexpr int arraysize(const A (&)[N]) { return N; } + +static void check_raster_transformations(sla::Raster::Orientation o, + sla::Raster::TMirroring mirroring) +{ + double disp_w = 120., disp_h = 68.; + sla::Raster::Resolution res{2560, 1440}; + sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px}; + + auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)}); + sla::Raster::Trafo trafo{o, mirroring}; + trafo.origin_x = bb.center().x(); + trafo.origin_y = bb.center().y(); + + sla::Raster raster{res, pixdim, trafo}; + + // create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors) + coord_t pw = 32 * coord_t(std::ceil(scaled(pixdim.w_mm))); + coord_t ph = 32 * coord_t(std::ceil(scaled(pixdim.h_mm))); + ExPolygon box; + box.contour.points = {{-pw, -ph}, {pw, -ph}, {pw, ph}, {-pw, ph}}; + + double tr_x = scaled(20.), tr_y = tr_x; + + box.translate(tr_x, tr_y); + ExPolygon expected_box = box; + + // Now calculate the position of the translated box according to output + // trafo. + if (o == sla::Raster::Orientation::roPortrait) expected_box.rotate(PI / 2.); + + if (mirroring[X]) + for (auto &p : expected_box.contour.points) p.x() = -p.x(); + + if (mirroring[Y]) + for (auto &p : expected_box.contour.points) p.y() = -p.y(); + + raster.draw(box); + + Point expected_coords = expected_box.contour.bounding_box().center(); + double rx = unscaled(expected_coords.x() + bb.center().x()) / pixdim.w_mm; + double ry = unscaled(expected_coords.y() + bb.center().y()) / pixdim.h_mm; + auto w = size_t(std::floor(rx)); + auto h = res.height_px - size_t(std::floor(ry)); + + ASSERT_TRUE(w < res.width_px && h < res.height_px); + + auto px = raster.read_pixel(w, h); + + if (px != FullWhite) { + sla::PNGImage img; + std::fstream outf("out.png", std::ios::out); + + outf << img.serialize(raster); + } + + ASSERT_EQ(px, FullWhite); +} + +TEST(SLARasterOutput, MirroringShouldBeCorrect) { + sla::Raster::TMirroring mirrorings[] = {sla::Raster::NoMirror, + sla::Raster::MirrorX, + sla::Raster::MirrorY, + sla::Raster::MirrorXY}; + + sla::Raster::Orientation orientations[] = {sla::Raster::roLandscape, + sla::Raster::roPortrait}; + for (auto orientation : orientations) + for (auto &mirror : mirrorings) + check_raster_transformations(orientation, mirror); +} + +static ExPolygon square_with_hole(double v) +{ + ExPolygon poly; + coord_t V = scaled(v / 2.); + + poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}}; + poly.holes.emplace_back(); + V = V / 2; + poly.holes.front().points = {{-V, V}, {V, V}, {V, -V}, {-V, -V}}; + return poly; +} + +static double pixel_area(TPixel px, const sla::Raster::PixelDim &pxdim) +{ + return (pxdim.h_mm * pxdim.w_mm) * px * 1. / (FullWhite - FullBlack); +} + +static double raster_white_area(const sla::Raster &raster) +{ + if (raster.empty()) return std::nan(""); + + auto res = raster.resolution(); + double a = 0; + + for (size_t x = 0; x < res.width_px; ++x) + for (size_t y = 0; y < res.height_px; ++y) { + auto px = raster.read_pixel(x, y); + a += pixel_area(px, raster.pixel_dimensions()); + } + + return a; +} + +static double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd) +{ + auto lines = p.lines(); + double pix_err = pixel_area(FullWhite, pd) / 2.; + + // Worst case is when a line is parallel to the shorter axis of one pixel, + // when the line will be composed of the max number of pixels + double pix_l = std::min(pd.h_mm, pd.w_mm); + + double error = 0.; + for (auto &l : lines) + error += (unscaled(l.length()) / pix_l) * pix_err; + + return error; +} + +TEST(SLARasterOutput, RasterizedPolygonAreaShouldMatch) { + double disp_w = 120., disp_h = 68.; + sla::Raster::Resolution res{2560, 1440}; + sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px}; + + sla::Raster raster{res, pixdim}; + auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)}); + + ExPolygon poly = square_with_hole(10.); + poly.translate(bb.center().x(), bb.center().y()); + raster.draw(poly); + + double a = poly.area() / (scaled(1.) * scaled(1.)); + double ra = raster_white_area(raster); + double diff = std::abs(a - ra); + + ASSERT_LE(diff, predict_error(poly, pixdim)); + + raster.clear(); + poly = square_with_hole(60.); + poly.translate(bb.center().x(), bb.center().y()); + raster.draw(poly); + + a = poly.area() / (scaled(1.) * scaled(1.)); + ra = raster_white_area(raster); + diff = std::abs(a - ra); + + ASSERT_LE(diff, predict_error(poly, pixdim)); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();