diff --git a/resources/icons/align_horizontal_center.svg b/resources/icons/align_horizontal_center.svg new file mode 100644 index 0000000000..7234939204 --- /dev/null +++ b/resources/icons/align_horizontal_center.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/align_horizontal_left.svg b/resources/icons/align_horizontal_left.svg new file mode 100644 index 0000000000..1b88ee7193 --- /dev/null +++ b/resources/icons/align_horizontal_left.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/icons/align_horizontal_right.svg b/resources/icons/align_horizontal_right.svg new file mode 100644 index 0000000000..b4dffb09e0 --- /dev/null +++ b/resources/icons/align_horizontal_right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/align_vertical_bottom.svg b/resources/icons/align_vertical_bottom.svg new file mode 100644 index 0000000000..5c0a94b06e --- /dev/null +++ b/resources/icons/align_vertical_bottom.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/icons/align_vertical_center.svg b/resources/icons/align_vertical_center.svg new file mode 100644 index 0000000000..e3655be39b --- /dev/null +++ b/resources/icons/align_vertical_center.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/icons/align_vertical_top.svg b/resources/icons/align_vertical_top.svg new file mode 100644 index 0000000000..a88217696e --- /dev/null +++ b/resources/icons/align_vertical_top.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index ac7b9302e7..7b2e7404e8 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -1208,63 +1208,156 @@ std::optional Emboss::letter2glyph(const FontFile &font, return priv::get_glyph(*font_info_opt, letter, flatness); } -ExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, - const char *text, - const FontProp &font_prop, - std::function was_canceled) +int Emboss::get_line_height(const FontFile &font, const FontProp &prop) { + unsigned int font_index = prop.collection_number.value_or(0); + assert(priv::is_valid(font, font_index)); + const FontFile::Info &info = font.infos[font_index]; + int line_height = info.ascent - info.descent + info.linegap; + line_height += prop.line_gap.value_or(0); + return static_cast(line_height / SHAPE_SCALE); +} + +namespace { + +ExPolygons letter2shapes( + wchar_t letter, Point &cursor, FontFileWithCache &font_with_cache, const FontProp &font_prop, fontinfo_opt& font_info_cache) { assert(font_with_cache.has_value()); - fontinfo_opt font_info_opt; - Point cursor(0, 0); + if (!font_with_cache.has_value()) + return {}; + + Glyphs &cache = *font_with_cache.cache; + const FontFile &font = *font_with_cache.font_file; + + if (letter == '\n') { + cursor.x() = 0; + // 2d shape has opposit direction of y + cursor.y() -= get_line_height(font, font_prop); + return {}; + } + if (letter == '\t') { + // '\t' = 4*space => same as imgui + const int count_spaces = 4; + const Glyph *space = priv::get_glyph(int(' '), font, font_prop, cache, font_info_cache); + if (space == nullptr) + return {}; + cursor.x() += count_spaces * space->advance_width; + return {}; + } + if (letter == '\r') + return {}; + + int unicode = static_cast(letter); + auto it = cache.find(unicode); + + // Create glyph from font file and cache it + const Glyph *glyph_ptr = (it != cache.end()) ? &it->second : priv::get_glyph(unicode, font, font_prop, cache, font_info_cache); + if (glyph_ptr == nullptr) + return {}; + + // move glyph to cursor position + ExPolygons expolygons = glyph_ptr->shape; // copy + for (ExPolygon &expolygon : expolygons) + expolygon.translate(cursor); + + cursor.x() += glyph_ptr->advance_width; + return expolygons; +} + +// Check cancel every X letters in text +// Lower number - too much checks(slows down) +// Higher number - slows down response on cancelation +const int CANCEL_CHECK = 10; +} // namespace + +ExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, const char *text, const FontProp &font_prop, const std::function& was_canceled) +{ + std::wstring text_w = boost::nowide::widen(text); + std::vector vshapes = text2vshapes(font_with_cache, text_w, font_prop, was_canceled); + // unify to one expolygon ExPolygons result; - const FontFile& font = *font_with_cache.font_file; - unsigned int font_index = font_prop.collection_number.value_or(0); - if (!priv::is_valid(font, font_index)) return {}; - const FontFile::Info& info = font.infos[font_index]; - Glyphs& cache = *font_with_cache.cache; - std::wstring ws = boost::nowide::widen(text); - for (wchar_t wc: ws){ - if (wc == '\n') { - int line_height = info.ascent - info.descent + info.linegap; - if (font_prop.line_gap.has_value()) - line_height += *font_prop.line_gap; - line_height = static_cast(line_height / SHAPE_SCALE); - - cursor.x() = 0; - cursor.y() -= line_height; + for (ExPolygons &shapes : vshapes) { + if (shapes.empty()) continue; - } - if (wc == '\t') { - // '\t' = 4*space => same as imgui - const int count_spaces = 4; - const Glyph* space = priv::get_glyph(int(' '), font, font_prop, cache, font_info_opt); - if (space == nullptr) continue; - cursor.x() += count_spaces * space->advance_width; - continue; - } - if (wc == '\r') continue; - - int unicode = static_cast(wc); - // check cancelation only before unknown symbol - loading of symbol could be timeconsuming on slow computer and dificult fonts - auto it = cache.find(unicode); - if (it == cache.end() && was_canceled != nullptr && was_canceled()) return {}; - const Glyph *glyph_ptr = (it != cache.end())? &it->second : - priv::get_glyph(unicode, font, font_prop, cache, font_info_opt); - if (glyph_ptr == nullptr) continue; - - // move glyph to cursor position - ExPolygons expolygons = glyph_ptr->shape; // copy - for (ExPolygon &expolygon : expolygons) - expolygon.translate(cursor); - - cursor.x() += glyph_ptr->advance_width; - expolygons_append(result, std::move(expolygons)); + expolygons_append(result, std::move(shapes)); } result = Slic3r::union_ex(result); heal_shape(result); return result; } +namespace { +/// +/// Align shape against pivot +/// +/// Horizontal and vertical alignment +/// Shapes to align +/// Prerequisities: shapes are aligned left top +/// To detect end of lines +/// Height of line for align[in font points] +void align_shape(FontProp::Align type, std::vector &shape, const std::wstring &text, int line_height); +} + +std::vector Emboss::text2vshapes(FontFileWithCache &font_with_cache, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled){ + assert(font_with_cache.has_value()); + const FontFile &font = *font_with_cache.font_file; + unsigned int font_index = font_prop.collection_number.value_or(0); + if (!priv::is_valid(font, font_index)) + return {}; + + unsigned counter = 0; + Point cursor(0, 0); + + fontinfo_opt font_info_cache; + std::vector result; + result.reserve(text.size()); + for (wchar_t letter : text) { + if (++counter == CANCEL_CHECK) { + counter = 0; + if (was_canceled()) + return {}; + } + result.emplace_back(letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache)); + } + + align_shape(font_prop.align, result, text, get_line_height(font, font_prop)); + return result; +} + +#include +unsigned Emboss::get_count_lines(const std::wstring& ws) +{ + if (ws.empty()) + return 0; + + unsigned count = 1; + for (wchar_t wc : ws) + if (wc == '\n') + ++count; + return count; + + // unsigned prev_count = 0; + // for (wchar_t wc : ws) + // if (wc == '\n') + // ++prev_count; + // else + // break; + // + // unsigned post_count = 0; + // for (wchar_t wc : boost::adaptors::reverse(ws)) + // if (wc == '\n') + // ++post_count; + // else + // break; + //return count - prev_count - post_count; +} + +unsigned Emboss::get_count_lines(const std::string &text) +{ + std::wstring ws = boost::nowide::widen(text.c_str()); + return get_count_lines(ws); +} + void Emboss::apply_transformation(const FontProp &font_prop, Transform3d &transformation){ apply_transformation(font_prop.angle, font_prop.distance, transformation); } @@ -1653,6 +1746,251 @@ std::optional Emboss::OrthoProject::unproject(const Vec3d &p, double *dep return Vec2d(pp.x(), pp.y()); } +// sample slice +namespace { + +// using coor2 = int64_t; +using Coord2 = double; +using P2 = Eigen::Matrix; + +bool point_in_distance(const Coord2 &distance_sq, PolygonPoint &polygon_point, const size_t &i, const Slic3r::Polygon &polygon, bool is_first, bool is_reverse = false) +{ + size_t s = polygon.size(); + size_t ii = (i + polygon_point.index) % s; + + // second point of line + const Point &p = polygon[ii]; + Point p_d = p - polygon_point.point; + + P2 p_d2 = p_d.cast(); + Coord2 p_distance_sq = p_d2.squaredNorm(); + if (p_distance_sq < distance_sq) + return false; + + // found line + if (is_first) { + // on same line + // center also lay on line + // new point is distance moved from point by direction + polygon_point.point += p_d * sqrt(distance_sq / p_distance_sq); + return true; + } + + // line cross circle + + // start point of line + size_t ii2 = (is_reverse) ? (ii + 1) % s : (ii + s - 1) % s; + polygon_point.index = (is_reverse) ? ii : ii2; + const Point &p2 = polygon[ii2]; + + Point line_dir = p2 - p; + P2 line_dir2 = line_dir.cast(); + + Coord2 a = line_dir2.dot(line_dir2); + Coord2 b = 2 * p_d2.dot(line_dir2); + Coord2 c = p_d2.dot(p_d2) - distance_sq; + + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) { + assert(false); + // no intersection + polygon_point.point = p; + return true; + } + + // ray didn't totally miss sphere, + // so there is a solution to + // the equation. + discriminant = sqrt(discriminant); + + // either solution may be on or off the ray so need to test both + // t1 is always the smaller value, because BOTH discriminant and + // a are nonnegative. + double t1 = (-b - discriminant) / (2 * a); + double t2 = (-b + discriminant) / (2 * a); + + double t = std::min(t1, t2); + if (t < 0. || t > 1.) { + // Bad intersection + assert(false); + polygon_point.point = p; + return true; + } + + polygon_point.point = p + (t * line_dir2).cast(); + return true; +} + +void point_in_distance(int32_t distance, PolygonPoint &p, const Slic3r::Polygon &polygon) +{ + Coord2 distance_sq = static_cast(distance) * distance; + bool is_first = true; + for (size_t i = 1; i < polygon.size(); ++i) { + if (point_in_distance(distance_sq, p, i, polygon, is_first)) + return; + is_first = false; + } + // There is not point on polygon with this distance +} + +void point_in_reverse_distance(int32_t distance, PolygonPoint &p, const Slic3r::Polygon &polygon) +{ + Coord2 distance_sq = static_cast(distance) * distance; + bool is_first = true; + bool is_reverse = true; + for (size_t i = polygon.size(); i > 0; --i) { + if (point_in_distance(distance_sq, p, i, polygon, is_first, is_reverse)) + return; + is_first = false; + } + // There is not point on polygon with this distance +} +} // namespace + +// calculate rotation, need copy of polygon point +double Emboss::calculate_angle(int32_t distance, PolygonPoint polygon_point, const Polygon &polygon) +{ + PolygonPoint polygon_point2 = polygon_point; // copy + point_in_distance(distance, polygon_point, polygon); + point_in_reverse_distance(distance, polygon_point2, polygon); + + Point surface_dir = polygon_point2.point - polygon_point.point; + Point norm(-surface_dir.y(), surface_dir.x()); + Vec2d norm_d = norm.cast(); + //norm_d.normalize(); + return std::atan2(norm_d.y(), norm_d.x()); +} + +std::vector Emboss::calculate_angles(int32_t distance, const PolygonPoints& polygon_points, const Polygon &polygon) +{ + std::vector result; + result.reserve(polygon_points.size()); + for(const PolygonPoint& pp: polygon_points) + result.emplace_back(calculate_angle(distance, pp, polygon)); + return result; +} + +PolygonPoints Emboss::sample_slice(const TextLine &slice, const BoundingBoxes &bbs, double scale) +{ + // find BB in center of line + size_t first_right_index = 0; + for (const BoundingBox &bb : bbs) + if (!bb.defined) // white char do not have bb + continue; + else if (bb.min.x() < 0) + ++first_right_index; + else + break; + + PolygonPoints samples(bbs.size()); + int32_t shapes_x_cursor = 0; + + PolygonPoint cursor = slice.start; //copy + + auto create_sample = [&] //polygon_cursor, &polygon_line_index, &line_bbs, &shapes_x_cursor, &shape_scale, &em_2_polygon, &line, &offsets] + (const BoundingBox &bb, bool is_reverse) { + if (!bb.defined) + return cursor; + Point letter_center = bb.center(); + int32_t shape_distance = shapes_x_cursor - letter_center.x(); + shapes_x_cursor = letter_center.x(); + double distance_mm = shape_distance * scale; + int32_t distance_polygon = static_cast(std::round(scale_(distance_mm))); + if (is_reverse) + point_in_distance(distance_polygon, cursor, slice.polygon); + else + point_in_reverse_distance(distance_polygon, cursor, slice.polygon); + return cursor; + }; + + // calc transformation for letters on the Right side from center + bool is_reverse = true; + for (size_t index = first_right_index; index < bbs.size(); ++index) + samples[index] = create_sample(bbs[index], is_reverse); + + // calc transformation for letters on the Left side from center + if (first_right_index < bbs.size()) { + shapes_x_cursor = bbs[first_right_index].center().x(); + cursor = samples[first_right_index]; + }else{ + // only left side exists + shapes_x_cursor = 0; + cursor = slice.start; // copy + } + is_reverse = false; + for (size_t index_plus_one = first_right_index; index_plus_one > 0; --index_plus_one) { + size_t index = index_plus_one - 1; + samples[index] = create_sample(bbs[index], is_reverse); + } + return samples; +} + +namespace { +template T get_align_y_offset(FontProp::VerticalAlign align, unsigned count_lines, T line_height) +{ + if (count_lines == 0) + return 0; + + // direction of Y in 2d is from top to bottom + // zero is on base line of first line + switch (align) { + case FontProp::VerticalAlign::center: return ((count_lines - 1) / 2) * line_height + ((count_lines % 2 == 0) ? (line_height / 2) : 0); + case FontProp::VerticalAlign::bottom: return (count_lines - 1) * line_height; + case FontProp::VerticalAlign::top: // no change + default: break; + } + return 0; +} + +int32_t get_align_x_offset(FontProp::HorizontalAlign align, const BoundingBox &shape_bb, const BoundingBox &line_bb) +{ + switch (align) { + case FontProp::HorizontalAlign::right: return -shape_bb.max.x() + (shape_bb.size().x() - line_bb.size().x()); + case FontProp::HorizontalAlign::center: return -shape_bb.center().x() + (shape_bb.size().x() - line_bb.size().x()) / 2; + case FontProp::HorizontalAlign::left: // no change + default: break; + } + return 0; +} + +void align_shape(FontProp::Align type, std::vector &shapes, const std::wstring &text, int line_height) +{ + constexpr FontProp::Align no_change(FontProp::HorizontalAlign::left, FontProp::VerticalAlign::top); + if (type == no_change) + return; // no alignment + + BoundingBox shape_bb; + for (const ExPolygons& shape: shapes) + shape_bb.merge(get_extents(shape)); + + auto get_line_bb = [&](size_t j) { + BoundingBox line_bb; + for (; j < text.length() && text[j] != '\n'; ++j) + line_bb.merge(get_extents(shapes[j])); + return line_bb; + }; + + Point offset( + get_align_x_offset(type.first, shape_bb, get_line_bb(0)), + get_align_y_offset(type.second, get_count_lines(text), line_height)); + assert(shapes.size() == text.length()); + for (size_t i = 0; i < shapes.size(); ++i) { + wchar_t letter = text[i]; + if (letter == '\n'){ + offset.x() = get_align_x_offset(type.first, shape_bb, get_line_bb(i+1)); + continue; + } + ExPolygons &shape = shapes[i]; + for (ExPolygon &s : shape) + s.translate(offset); + } +} +} // namespace + +double Emboss::get_align_y_offset(FontProp::VerticalAlign align, unsigned count_lines, double line_height){ + return ::get_align_y_offset(align, count_lines, line_height); +} + #ifdef REMOVE_SPIKES #include void priv::remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp index 6e752a90d7..ca02966dbf 100644 --- a/src/libslic3r/Emboss.hpp +++ b/src/libslic3r/Emboss.hpp @@ -8,6 +8,7 @@ #include // indexed_triangle_set #include "Polygon.hpp" #include "ExPolygon.hpp" +#include "BoundingBox.hpp" #include "TextConfiguration.hpp" namespace Slic3r { @@ -108,7 +109,7 @@ namespace Emboss std::shared_ptr cache; FontFileWithCache() : font_file(nullptr), cache(nullptr) {} - FontFileWithCache(std::unique_ptr font_file) + explicit FontFileWithCache(std::unique_ptr font_file) : font_file(std::move(font_file)) , cache(std::make_shared()) {} @@ -147,7 +148,12 @@ namespace Emboss /// User defined property of the font /// Way to interupt processing /// Inner polygon cw(outer ccw) - ExPolygons text2shapes(FontFileWithCache &font, const char *text, const FontProp &font_prop, std::function was_canceled = nullptr); + ExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function &was_canceled = []() {return false;}); + std::vector text2vshapes(FontFileWithCache &font, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled = []() {return false;}); + + /// Sum of character '\n' + unsigned get_count_lines(const std::wstring &ws); + unsigned get_count_lines(const std::string &text); /// /// Fix duplicit points and self intersections in polygons. @@ -218,6 +224,24 @@ namespace Emboss /// Conversion to mm double get_text_shape_scale(const FontProp &fp, const FontFile &ff); + /// + /// Read from font file and properties height of line with spacing + /// + /// Infos for collections + /// Collection index + Additional line gap + /// Line height with spacing in ExPolygon size + int get_line_height(const FontFile &font, const FontProp &prop); + + /// + /// Calculate Vertical align + /// + /// double for mm + /// type + /// + /// + /// In same unit as line height + double get_align_y_offset(FontProp::VerticalAlign align, unsigned count_lines, double line_height); + /// /// Project spatial point /// @@ -333,6 +357,36 @@ namespace Emboss } }; + class ProjectTransform : public IProjection + { + std::unique_ptr m_core; + Transform3d m_tr; + Transform3d m_tr_inv; + double z_scale; + public: + ProjectTransform(std::unique_ptr core, const Transform3d &tr) : m_core(std::move(core)), m_tr(tr) + { + m_tr_inv = m_tr.inverse(); + z_scale = (m_tr.linear() * Vec3d::UnitZ()).norm(); + } + + // Inherited via IProject + std::pair create_front_back(const Point &p) const override + { + auto [front, back] = m_core->create_front_back(p); + return std::make_pair(m_tr * front, m_tr * back); + } + Vec3d project(const Vec3d &point) const override{ + return m_core->project(point); + } + std::optional unproject(const Vec3d &p, double *depth = nullptr) const override { + auto res = m_core->unproject(m_tr_inv * p, depth); + if (depth != nullptr) + *depth *= z_scale; + return res; + } + }; + class OrthoProject3d : public Emboss::IProject3d { // size and direction of emboss for ortho projection @@ -356,7 +410,43 @@ namespace Emboss Vec3d project(const Vec3d &point) const override; std::optional unproject(const Vec3d &p, double * depth = nullptr) const override; }; -} // namespace Emboss + /// + /// Define polygon for draw letters + /// + struct TextLine + { + // slice of object + Polygon polygon; + + // point laying on polygon closest to zero + PolygonPoint start; + + // offset of text line in volume mm + float y; + }; + using TextLines = std::vector; + + /// + /// Sample slice polygon by bounding boxes centers + /// slice start point has shape_center_x coor + /// + /// Polygon and start point[Slic3r scaled milimeters] + /// Bounding boxes of letter on one line[in font scales] + /// Scale for bbs (after multiply bb is in milimeters) + /// Sampled polygon by bounding boxes + PolygonPoints sample_slice(const TextLine &slice, const BoundingBoxes &bbs, double scale); + + /// + /// Calculate angle for polygon point + /// + /// Distance for found normal in point + /// Select point on polygon + /// Polygon know neighbor of point + /// angle(atan2) of normal in polygon point + double calculate_angle(int32_t distance, PolygonPoint polygon_point, const Polygon &polygon); + std::vector calculate_angles(int32_t distance, const PolygonPoints& polygon_points, const Polygon &polygon); + +} // namespace Emboss } // namespace Slic3r #endif // slic3r_Emboss_hpp_ diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 7e0a9338ea..133e307c41 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -161,6 +161,9 @@ static constexpr const char *LINE_GAP_ATTR = "line_gap"; static constexpr const char *LINE_HEIGHT_ATTR = "line_height"; static constexpr const char *BOLDNESS_ATTR = "boldness"; static constexpr const char *SKEW_ATTR = "skew"; +static constexpr const char *PER_GLYPH_ATTR = "per_glyph"; +static constexpr const char *HORIZONTAL_ALIGN_ATTR = "horizontal"; +static constexpr const char *VERTICAL_ALIGN_ATTR = "vertical"; static constexpr const char *COLLECTION_NUMBER_ATTR = "collection"; static constexpr const char *FONT_FAMILY_ATTR = "family"; @@ -3565,6 +3568,10 @@ void TextConfigurationSerialization::to_xml(std::stringstream &stream, const Tex stream << BOLDNESS_ATTR << "=\"" << *fp.boldness << "\" "; if (fp.skew.has_value()) stream << SKEW_ATTR << "=\"" << *fp.skew << "\" "; + if (fp.per_glyph) + stream << PER_GLYPH_ATTR << "=\"" << 1 << "\" "; + stream << HORIZONTAL_ALIGN_ATTR << "=\"" << static_cast(fp.align.first) << "\" "; + stream << VERTICAL_ALIGN_ATTR << "=\"" << static_cast(fp.align.second) << "\" "; if (fp.collection_number.has_value()) stream << COLLECTION_NUMBER_ATTR << "=\"" << *fp.collection_number << "\" "; // font descriptor @@ -3593,7 +3600,17 @@ std::optional TextConfigurationSerialization::read(const char float skew = get_attribute_value_float(attributes, num_attributes, SKEW_ATTR); if (std::fabs(skew) > std::numeric_limits::epsilon()) fp.skew = skew; - + int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) fp.use_surface = true; + int per_glyph = get_attribute_value_int(attributes, num_attributes, PER_GLYPH_ATTR); + if (per_glyph == 1) fp.per_glyph = true; + + int horizontal = get_attribute_value_int(attributes, num_attributes, HORIZONTAL_ALIGN_ATTR); + int vertical = get_attribute_value_int(attributes, num_attributes, VERTICAL_ALIGN_ATTR); + fp.align = FontProp::Align( + static_cast(horizontal), + static_cast(vertical)); + int collection_number = get_attribute_value_int(attributes, num_attributes, COLLECTION_NUMBER_ATTR); if (collection_number > 0) fp.collection_number = static_cast(collection_number); diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index e0c3958fd9..08b6da7ed1 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -268,6 +268,21 @@ bool polygons_match(const Polygon &l, const Polygon &r); Polygon make_circle(double radius, double error); Polygon make_circle_num_segments(double radius, size_t num_segments); +/// +/// Define point laying on polygon +/// keep index of polygon line and point coordinate +/// +struct PolygonPoint +{ + // index of line inside of polygon + // 0 .. from point polygon[0] to polygon[1] + size_t index; + + // Point, which lay on line defined by index + Point point; +}; +using PolygonPoints = std::vector; + } // Slic3r // start Boost diff --git a/src/libslic3r/TextConfiguration.hpp b/src/libslic3r/TextConfiguration.hpp index ef1df301ba..4bfc8f50fa 100644 --- a/src/libslic3r/TextConfiguration.hpp +++ b/src/libslic3r/TextConfiguration.hpp @@ -40,6 +40,17 @@ struct FontProp // Select index of font in collection std::optional collection_number; + // Distiguish projection per glyph + bool per_glyph; + + // NOTE: way of serialize to 3mf force that zero must be default value + enum class HorizontalAlign { left = 0, center, right }; + enum class VerticalAlign { top = 0, center, bottom }; + using Align = std::pair; + // change pivot of text + // When not set, center is used and is not stored + Align align = Align(HorizontalAlign::left, VerticalAlign::top); + [[deprecated("Back compatibility only, now it is stored EmbossProjection like depth")]] float emboss; @@ -73,14 +84,15 @@ struct FontProp /// /// Y size of text [in mm] /// Z size of text [in mm] - FontProp(float line_height = 10.f, float depth = 2.f) - : emboss(depth), size_in_mm(line_height), use_surface(false) + FontProp(float line_height = 10.f, float depth = 2.f) : emboss(depth), size_in_mm(line_height), use_surface(false), per_glyph(false) {} bool operator==(const FontProp& other) const { return char_gap == other.char_gap && line_gap == other.line_gap && + per_glyph == other.per_glyph && + align == other.align && is_approx(size_in_mm, other.size_in_mm) && is_approx(boldness, other.boldness) && is_approx(skew, other.skew); @@ -89,7 +101,7 @@ struct FontProp // undo / redo stack recovery template void save(Archive &ar) const { - ar(size_in_mm); + ar(size_in_mm, per_glyph, align.first, align.second); cereal::save(ar, char_gap); cereal::save(ar, line_gap); cereal::save(ar, boldness); @@ -98,7 +110,7 @@ struct FontProp } template void load(Archive &ar) { - ar(size_in_mm); + ar(size_in_mm, per_glyph, align.first, align.second); cereal::load(ar, char_gap); cereal::load(ar, line_gap); cereal::load(ar, boldness); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b5a40ded19..af851cc4af 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -163,6 +163,8 @@ set(SLIC3R_GUI_SOURCES GUI/SendSystemInfoDialog.hpp GUI/SurfaceDrag.cpp GUI/SurfaceDrag.hpp + GUI/TextLines.cpp + GUI/TextLines.hpp GUI/BonjourDialog.cpp GUI/BonjourDialog.hpp GUI/ButtonsDescription.cpp diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 885dfcfc9e..62ea106faf 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2601,6 +2601,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re update_object_list = true; } + // @Enrico suggest this solution to preven accessing pointer on caster without data + m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume); m_gizmos.update_data(); m_gizmos.refresh_on_off_state(); @@ -2660,7 +2662,6 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } // refresh volume raycasters for picking - m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume); for (size_t i = 0; i < m_volumes.volumes.size(); ++i) { const GLVolume* v = m_volumes.volumes[i]; assert(v->mesh_raycaster != nullptr); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index 2dde1536f9..318079ab37 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -96,9 +96,18 @@ static const struct Limits /// /// Text to emboss /// Keep actual selected style +/// Needed when transform per glyph +/// Needed for transform per glyph +/// Define type of volume - side of surface(in / out) /// Cancel for previous job /// Base data for emboss text -std::unique_ptr create_emboss_data_base(const std::string &text, StyleManager &style_manager, std::shared_ptr> &cancel); +std::unique_ptr create_emboss_data_base( + const std::string& text, + StyleManager& style_manager, + TextLinesModel& text_lines, + const Selection& selection, + ModelVolumeType type, + std::shared_ptr>& cancel); CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager &raycaster, ModelVolumeType volume_type); /// @@ -110,6 +119,23 @@ ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size /// /// Data for emboss job to create shape /// +/// Define params of text +/// Emboss / engrave +/// Mouse position which define position +/// Volume to find surface for create +/// Ability to ray cast to model +/// Per glyph transformation +/// Line height need font file/param> +/// Contain already used scene RayCasters +/// True when start creation, False when there is no hit surface by screen coor +static bool start_create_volume_on_surface_job(DataBase &emboss_data, + ModelVolumeType volume_type, + const Vec2d &screen_coor, + const GLVolume *gl_volume, + RaycastManager &raycaster, + TextLinesModel &text_lines, + /*const */ StyleManager &style_manager, + GLCanvas3D &canvas); struct TextDataBase : public DataBase { TextDataBase(DataBase &&parent, const FontFileWithCache &font_file, @@ -144,6 +170,12 @@ enum class IconType : unsigned { lock_bold, unlock, unlock_bold, + align_horizontal_left, + align_horizontal_center, + align_horizontal_right, + align_vertical_top, + align_vertical_center, + align_vertical_bottom, // automatic calc of icon's count _count }; @@ -154,6 +186,14 @@ const IconManager::Icon &get_icon(const IconManager::VIcons& icons, IconType typ // short call of Slic3r::GUI::button bool draw_button(const IconManager::VIcons& icons, IconType type, bool disable = false); +/// +/// Apply camera direction for emboss direction +/// +/// Define view vector +/// Containe Selected Model to modify +/// Keep same up vector +/// True when apply change otherwise false +static bool apply_camera_dir(const Camera &camera, GLCanvas3D &canvas, bool keep_up); struct FaceName { wxString wx_name; @@ -257,9 +297,11 @@ struct GuiCfg std::string font; std::string height; std::string depth; - std::string use_surface; // advanced + std::string use_surface; + std::string per_glyph; + std::string alignment; std::string char_gap; std::string line_gap; std::string boldness; @@ -274,6 +316,10 @@ struct GuiCfg GuiCfg create_gui_configuration(); void draw_font_preview(FaceName &face, const std::string &text, Facenames &faces, const GuiCfg &cfg, bool is_visible); +// for existing volume which is selected(could init different(to volume text) lines count when edit text) +void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* const*/ StyleManager &style_manager, unsigned count_lines=0); +// before text volume is created +void init_new_text_line(TextLinesModel &text_lines, const Transform3d& new_text_tr, const ModelObject& mo, /* const*/ StyleManager &style_manager); } // namespace priv // use private definition @@ -333,13 +379,20 @@ void GLGizmoEmboss::on_shortcut_key() { } } +namespace{ +// verify correct volume type for creation of text +bool check(ModelVolumeType volume_type) { + return volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER; +} +} + bool GLGizmoEmboss::init_create(ModelVolumeType volume_type) { // check valid volume type - if (volume_type != ModelVolumeType::MODEL_PART && - volume_type != ModelVolumeType::NEGATIVE_VOLUME && - volume_type != ModelVolumeType::PARAMETER_MODIFIER) { - BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int)volume_type; + if (!check(volume_type)){ + BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int) volume_type; return false; } @@ -406,6 +459,8 @@ bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) angle_opt = angle; m_style_manager.get_style().angle = angle_opt; } + + volume_transformation_changing(); } return used; } @@ -425,15 +480,9 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) bool is_dragging = m_surface_drag.has_value(); // End with surface dragging? - if (was_dragging && !is_dragging) { - // Update surface by new position - if (m_volume->emboss_shape->projection.use_surface) - process(); - - // Show correct value of height & depth inside of inputs - calculate_scale(); - } - + if (was_dragging && !is_dragging) + volume_transformation_changed(); + // Start with dragging else if (!was_dragging && is_dragging) { // Cancel job to prevent interuption of dragging (duplicit result) @@ -454,8 +503,10 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) if (gl_volume == nullptr || !m_style_manager.is_active_font()) return res; - m_style_manager.get_style().angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); + m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); } + + volume_transformation_changing(); } return res; } @@ -556,6 +607,36 @@ bool GLGizmoEmboss::on_mouse(const wxMouseEvent &mouse_event) return false; } +void GLGizmoEmboss::volume_transformation_changing() +{ + if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + assert(false); + return; + } + const FontProp &prop = m_volume->text_configuration->style.prop; + if (prop.per_glyph) + init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, m_text_lines.get_lines().size()); +} + +void GLGizmoEmboss::volume_transformation_changed() +{ + if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + assert(false); + return; + } + + const FontProp &prop = m_volume->text_configuration->style.prop; + if (prop.per_glyph) + init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, m_text_lines.get_lines().size()); + + // Update surface by new position + if (prop.use_surface || prop.per_glyph) + process(); + + // Show correct value of height & depth inside of inputs + calculate_scale(); +} + bool GLGizmoEmboss::wants_enter_leave_snapshots() const { return true; } std::string GLGizmoEmboss::get_gizmo_entering_text() const { return _u8L("Enter emboss gizmo"); } std::string GLGizmoEmboss::get_gizmo_leaving_text() const { return _u8L("Leave emboss gizmo"); } @@ -591,6 +672,18 @@ void GLGizmoEmboss::on_render() { // prevent get local coordinate system on multi volumes if (!selection.is_single_volume_or_modifier() && !selection.is_single_volume_instance()) return; + + const GLVolume *gl_volume_ptr = m_parent.get_selection().get_first_volume(); + if (gl_volume_ptr == nullptr) return; + + if (m_text_lines.is_init()) { + const Transform3d& tr = gl_volume_ptr->world_matrix(); + const auto &fix = m_volume->text_configuration->fix_3mf_tr; + if (fix.has_value()) + m_text_lines.render(tr * fix->inverse()); + else + m_text_lines.render(tr); + } bool is_surface_dragging = m_surface_drag.has_value(); bool is_parent_dragging = m_parent.is_mouse_dragging(); @@ -831,9 +924,7 @@ void GLGizmoEmboss::on_stop_dragging() m_rotate_start_angle.reset(); - // recalculate for surface cut - if (m_style_manager.get_style().projection.use_surface) - process(); + volume_transformation_changed(); } void GLGizmoEmboss::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } @@ -954,6 +1045,125 @@ std::optional get_installed_face_name(const std::optional } } // namespace +namespace { + +bool get_line_height_offset(/* const*/ StyleManager &style_manager, double &line_height_mm, double &line_offset_mm) +{ + assert(style_manager.is_active_font()); + if (!style_manager.is_active_font()) + return false; + const auto &ffc = style_manager.get_font_file_with_cache(); + assert(ffc.has_value()); + if (!ffc.has_value()) + return false; + const auto &ff_ptr = ffc.font_file; + assert(ff_ptr != nullptr); + if (ff_ptr == nullptr) + return false; + const FontProp &fp = style_manager.get_font_prop(); + const FontFile &ff = *ff_ptr; + + double third_ascent_shape_size = ff.infos[fp.collection_number.value_or(0)].ascent / 3.; + int line_height_shape_size = get_line_height(ff, fp); // In shape size + + double scale = get_shape_scale(fp, ff); + line_offset_mm = third_ascent_shape_size * scale / SHAPE_SCALE; + line_height_mm = line_height_shape_size * scale; + + if (line_height_mm < 0) + return false; + + // fix for bad filled ascent in font file + if (line_offset_mm <= 0) + line_offset_mm = line_height_mm / 3; + + return true; +} + +void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* const*/ StyleManager &style_manager, unsigned count_lines) +{ + const GLVolume *gl_volume_ptr = selection.get_first_volume(); + if (gl_volume_ptr == nullptr) + return; + const GLVolume &gl_volume = *gl_volume_ptr; + const ModelObjectPtrs &objects = selection.get_model()->objects; + const ModelObject *mo_ptr = get_model_object(gl_volume, objects); + if (mo_ptr == nullptr) + return; + const ModelObject &mo = *mo_ptr; + + const ModelVolume *mv_ptr = get_model_volume(gl_volume, objects); + if (mv_ptr == nullptr) + return; + const ModelVolume &mv = *mv_ptr; + if (mv.is_the_only_one_part()) + return; + + const std::optional &tc_opt = mv.text_configuration; + if (!tc_opt.has_value()) + return; + const TextConfiguration &tc = *tc_opt; + + // calculate count lines when not set + if (count_lines == 0) { + count_lines = get_count_lines(tc.text); + if (count_lines == 0) + return; + } + + // prepare volumes to slice + ModelVolumePtrs volumes; + volumes.reserve(mo.volumes.size()); + for (ModelVolume *volume : mo.volumes) { + // only part could be surface for volumes + if (!volume->is_model_part()) + continue; + + // is selected volume + if (mv.id() == volume->id()) + continue; + + volumes.push_back(volume); + } + + // For interactivity during drag over surface it must be from gl_volume not volume. + Transform3d mv_trafo = gl_volume.get_volume_transformation().get_matrix(); + if (tc.fix_3mf_tr.has_value()) + mv_trafo = mv_trafo * (tc.fix_3mf_tr->inverse()); + FontProp::VerticalAlign align = style_manager.get_font_prop().align.second; + double line_height_mm, line_offset_mm; + if (!get_line_height_offset(style_manager, line_height_mm, line_offset_mm)) + return; + + text_lines.init(mv_trafo, volumes, align, line_height_mm, line_offset_mm, count_lines); +} + +void init_new_text_line(TextLinesModel &text_lines, const Transform3d& new_text_tr, const ModelObject& mo, /* const*/ StyleManager &style_manager) +{ + // prepare volumes to slice + ModelVolumePtrs volumes; + volumes.reserve(mo.volumes.size()); + for (ModelVolume *volume : mo.volumes) { + // only part could be surface for volumes + if (!volume->is_model_part()) + continue; + volumes.push_back(volume); + } + + FontProp::VerticalAlign align = style_manager.get_font_prop().align.second; + double line_height_mm, line_offset_mm; + if (!get_line_height_offset(style_manager, line_height_mm, line_offset_mm)) + return; + unsigned count_lines = 1; + text_lines.init(new_text_tr, volumes, align, line_height_mm, line_offset_mm, count_lines); +} + +} + +void GLGizmoEmboss::reinit_text_lines(unsigned count_lines) { + init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, count_lines); +} + void GLGizmoEmboss::set_volume_by_selection() { const Selection &selection = m_parent.get_selection(); @@ -1062,6 +1272,14 @@ void GLGizmoEmboss::set_volume_by_selection() m_text = tc.text; m_volume = volume; m_volume_id = volume->id(); + + if (tc.style.prop.per_glyph) + reinit_text_lines(); + + // Calculate current angle of up vector + assert(m_style_manager.is_active_font()); + if (m_style_manager.is_active_font()) + m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); // calculate scale for height and depth inside of scaled object instance calculate_scale(); @@ -1111,23 +1329,65 @@ bool GLGizmoEmboss::process() if (m_volume == nullptr) return false; // without text there is nothing to emboss - if (m_text.empty()) return false; + if (priv::is_text_empty(m_text)) return false; // exist loaded font file? if (!m_style_manager.is_active_font()) return false; - - assert(m_volume->text_configuration.has_value()); - if (!m_volume->text_configuration.has_value()) return false; - assert(m_volume->emboss_shape.has_value()); - if (!m_volume->emboss_shape.has_value()) return false; - DataUpdate data{create_emboss_data_base(m_text, m_style_manager, m_job_cancel), m_volume->id()}; - bool start = start_update_volume(std::move(data), *m_volume, m_parent.get_selection(), m_raycast_manager); - if (start) - // notification is removed befor object is changed by job - remove_notification_not_valid_font(); + DataUpdate data{priv::create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), m_volume->type(), m_job_cancel), + m_volume->id()}; + std::unique_ptr job = nullptr; - return start; + // check cutting from source mesh + bool &use_surface = data.text_configuration.style.prop.use_surface; + bool is_object = m_volume->get_object()->volumes.size() == 1; + if (use_surface && is_object) + use_surface = false; + + assert(!data.text_configuration.style.prop.per_glyph || + get_count_lines(m_text) == m_text_lines.get_lines().size()); + + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_volume_sources(m_volume); + if (sources.empty()) + return false; + + Transform3d text_tr = m_volume->get_matrix(); + auto& fix_3mf = m_volume->text_configuration->fix_3mf_tr; + if (fix_3mf.has_value()) + text_tr = text_tr * fix_3mf->inverse(); + + // when it is new applying of use surface than move origin onto surfaca + if (!m_volume->text_configuration->style.prop.use_surface) { + auto offset = calc_surface_offset(m_parent.get_selection(), m_raycast_manager); + if (offset.has_value()) + text_tr *= Eigen::Translation(*offset); + } + + // check that there is not unexpected volume type + bool is_valid_type = check(m_volume->type()); + assert(is_valid_type); + if (!is_valid_type) + return false; + + UpdateSurfaceVolumeData surface_data{std::move(data), {text_tr, std::move(sources)}}; + job = std::make_unique(std::move(surface_data)); + } else { + job = std::make_unique(std::move(data)); + } + +#ifndef EXECUTE_PROCESS_ON_MAIN_THREAD + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::move(job)); +#else + // Run Job on main thread (blocking) - ONLY DEBUG + priv::execute_job(std::move(job)); +#endif // EXECUTE_PROCESS_ON_MAIN_THREAD + + // notification is removed befor object is changed by job + remove_notification_not_valid_font(); + return true; } namespace { @@ -1311,6 +1571,8 @@ void GLGizmoEmboss::draw_text_input() append_warning(_u8L("Too tall, diminished font height inside text input.")); if (imgui_size < StyleManager::min_imgui_font_size) append_warning(_u8L("Too small, enlarged font height inside text input.")); + if (prop.align.first == FontProp::HorizontalAlign::center || prop.align.first == FontProp::HorizontalAlign::right) + append_warning(_u8L("Text doesn't show current horizontal alignment.")); } // flag for extend font ranges if neccessary @@ -1322,6 +1584,12 @@ void GLGizmoEmboss::draw_text_input() ImVec2 input_size(m_gui_cfg->text_size.x, m_gui_cfg->text_size.y + extra_height); const ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_AutoSelectAll; if (ImGui::InputTextMultiline("##Text", &m_text, input_size, flags)) { + if (m_style_manager.get_font_prop().per_glyph) { + unsigned count_lines = get_count_lines(m_text); + if (count_lines != m_text_lines.get_lines().size()) + // Necesarry to initialize count by given number (differ from stored in volume at the moment) + reinit_text_lines(count_lines); + } process(); range_text = create_range_text_prep(); } @@ -1457,6 +1725,8 @@ void GLGizmoEmboss::draw_font_list_line() if (exist_change) { m_style_manager.clear_glyphs_cache(); + if (m_style_manager.get_font_prop().per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); process(); } } @@ -1680,13 +1950,11 @@ void GLGizmoEmboss::draw_model_type() Plater::TakeSnapshot snapshot(plater, _L("Change Text Type"), UndoRedo::SnapshotType::GizmoAction); m_volume->set_type(*new_type); - // Update volume position when switch from part or into part - if (m_volume->emboss_shape->projection.use_surface) { - // move inside - bool is_volume_move_inside = (type == part); - bool is_volume_move_outside = (*new_type == part); - if (is_volume_move_inside || is_volume_move_outside) process(); - } + bool is_volume_move_inside = (type == part); + bool is_volume_move_outside = (*new_type == part); + // Update volume position when switch (from part) or (into part) + if ((is_volume_move_inside || is_volume_move_outside)) + process(); // inspiration in ObjectList::change_part_type() // how to view correct side panel with objects @@ -2198,13 +2466,16 @@ bool GLGizmoEmboss::revertible(const std::string &name, bool result = draw(); // render revert changes button - if (changed) { - ImGui::SameLine(undo_offset); + if (changed) { + ImGuiWindow *window = ImGui::GetCurrentWindow(); + float prev_x = window->DC.CursorPosPrevLine.x; + ImGui::SameLine(undo_offset); // change cursor postion if (draw_button(m_icons, IconType::undo)) { value = *default_value; return true; } else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", undo_tooltip.c_str()); + window->DC.CursorPosPrevLine.x = prev_x; // set back previous position } return result; } @@ -2285,7 +2556,7 @@ bool GLGizmoEmboss::rev_checkbox(const std::string &name, } bool GLGizmoEmboss::set_height() { - float &value = m_style_manager.get_style().prop.size_in_mm; + float &value = m_style_manager.get_font_prop().size_in_mm; // size can't be zero or negative apply(value, limits.size_in_mm); @@ -2299,6 +2570,9 @@ bool GLGizmoEmboss::set_height() { if (is_approx(value, m_volume->text_configuration->style.prop.size_in_mm)) return false; + if (m_style_manager.get_font_prop().per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + #ifdef USE_PIXEL_SIZE_IN_WX_FONT // store font size into path serialization const wxFont &wx_font = m_style_manager.get_wx_font(); @@ -2313,7 +2587,7 @@ bool GLGizmoEmboss::set_height() { void GLGizmoEmboss::draw_height(bool use_inch) { - float &value = m_style_manager.get_style().prop.size_in_mm; + float &value = m_style_manager.get_font_prop().size_in_mm; const EmbossStyle* stored_style = m_style_manager.get_stored_style(); const float *stored = (stored_style != nullptr)? &stored_style->prop.size_in_mm : nullptr; const char *size_format = use_inch ? "%.2f in" : "%.1f mm"; @@ -2324,6 +2598,17 @@ void GLGizmoEmboss::draw_height(bool use_inch) process(); } +bool GLGizmoEmboss::set_depth() +{ + float &value = m_style_manager.get_font_prop().emboss; + + // size can't be zero or negative + priv::Limits::apply(value, priv::limits.emboss); + + // only different value need process + return !is_approx(value, m_volume->text_configuration->style.prop.emboss); +} + void GLGizmoEmboss::draw_depth(bool use_inch) { double &value = m_style_manager.get_style().projection.depth; @@ -2431,8 +2716,7 @@ void GLGizmoEmboss::draw_advanced() ", unitPerEm=" + std::to_string(font_info.unit_per_em) + ", cache(" + std::to_string(cache_size) + " glyphs)"; if (font_file->infos.size() > 1) { - unsigned int collection = current_prop.collection_number.has_value() ? - *current_prop.collection_number : 0; + unsigned int collection = current_prop.collection_number.value_or(0); ff_property += ", collect=" + std::to_string(collection+1) + "/" + std::to_string(font_file->infos.size()); } m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, ff_property); @@ -2444,10 +2728,10 @@ void GLGizmoEmboss::draw_advanced() const StyleManager::Style *stored_style = nullptr; if (m_style_manager.exist_stored_style()) stored_style = m_style_manager.get_stored_style(); - - bool can_use_surface = (m_volume == nullptr)? false : - (m_volume->emboss_shape->projection.use_surface)? true : // already used surface must have option to uncheck - !m_volume->is_the_only_one_part(); + + bool is_the_only_one_part = m_volume->is_the_only_one_part(); + bool can_use_surface = (m_volume->emboss_shape->projection.use_surface)? true : // already used surface must have option to uncheck + !is_the_only_one_part; m_imgui->disabled_begin(!can_use_surface); const bool *def_use_surface = stored_style ? &stored_style->projection.use_surface : nullptr; @@ -2460,6 +2744,77 @@ void GLGizmoEmboss::draw_advanced() process(); } m_imgui->disabled_end(); // !can_use_surface + + bool &per_glyph = font_prop.per_glyph; + bool can_use_per_glyph = (per_glyph) ? true : // already used surface must have option to uncheck + !is_the_only_one_part; + m_imgui->disabled_begin(!can_use_per_glyph); + const bool *def_per_glyph = stored_style ? &stored_style->prop.per_glyph : nullptr; + if (rev_checkbox(tr.per_glyph, per_glyph, def_per_glyph, + _u8L("Revert Transformation per glyph."))) { + if (per_glyph && !m_text_lines.is_init()) + reinit_text_lines(); + process(); + } else if (ImGui::IsItemHovered()) { + if (per_glyph) { + ImGui::SetTooltip("%s", _u8L("Set global orientation for whole text.").c_str()); + } else { + ImGui::SetTooltip("%s", _u8L("Set position and orientation per Glyph.").c_str()); + if (!m_text_lines.is_init()) + reinit_text_lines(); + } + } else if (!per_glyph && m_text_lines.is_init()) + m_text_lines.reset(); + m_imgui->disabled_end(); // !can_use_per_glyph + + m_imgui->disabled_begin(!per_glyph); + ImGui::SameLine(); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + if (m_imgui->slider_float("##base_line_y_offset", &m_text_lines.offset, -10.f, 10.f, "%f mm")) { + reinit_text_lines(m_text_lines.get_lines().size()); + process(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("TEST PURPOSE ONLY\nMove base line (up/down) for allign letters"); + m_imgui->disabled_end(); // !per_glyph + + auto draw_align = [&align = font_prop.align, gui_cfg = m_gui_cfg, &icons = m_icons]() { + bool is_change = false; + ImGui::SameLine(gui_cfg->advanced_input_offset); + if (align.first==FontProp::HorizontalAlign::left) draw(get_icon(icons, IconType::align_horizontal_left, IconState::hovered)); + else if (draw_button(icons, IconType::align_horizontal_left)) { align.first=FontProp::HorizontalAlign::left; is_change = true; } + else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set left alignment").c_str()); + ImGui::SameLine(); + if (align.first==FontProp::HorizontalAlign::center) draw(get_icon(icons, IconType::align_horizontal_center, IconState::hovered)); + else if (draw_button(icons, IconType::align_horizontal_center)) { align.first=FontProp::HorizontalAlign::center; is_change = true; } + else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set horizont center alignment").c_str()); + ImGui::SameLine(); + if (align.first==FontProp::HorizontalAlign::right) draw(get_icon(icons, IconType::align_horizontal_right, IconState::hovered)); + else if (draw_button(icons, IconType::align_horizontal_right)) { align.first=FontProp::HorizontalAlign::right; is_change = true; } + else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set right alignment").c_str()); + + ImGui::SameLine(); + if (align.second==FontProp::VerticalAlign::top) draw(get_icon(icons, IconType::align_vertical_top, IconState::hovered)); + else if (draw_button(icons, IconType::align_vertical_top)) { align.second=FontProp::VerticalAlign::top; is_change = true; } + else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set top alignment").c_str()); + ImGui::SameLine(); + if (align.second==FontProp::VerticalAlign::center) draw(get_icon(icons, IconType::align_vertical_center, IconState::hovered)); + else if (draw_button(icons, IconType::align_vertical_center)) { align.second=FontProp::VerticalAlign::center; is_change = true; } + else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set vertical center alignment").c_str()); + ImGui::SameLine(); + if (align.second==FontProp::VerticalAlign::bottom) draw(get_icon(icons, IconType::align_vertical_bottom, IconState::hovered)); + else if (draw_button(icons, IconType::align_vertical_bottom)) { align.second=FontProp::VerticalAlign::bottom; is_change = true; } + else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set bottom alignment").c_str()); + return is_change; + }; + const FontProp::Align * def_align = stored_style ? &stored_style->prop.align : nullptr; + float undo_offset = ImGui::GetStyle().FramePadding.x; + if (revertible(tr.alignment, font_prop.align, def_align, _u8L("Revert alignment."), undo_offset, draw_align)) { + if (font_prop.per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + // TODO: move with text in finalize to not change position + process(); + } + // TRN EmbossGizmo: font units std::string units = _u8L("points"); std::string units_fmt = "%.0f " + units; @@ -2496,6 +2851,8 @@ void GLGizmoEmboss::draw_advanced() !volume_line_gap.has_value() || volume_line_gap != current_prop.line_gap) { // line gap is planed to be stored inside of imgui font atlas m_style_manager.clear_imgui_font(); + if (font_prop.per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); exist_change = true; } } @@ -2558,9 +2915,14 @@ void GLGizmoEmboss::draw_advanced() min_distance, max_distance, "%.2f mm", move_tooltip)) is_moved = true; } - if (is_moved) - do_local_z_move(m_parent, distance.value_or(.0f) - prev_distance); - m_imgui->disabled_end(); // allowe_surface_distance + if (is_moved){ + if (font_prop.per_glyph){ + process(); + } else { + do_local_z_move(m_parent, distance.value_or(.0f) - prev_distance); + } + } + m_imgui->disabled_end(); // allowe_surface_distance // slider for Clock-wise angle in degress // stored angle is optional CCW and in radians @@ -2590,8 +2952,11 @@ void GLGizmoEmboss::draw_advanced() if (m_style_manager.is_active_font() && gl_volume != nullptr) m_style_manager.get_style().angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); + if (font_prop.per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + // recalculate for surface cut - if (use_surface) + if (use_surface || font_prop.per_glyph) process(); } @@ -2637,17 +3002,23 @@ void GLGizmoEmboss::draw_advanced() if (exist_change) { m_style_manager.clear_glyphs_cache(); + if (m_style_manager.get_font_prop().per_glyph) + reinit_text_lines(); + else + m_text_lines.reset(); process(); } if (ImGui::Button(_u8L("Set text to face camera").c_str())) { assert(get_selected_volume(m_parent.get_selection()) == m_volume); const Camera &cam = wxGetApp().plater()->get_camera(); - if (face_selected_volume_to_camera(cam, m_parent) && use_surface) + if (face_selected_volume_to_camera(cam, m_parent) && + (use_surface || prop.per_glyph)) process(); } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", _u8L("Orient the text towards the camera.").c_str()); } + #ifdef ALLOW_DEBUG_MODE ImGui::Text("family = %s", (current_prop.family.has_value() ? current_prop.family->c_str() : @@ -2744,7 +3115,7 @@ bool GLGizmoEmboss::choose_font_by_wxdialog() const auto&ff = m_style_manager.get_font_file_with_cache(); if (WxFontUtils::is_italic(wx_font) && !Emboss::is_italic(*ff.font_file, font_collection)) { - m_style_manager.get_style().prop.skew = 0.2; + m_style_manager.get_font_prop().skew = 0.2; } return true; } @@ -2856,7 +3227,13 @@ void GLGizmoEmboss::init_icons() "lock_closed.svg", // lock, "lock_closed_f.svg",// lock_bold, "lock_open.svg", // unlock, - "lock_open_f.svg" // unlock_bold, + "lock_open_f.svg", // unlock_bold, + "align_horizontal_left.svg", + "align_horizontal_center.svg", + "align_horizontal_right.svg", + "align_vertical_top.svg", + "align_vertical_center.svg", + "align_vertical_bottom.svg" }; assert(filenames.size() == static_cast(IconType::_count)); std::string path = resources_dir() + "/icons/"; @@ -2966,7 +3343,12 @@ void TextDataBase::write(ModelVolume &volume) const fp.emboss = static_cast(ep.depth); } -std::unique_ptr create_emboss_data_base(const std::string &text, StyleManager &style_manager, std::shared_ptr>& cancel) +std::unique_ptr create_emboss_data_base(const std::string &text, + StyleManager &style_manager, + TextLinesModel &text_lines, + const Selection &selection, + ModelVolumeType type, + std::shared_ptr> &cancel) { // create volume_name std::string volume_name = text; // copy @@ -2988,6 +3370,14 @@ std::unique_ptr create_emboss_data_base(const std::string &text, Style assert(style_manager.get_wx_font().IsOk()); assert(style.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0); + if (es.prop.per_glyph) { + if (!text_lines.is_init()) + init_text_lines(text_lines, selection, style_manager); + } else + text_lines.reset(); + + bool is_outside = (type == ModelVolumeType::MODEL_PART); + // Cancel previous Job, when it is in process // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs // Cancel only EmbossUpdateJob no others @@ -2998,7 +3388,7 @@ std::unique_ptr create_emboss_data_base(const std::string &text, Style DataBase base(volume_name, cancel); FontFileWithCache &font = style_manager.get_font_file_with_cache(); TextConfiguration tc{static_cast(style), text}; - return std::make_unique(std::move(base), font, std::move(tc), style.projection); + return std::make_unique(std::move(base), font, std::move(tc), style.projection, is_outside, text_lines.get_lines()); } CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager& raycaster, ModelVolumeType volume_type) @@ -3368,6 +3758,51 @@ GuiCfg create_gui_configuration() return cfg; } +bool apply_camera_dir(const Camera& camera, GLCanvas3D& canvas, bool keep_up) { + const Vec3d& cam_dir = camera.get_dir_forward(); + + Selection& sel = canvas.get_selection(); + if (sel.is_empty()) return false; + + // camera direction transformed into volume coordinate system + Transform3d to_world = world_matrix_fixed(sel); + Vec3d cam_dir_tr = to_world.inverse().linear() * cam_dir; + cam_dir_tr.normalize(); + + Vec3d emboss_dir(0., 0., -1.); + + // check wether cam_dir is already used + if (is_approx(cam_dir_tr, emboss_dir)) return false; + + assert(sel.get_volume_idxs().size() == 1); + GLVolume* gl_volume = sel.get_volume(*sel.get_volume_idxs().begin()); + + Transform3d vol_rot; + Transform3d vol_tr = gl_volume->get_volume_transformation().get_matrix(); + // check whether cam_dir is opposit to emboss dir + if (is_approx(cam_dir_tr, -emboss_dir)) { + // rotate 180 DEG by y + vol_rot = Eigen::AngleAxis(M_PI_2, Vec3d(0., 1., 0.)); + } + else { + // calc params for rotation + Vec3d axe = emboss_dir.cross(cam_dir_tr); + axe.normalize(); + double angle = std::acos(emboss_dir.dot(cam_dir_tr)); + vol_rot = Eigen::AngleAxis(angle, axe); + } + + Vec3d offset = vol_tr * Vec3d::Zero(); + Vec3d offset_inv = vol_rot.inverse() * offset; + Transform3d res = vol_tr * + Eigen::Translation(-offset) * + vol_rot * + Eigen::Translation(offset_inv); + //Transform3d res = vol_tr * vol_rot; + gl_volume->set_volume_transformation(Geometry::Transformation(res)); + get_model_volume(*gl_volume, sel.get_model()->objects)->set_transformation(res); + return true; +} } // namespace // any existing icon filename to not influence GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp index 0dba23ecc7..070ba55daa 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp @@ -5,6 +5,8 @@ #include "GLGizmoRotate.hpp" #include "slic3r/GUI/IconManager.hpp" #include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/I18N.hpp" // TODO: not needed +#include "slic3r/GUI/TextLines.hpp" #include "slic3r/Utils/RaycastManager.hpp" #include "slic3r/Utils/EmbossStyleManager.hpp" @@ -80,6 +82,9 @@ protected: std::string get_action_snapshot_name() const override; private: + void volume_transformation_changing(); + void volume_transformation_changed(); + static EmbossStyles create_default_styles(); // localized default text bool init_create(ModelVolumeType volume_type); @@ -189,6 +194,10 @@ private: // cancel for previous update of volume to cancel finalize part std::shared_ptr> m_job_cancel = nullptr; + // Keep information about curvature of text line around surface + TextLinesModel m_text_lines; + void reinit_text_lines(unsigned count_lines=0); + // Rotation gizmo GLGizmoRotate m_rotate_gizmo; // Value is set only when dragging rotation to calculate actual angle diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp index e0e19ad52d..28edc387a3 100644 --- a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -33,10 +33,11 @@ void CreateFontStyleImagesJob::process(Ctl &ctl) std::vector scales(m_input.styles.size()); m_images = std::vector(m_input.styles.size()); + auto was_canceled = []() { return false; }; for (auto &item : m_input.styles) { size_t index = &item - &m_input.styles.front(); ExPolygons &shapes = name_shapes[index]; - shapes = text2shapes(item.font, m_input.text.c_str(), item.prop); + shapes = text2shapes(item.font, m_input.text.c_str(), item.prop, was_canceled); // create image description StyleManager::StyleImage &image = m_images[index]; diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index 0a840b1d4a..a5d4542de0 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -1,6 +1,7 @@ #include "EmbossJob.hpp" #include +#include #include #include // load_obj for default mesh @@ -55,6 +56,9 @@ struct DataCreateVolume GLGizmosManager::EType gizmo; }; +// Offset of clossed side to model +constexpr float SAFE_SURFACE_OFFSET = 0.015f; // [in mm] + /// /// Create new TextVolume on the surface of ModelObject /// Should not be stopped @@ -155,6 +159,8 @@ bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surfac bool check(const CreateSurfaceVolumeData &input, bool is_main_thread = false); bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread = false); +template static ExPolygons create_shape(DataBase &input, Fnc was_canceled); +template static std::vector create_shapes(DataBase &input, Fnc was_canceled); // create sure that emboss object is bigger than source object [in mm] constexpr float safe_extension = 1.0f; @@ -267,13 +273,10 @@ void Slic3r::GUI::Emboss::DataBase::write(ModelVolume &volume) const{ CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input): m_input(std::move(input)){ assert(check(m_input, true)); } void CreateVolumeJob::process(Ctl &ctl) { - if (!check(m_input)) - throw JobException("Bad input data for EmbossCreateVolumeJob."); - - m_result = create_mesh(*m_input.base, was_canceled(ctl, *m_input.base), ctl); - // center result - Vec3f c = m_result.bounding_box().center().cast(); - if (!c.isApprox(Vec3f::Zero())) m_result.translate(-c); + if (!priv::check(m_input)) throw std::runtime_error("Bad input data for EmbossCreateVolumeJob."); + auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); }; + m_result = priv::create_mesh(m_input, was_canceled, ctl); + //m_result = create_mesh(*m_input.base, was_canceled(ctl, *m_input.base), ctl); // svg } void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { if (!::finalize(canceled, eptr, *m_input.base)) @@ -390,11 +393,7 @@ void UpdateJob::process(Ctl &ctl) m_result = ::try_create_mesh(*m_input.base, was_canceled); if (was_canceled()) return; if (m_result.its.empty()) - throw JobException("Created text volume is empty. Change text or font."); - - // center triangle mesh - Vec3d shift = m_result.bounding_box().center(); - m_result.translate(-shift.cast()); + throw priv::JobException("Created text volume is empty. Change text or font."); } void UpdateJob::finalize(bool canceled, std::exception_ptr &eptr) @@ -695,9 +694,14 @@ bool check(const DataBase &input, bool check_fontfile, bool use_surface) // res &= !input.text_configuration.text.empty(); assert(!input.volume_name.empty()); res &= !input.volume_name.empty(); - // assert(input.text_configuration.style.prop.use_surface == use_surface); - // res &= input.text_configuration.style.prop.use_surface == use_surface; - return res; + const FontProp& prop = input.text_configuration.style.prop; + assert(prop.per_glyph == !input.text_lines.empty()); + res &= prop.per_glyph == !input.text_lines.empty(); + if (prop.per_glyph) { + assert(get_count_lines(input.text_configuration.text) == input.text_lines.size()); + res &= get_count_lines(input.text_configuration.text) == input.text_lines.size(); + } + return res; } bool check(GLGizmosManager::EType gizmo) @@ -786,20 +790,218 @@ bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread) return res; } -template TriangleMesh try_create_mesh(DataBase &base, const Fnc &was_canceled) +template +ExPolygons priv::create_shape(DataBase &input, Fnc was_canceled) { + FontFileWithCache &font = input.font_file; + const TextConfiguration &tc = input.text_configuration; + const char *text = tc.text.c_str(); + const FontProp &prop = tc.style.prop; + assert(!prop.per_glyph); + assert(font.has_value()); + if (!font.has_value()) + return {}; + + ExPolygons shapes = text2shapes(font, text, prop, was_canceled); + if (shapes.empty()) + return {}; + + return shapes; +} + +template +std::vector priv::create_shapes(DataBase &input, Fnc was_canceled) { + FontFileWithCache &font = input.font_file; + const TextConfiguration &tc = input.text_configuration; + const char *text = tc.text.c_str(); + const FontProp &prop = tc.style.prop; + assert(prop.per_glyph); + assert(font.has_value()); + if (!font.has_value()) + return {}; + + std::wstring ws = boost::nowide::widen(text); + std::vector shapes = text2vshapes(font, ws, prop, was_canceled); + if (shapes.empty()) + return {}; + + if (was_canceled()) + return {}; + + return shapes; +} + +//#define STORE_SAMPLING +#ifdef STORE_SAMPLING +#include "libslic3r/SVG.hpp" +#endif // STORE_SAMPLING +namespace { + +std::vector create_line_bounds(const std::vector &shapes, const std::wstring& text, size_t count_lines = 0) { - const EmbossShape &shape = base.create_shape(); + assert(text.size() == shapes.size()); + if (count_lines == 0) + count_lines = get_count_lines(text); + assert(count_lines == get_count_lines(text)); + + std::vector result(count_lines); + size_t text_line_index = 0; + // s_i .. shape index + for (size_t s_i = 0; s_i < shapes.size(); ++s_i) { + const ExPolygons &shape = shapes[s_i]; + BoundingBox bb; + if (!shape.empty()) { + bb = get_extents(shape); + } + BoundingBoxes &line_bbs = result[text_line_index]; + line_bbs.push_back(bb); + if (text[s_i] == '\n'){ + // skip enters on beginig and tail + ++text_line_index; + } + } + return result; +} + +template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc was_canceled) +{ + // method use square of coord stored into int64_t + static_assert(std::is_same()); + + std::vector shapes = priv::create_shapes(input, was_canceled); + if (shapes.empty()) + return {}; + + // Precalculate bounding boxes of glyphs + // Separate lines of text to vector of Bounds + const TextConfiguration &tc = input.text_configuration; + std::wstring ws = boost::nowide::widen(tc.text.c_str()); + assert(get_count_lines(ws) == input.text_lines.size()); + size_t count_lines = input.text_lines.size(); + std::vector bbs = create_line_bounds(shapes, ws, count_lines); + + const FontProp &prop = tc.style.prop; + FontFileWithCache &font = input.font_file; + double shape_scale = get_shape_scale(prop, *font.font_file); + double projec_scale = shape_scale / SHAPE_SCALE; + double depth = prop.emboss / projec_scale; + auto scale_tr = Eigen::Scaling(projec_scale); + + // half of font em size for direction of letter emboss + double em_2_mm = prop.size_in_mm / 2.; + int32_t em_2_polygon = static_cast(std::round(scale_(em_2_mm))); + + size_t s_i_offset = 0; // shape index offset(for next lines) + indexed_triangle_set result; + for (size_t text_line_index = 0; text_line_index < input.text_lines.size(); ++text_line_index) { + const BoundingBoxes &line_bbs = bbs[text_line_index]; + const TextLine &line = input.text_lines[text_line_index]; + PolygonPoints samples = sample_slice(line, line_bbs, shape_scale); + std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); + + for (size_t i = 0; i < line_bbs.size(); ++i) { + const BoundingBox &letter_bb = line_bbs[i]; + if (!letter_bb.defined) + continue; + + Vec2d to_zero_vec = letter_bb.center().cast() * shape_scale; // [in mm] + float surface_offset = input.is_outside ? -priv::SAFE_SURFACE_OFFSET : (-prop.emboss + priv::SAFE_SURFACE_OFFSET); + if (prop.distance.has_value()) + surface_offset += *prop.distance; + + Eigen::Translation to_zero(-to_zero_vec.x(), 0., static_cast(surface_offset)); + + const double &angle = angles[i]; + Eigen::AngleAxisd rotate(angle + M_PI_2, Vec3d::UnitY()); + + const PolygonPoint &sample = samples[i]; + Vec2d offset_vec = unscale(sample.point); // [in mm] + Eigen::Translation offset_tr(offset_vec.x(), 0., -offset_vec.y()); + Transform3d tr = offset_tr * rotate * to_zero * scale_tr; + + const ExPolygons &letter_shape = shapes[s_i_offset + i]; + assert(get_extents(letter_shape) == letter_bb); + auto projectZ = std::make_unique(depth); + ProjectTransform project(std::move(projectZ), tr); + indexed_triangle_set glyph_its = polygons2model(letter_shape, project); + its_merge(result, std::move(glyph_its)); + + if (((s_i_offset + i) % 15) && was_canceled()) + return {}; + } + s_i_offset += line_bbs.size(); + +#ifdef STORE_SAMPLING + { // Debug store polygon + //std::string stl_filepath = "C:/data/temp/line" + std::to_string(text_line_index) + "_model.stl"; + //bool suc = its_write_stl_ascii(stl_filepath.c_str(), "label", result); + + BoundingBox bbox = get_extents(line.polygon); + std::string file_path = "C:/data/temp/line" + std::to_string(text_line_index) + "_letter_position.svg"; + SVG svg(file_path, bbox); + svg.draw(line.polygon); + int32_t radius = bbox.size().x() / 300; + for (size_t i = 0; i < samples.size(); i++) { + const PolygonPoint &pp = samples[i]; + const Point& p = pp.point; + svg.draw(p, "green", radius); + std::string label = std::string(" ")+tc.text[i]; + svg.draw_text(p, label.c_str(), "black"); + + double a = angles[i]; + double length = 3.0 * radius; + Point n(length * std::cos(a), length * std::sin(a)); + svg.draw(Slic3r::Line(p - n, p + n), "Lime"); + } + } +#endif // STORE_SAMPLING + } + return TriangleMesh(std::move(result)); +} + +// svg +template TriangleMesh try_create_mesh(DataBase& base, const Fnc& was_canceled) +{ + const EmbossShape& shape = base.create_shape(); if (shape.shapes.empty()) return {}; - double depth = shape.projection.depth / shape.scale; + double depth = shape.projection.depth / shape.scale; auto projectZ = std::make_unique(depth); ProjectScale project(std::move(projectZ), shape.scale); if (was_canceled()) return {}; return TriangleMesh(polygons2model(shape.shapes, project)); } +} // namespace -template TriangleMesh create_mesh(DataBase &input, const Fnc &was_canceled, Job::Ctl &ctl) + +template +TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) +{ + if (!input.text_lines.empty()) { + TriangleMesh tm = create_mesh_per_glyph(input, was_canceled); + if (was_canceled()) return {}; + if (!tm.empty()) return tm; + } + + ExPolygons shapes = priv::create_shape(input, was_canceled); + if (shapes.empty()) return {}; + if (was_canceled()) return {}; + + const FontProp &prop = input.text_configuration.style.prop; + const FontFile &ff = *input.font_file.font_file; + // NOTE: SHAPE_SCALE is applied in ProjectZ + double scale = get_shape_scale(prop, ff) / SHAPE_SCALE; + double depth = prop.emboss / scale; + auto projectZ = std::make_unique(depth); + float offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (SAFE_SURFACE_OFFSET - prop.emboss); + Transform3d tr = Eigen::Translation(0., 0.,static_cast(offset)) * Eigen::Scaling(scale); + ProjectTransform project(std::move(projectZ), tr); + if (was_canceled()) return {}; + return TriangleMesh(polygons2model(shapes, project)); +} + +template +TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl) { // It is neccessary to create some shape // Emboss text window is opened by creation new emboss text object @@ -1065,46 +1267,35 @@ OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) { - // Offset of clossed side to model - const float surface_offset = 0.015f; // [in mm] - float front_move = is_outside ? emboss : surface_offset, - back_move = -(is_outside ? surface_offset : emboss); - its_transform(cut, tr.pretranslate(Vec3d(0., 0., front_move))); + float + front_move = (is_outside) ? emboss : SAFE_SURFACE_OFFSET, + back_move = -((is_outside) ? SAFE_SURFACE_OFFSET : emboss); + its_transform(cut, tr.pretranslate(Vec3d(0., 0., front_move))); Vec3d from_front_to_back(0., 0., back_move - front_move); return OrthoProject3d(from_front_to_back); } -// input can't be const - cache of font -template TriangleMesh cut_surface(DataBase &base, const SurfaceVolumeData &input2, const Fnc &was_canceled) -{ - EmbossShape &emboss_shape = base.create_shape(); - ExPolygons &shapes = emboss_shape.shapes; - if (shapes.empty()) - throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); +namespace { - if (was_canceled()) - return {}; +indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transform3d& tr,const SurfaceVolumeData::ModelSources &sources, DataBase& input, std::function was_canceled) { + assert(!sources.empty()); + BoundingBox bb = get_extents(shapes); + const FontFile &ff = *input.font_file.font_file; + const FontProp &fp = input.text_configuration.style.prop; + double shape_scale = get_shape_scale(fp, ff); - // Define alignment of text - left, right, center, top bottom, .... - BoundingBox bb = get_extents(shapes); - Point projection_center = bb.center(); - for (ExPolygon &shape : shapes) - shape.translate(-projection_center); - bb.translate(-projection_center); - - const SurfaceVolumeData::ModelSources &sources = input2.sources; - const SurfaceVolumeData::ModelSource *biggest = &sources.front(); + const SurfaceVolumeData::ModelSource *biggest = &sources.front(); size_t biggest_count = 0; // convert index from (s)ources to (i)ndexed (t)riangle (s)ets - std::vector s_to_itss(sources.size(), std::numeric_limits::max()); - std::vector itss; + std::vector s_to_itss(sources.size(), std::numeric_limits::max()); + std::vector itss; itss.reserve(sources.size()); for (const SurfaceVolumeData::ModelSource &s : sources) { - Transform3d mesh_tr_inv = s.tr.inverse(); - Transform3d cut_projection_tr = mesh_tr_inv * input2.transform; + Transform3d mesh_tr_inv = s.tr.inverse(); + Transform3d cut_projection_tr = mesh_tr_inv * tr; std::pair z_range{0., 1.}; - OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, emboss_shape.scale, z_range); + OrthoProject cut_projection = priv::create_projection_for_cut(cut_projection_tr, shape_scale, z_range); // copy only part of source model indexed_triangle_set its = its_cut_AoI(s.mesh->its, bb, cut_projection); if (its.indices.empty()) @@ -1119,10 +1310,10 @@ template TriangleMesh cut_surface(DataBase &base, const SurfaceVol itss.emplace_back(std::move(its)); } if (itss.empty()) - throw JobException(_u8L("There is no volume in projection direction.").c_str()); + return {}; - Transform3d tr_inv = biggest->tr.inverse(); - Transform3d cut_projection_tr = tr_inv * input2.transform; + Transform3d tr_inv = biggest->tr.inverse(); + Transform3d cut_projection_tr = tr_inv * tr; size_t itss_index = s_to_itss[biggest - &sources.front()]; BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); @@ -1145,22 +1336,27 @@ template TriangleMesh cut_surface(DataBase &base, const SurfaceVol Transform3d emboss_tr = cut_projection_tr.inverse(); BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); std::pair z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()}; - OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, emboss_shape.scale, z_range); - float projection_ratio = (-z_range.first + safe_extension) / (z_range.second - z_range.first + 2 * safe_extension); + OrthoProject cut_projection = priv::create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + float projection_ratio = (-z_range.first + priv::safe_extension) / + (z_range.second - z_range.first + 2 * priv::safe_extension); - bool is_text_reflected = Slic3r::has_reflection(input2.transform); + ExPolygons shapes_data; // is used only when text is reflected to reverse polygon points order + const ExPolygons *shapes_ptr = &shapes; + bool is_text_reflected = Slic3r::has_reflection(tr); if (is_text_reflected) { // revert order of points in expolygons // CW --> CCW - for (ExPolygon &shape : shapes) { + shapes_data = shapes; // copy + for (ExPolygon &shape : shapes_data) { shape.contour.reverse(); for (Slic3r::Polygon &hole : shape.holes) hole.reverse(); } + shapes_ptr = &shapes_data; } // Use CGAL to cut surface from triangle mesh - SurfaceCut cut = cut_surface(shapes, itss, cut_projection, projection_ratio); + SurfaceCut cut = cut_surface(*shapes_ptr, itss, cut_projection, projection_ratio); if (is_text_reflected) { for (SurfaceCut::Contour &c : cut.contours) @@ -1169,18 +1365,105 @@ template TriangleMesh cut_surface(DataBase &base, const SurfaceVol std::swap(t[0], t[1]); } - if (cut.empty()) - throw JobException(_u8L("There is no valid surface for text projection.").c_str()); - if (was_canceled()) - return {}; + if (cut.empty()) return {}; // There is no valid surface for text projection. + if (was_canceled()) return {}; // !! Projection needs to transform cut - OrthoProject3d projection = create_emboss_projection(input2.is_outside, static_cast(emboss_shape.projection.depth), emboss_tr, cut); - indexed_triangle_set new_its = cut2model(cut, projection); - assert(!new_its.empty()); - if (was_canceled()) - return {}; - return TriangleMesh(std::move(new_its)); + OrthoProject3d projection = priv::create_emboss_projection(input.is_outside, fp.emboss, emboss_tr, cut); + return cut2model(cut, projection); +} + +TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &input2, std::function was_canceled) +{ + std::vector shapes = priv::create_shapes(input1, was_canceled); + if (was_canceled()) return {}; + if (shapes.empty()) + throw priv::JobException(_u8L("Font doesn't have any shape for given text.").c_str()); + + // Precalculate bounding boxes of glyphs + // Separate lines of text to vector of Bounds + const TextConfiguration &tc = input1.text_configuration; + std::wstring ws = boost::nowide::widen(tc.text.c_str()); + assert(get_count_lines(ws) == input1.text_lines.size()); + size_t count_lines = input1.text_lines.size(); + std::vector bbs = create_line_bounds(shapes, ws, count_lines); + + const FontProp &prop = tc.style.prop; + FontFileWithCache &font = input1.font_file; + double shape_scale = get_shape_scale(prop, *font.font_file); + + // half of font em size for direction of letter emboss + double em_2_mm = prop.size_in_mm / 2.; + int32_t em_2_polygon = static_cast(std::round(scale_(em_2_mm))); + + size_t s_i_offset = 0; // shape index offset(for next lines) + indexed_triangle_set result; + for (size_t text_line_index = 0; text_line_index < input1.text_lines.size(); ++text_line_index) { + const BoundingBoxes &line_bbs = bbs[text_line_index]; + const TextLine &line = input1.text_lines[text_line_index]; + PolygonPoints samples = sample_slice(line, line_bbs, shape_scale); + std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); + + for (size_t i = 0; i < line_bbs.size(); ++i) { + const BoundingBox &glyph_bb = line_bbs[i]; + if (!glyph_bb.defined) + continue; + + const double &angle = angles[i]; + auto rotate = Eigen::AngleAxisd(angle + M_PI_2, Vec3d::UnitY()); + + const PolygonPoint &sample = samples[i]; + Vec2d offset_vec = unscale(sample.point); // [in mm] + auto offset_tr = Eigen::Translation(offset_vec.x(), 0., -offset_vec.y()); + + ExPolygons &glyph_shape = shapes[s_i_offset + i]; + assert(get_extents(glyph_shape) == glyph_bb); + + Point offset(-glyph_bb.center().x(), 0); + for (ExPolygon& s: glyph_shape) + s.translate(offset); + + Transform3d modify = offset_tr * rotate; + Transform3d tr = input2.text_tr * modify; + indexed_triangle_set glyph_its = cut_surface_to_its(glyph_shape, tr, input2.sources, input1, was_canceled); + // move letter in volume on the right position + its_transform(glyph_its, modify); + + // Improve: union instead of merge + its_merge(result, std::move(glyph_its)); + + if (((s_i_offset + i) % 15) && was_canceled()) + return {}; + } + s_i_offset += line_bbs.size(); + } + + if (was_canceled()) return {}; + if (result.empty()) + throw priv::JobException(_u8L("There is no valid surface for text projection.").c_str()); + return TriangleMesh(std::move(result)); +} + +} // namespace + +// input can't be const - cache of font +TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2, std::function was_canceled) +{ + const FontProp &fp = input1.text_configuration.style.prop; + if (fp.per_glyph) + return cut_per_glyph_surface(input1, input2, was_canceled); + + ExPolygons shapes = create_shape(input1, was_canceled); + if (was_canceled()) return {}; + if (shapes.empty()) + throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); + + indexed_triangle_set its = cut_surface_to_its(shapes, input2.text_tr, input2.sources, input1, was_canceled); + if (was_canceled()) return {}; + if (its.empty()) + throw JobException(_u8L("There is no valid surface for text projection.").c_str()); + + return TriangleMesh(std::move(its)); } SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) diff --git a/src/slic3r/GUI/Jobs/EmbossJob.hpp b/src/slic3r/GUI/Jobs/EmbossJob.hpp index eab04a5696..b25ccb25f5 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.hpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.hpp @@ -11,7 +11,7 @@ #include "slic3r/GUI/Jobs/EmbossJob.hpp" // Emboss::DataBase #include "slic3r/GUI/Camera.hpp" - +#include "slic3r/GUI/TextLines.hpp" #include "Job.hpp" // forward declarations @@ -42,13 +42,43 @@ public: DataBase(DataBase &&) = default; virtual ~DataBase() = default; + // Define projection move + // True (raised) .. move outside from surface + // False (engraved).. move into object + bool is_outside; + + // flag that job is canceled + // for time after process. + std::shared_ptr> cancel; + + // Define per letter projection on one text line + // [optional] It is not used when empty + Slic3r::Emboss::TextLines text_lines; + /// /// Create shape /// e.g. Text extract glyphs from font /// Not 'const' function because it could modify shape /// - virtual EmbossShape &create_shape() { return shape; }; + virtual EmbossShape& create_shape() { return shape; }; +}; +/// +/// Hold neccessary data to create ModelVolume in job +/// Volume is created on the surface of existing volume in object. +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +struct DataCreateVolume : public DataBase +{ + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // new created volume transformation + Transform3d trmat; +}; /// /// Write data how to reconstruct shape to volume /// @@ -121,11 +151,6 @@ struct SurfaceVolumeData // Transformation of volume inside of object Transform3d transform; - // Define projection move - // True (raised) .. move outside from surface - // False (engraved).. move into object - bool is_outside; - struct ModelSource { // source volumes diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 9f615a0bfb..92811b01e0 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -849,11 +849,31 @@ std::pair Selection::get_bounding_box_in_reference_s } } } + const Vec3d box_size = max - min; - const Vec3d half_box_size = 0.5 * box_size; - BoundingBoxf3 out_box(-half_box_size, half_box_size); + Vec3d half_box_size = 0.5 * box_size; Geometry::Transformation out_trafo(trafo); - const Vec3d center = 0.5 * (min + max); + Vec3d center = 0.5 * (min + max); + + // Fix for non centered volume + // by move with calculated center(to volume center) and extend half box size + // e.g. for right aligned embossed text + if (m_list.size() == 1 && + type == ECoordinatesType::Local) { + const GLVolume& vol = *get_volume(*m_list.begin()); + const Transform3d vol_world_trafo = vol.world_matrix(); + Vec3d world_zero = vol_world_trafo * Vec3d::Zero(); + for (size_t i = 0; i < 3; i++){ + // move center to local volume zero + center[i] = world_zero.dot(axes[i]); + // extend half size to bigger distance from center + half_box_size[i] = std::max( + abs(center[i] - min[i]), + abs(center[i] - max[i])); + } + } + + const BoundingBoxf3 out_box(-half_box_size, half_box_size); out_trafo.set_offset(basis_trafo * center); return { out_box, out_trafo.get_matrix_no_scaling_factor() }; } diff --git a/src/slic3r/GUI/TextLines.cpp b/src/slic3r/GUI/TextLines.cpp new file mode 100644 index 0000000000..55002fcf63 --- /dev/null +++ b/src/slic3r/GUI/TextLines.cpp @@ -0,0 +1,345 @@ +#include "TextLines.hpp" + +#include + +#include "libslic3r/Model.hpp" + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/TriangleMeshSlicer.hpp" +#include "libslic3r/Tesselate.hpp" + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/ExPolygonsIndex.hpp" + +#include "slic3r/GUI/Selection.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GLModel.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/GUI/3DScene.hpp" + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; + +namespace { +const Slic3r::Polygon *largest(const Slic3r::Polygons &polygons) +{ + if (polygons.empty()) + return nullptr; + if (polygons.size() == 1) + return &polygons.front(); + + // compare polygon to find largest + size_t biggest_size = 0; + const Slic3r::Polygon *result = nullptr; + for (const Slic3r::Polygon &polygon : polygons) { + Point s = polygon.bounding_box().size(); + size_t size = s.x() * s.y(); + if (size <= biggest_size) + continue; + biggest_size = size; + result = &polygon; + } + return result; +} + +indexed_triangle_set its_create_belt(const Slic3r::Polygon &polygon, float width_half) { + // Improve: Create torus instead of flat belt path (with model overlaps) + assert(!polygon.empty()); + if (polygon.empty()) + return {}; + + // add a small positive offset to avoid z-fighting + float offset = static_cast(scale_(0.015f)); + Polygons polygons_expanded = expand(polygon, offset); + const Slic3r::Polygon *polygon_expanded_ptr = largest(polygons_expanded); + assert(polygon_expanded_ptr != nullptr); + if (polygon_expanded_ptr == nullptr || polygon_expanded_ptr->empty()) + return {}; + const Slic3r::Polygon &polygon_expanded = *polygon_expanded_ptr; + + // inspired by 3DScene.cpp void GLVolume::SinkingContours::update() + indexed_triangle_set model; + size_t count = polygon_expanded.size(); + model.vertices.reserve(2 * count); + model.indices.reserve(2 * count); + + for (const Point &point : polygon_expanded.points) { + Vec2f point_d = unscale(point).cast(); + Vec3f vertex(point_d.x(), point_d.y(), width_half); + model.vertices.push_back(vertex); + vertex.z() *= -1; + model.vertices.push_back(vertex); + } + + unsigned int prev_i = count - 1; + for (unsigned int i = 0; i < count; ++i) { + // t .. top + // b .. bottom + unsigned int t1 = prev_i * 2; + unsigned int b1 = t1 + 1; + unsigned int t2 = i * 2; + unsigned int b2 = t2 + 1; + model.indices.emplace_back(t1, b1, t2); + model.indices.emplace_back(b2, t2, b1); + prev_i = i; + } + return model; +} + +indexed_triangle_set its_create_torus(const Slic3r::Polygon &polygon, float radius, size_t steps = 20) +{ + assert(!polygon.empty()); + if (polygon.empty()) + return {}; + + size_t count = polygon.size(); + if (count < 3) + return {}; + + // convert and scale to float + std::vector points_d; + points_d.reserve(count); + for (const Point &point : polygon.points) + points_d.push_back(unscale(point).cast()); + + // pre calculate normalized line directions + auto calc_line_norm = [](const Vec2f &f, const Vec2f &s) -> Vec2f { return (s - f).normalized(); }; + std::vector line_norm(points_d.size()); + for (size_t i = 0; i < count - 1; ++i) + line_norm[i] = calc_line_norm(points_d[i], points_d[i + 1]); + line_norm.back() = calc_line_norm(points_d.back(), points_d.front()); + + // calculate normals for each point + auto calc_norm = [](const Vec2f &prev, const Vec2f &next) -> Vec2f { + Vec2f dir = prev + next; + return Vec2f(-dir.x(), dir.y()); + }; + std::vector points_norm(points_d.size()); + points_norm.front() = calc_norm(line_norm.back(), line_norm[1]); + for (size_t i = 1; i < points_d.size() - 1; ++i) + points_norm[i] = calc_norm(line_norm[i - 1], line_norm[i + 1]); + points_norm.back() = calc_norm(line_norm[points_d.size() - 2], line_norm.front()); + + // precalculate sinus and cosinus + double angle_step = 2 * M_PI / steps; + std::vector> sin_cos; + sin_cos.reserve(steps); + for (size_t s = 0; s < steps; ++s) { + double angle = s * angle_step; + sin_cos.emplace_back( + radius * std::sin(angle), + static_cast(radius * std::cos(angle)) + ); + } + + // create torus model along polygon path + indexed_triangle_set model; + model.vertices.reserve(steps * count); + model.indices.reserve(2 * steps * count); + for (size_t i = 0; i < count; ++i) { + const Vec2f point_d = points_d[i]; + const Vec2f norm = points_norm[i]; + for (const auto &[s, c] : sin_cos) { + Vec2f xy = s * norm + point_d; + model.vertices.emplace_back(xy.x(), xy.y(), c); + } + } + + unsigned int prev_i = count - 1; + for (unsigned int i = 0; i < count; ++i) { + // TODO: solve <180, =180 and >180 angle + // to not create self intersection + + // t .. top + // b .. bottom + unsigned int prev_t = (prev_i+1) * steps - 1; + unsigned int t = (i+1) * steps - 1; + for (size_t s = 0; s < steps; ++s) { + unsigned int prev_b = prev_i * steps + s; + unsigned int b = i * steps + s; + model.indices.emplace_back(prev_t, prev_b, t); + model.indices.emplace_back(b, t, prev_b); + prev_t = prev_b; + t = b; + } + prev_i = i; + } + return model; +} + +// select closest contour for each line +TextLines select_closest_contour(const std::vector &line_contours) { + TextLines result; + result.reserve(line_contours.size()); + Vec2d zero(0., 0.); + for (const Polygons &polygons : line_contours){ + if (polygons.empty()) { + result.emplace_back(); + continue; + } + // Improve: use int values and polygons only + // Slic3r::Polygons polygons = union_(polygons); + // std::vector lines = to_lines(polygons); + // AABBTreeIndirect::Tree<2, Point> tree; + // size_t line_idx; + // Point hit_point; + // Point::Scalar distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, line_idx, hit_point); + + ExPolygons expolygons = union_ex(polygons); + std::vector linesf = to_linesf(expolygons); + AABBTreeIndirect::Tree2d tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); + + size_t line_idx; + Vec2d hit_point; + double distance = AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, zero, line_idx, hit_point); + + // conversion between index of point and expolygon + ExPolygonsIndices cvt(expolygons); + ExPolygonsIndex index = cvt.cvt(static_cast(line_idx)); + + const Slic3r::Polygon& polygon = index.is_contour() ? + expolygons[index.expolygons_index].contour : + expolygons[index.expolygons_index].holes[index.hole_index()]; + + Point hit_point_int = hit_point.cast(); + TextLine tl{polygon, PolygonPoint{index.point_index, hit_point_int}}; + result.emplace_back(tl); + } + return result; +} + +inline Eigen::AngleAxis get_rotation() { return Eigen::AngleAxis(-M_PI_2, Vec3d::UnitX()); } + +indexed_triangle_set create_its(const TextLines &lines) +{ + const float model_half_width = 0.75; // [in volume mm] + indexed_triangle_set its; + // create model from polygons + for (const TextLine &line : lines) { + const Slic3r::Polygon &polygon = line.polygon; + if (polygon.empty()) continue; + indexed_triangle_set line_its = its_create_belt(polygon, model_half_width); + //indexed_triangle_set line_its = its_create_torus(polygon, model_half_width); + auto transl = Eigen::Translation3d(0., line.y, 0.); + Transform3d tr = transl * get_rotation(); + its_transform(line_its, tr); + its_merge(its, line_its); + } + return its; +} + +GLModel::Geometry create_geometry(const TextLines &lines) +{ + indexed_triangle_set its = create_its(lines); + + GLModel::Geometry geometry; + geometry.format = {GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3}; + ColorRGBA color(.7f, .7f, .7f, .7f); // Transparent Gray + geometry.color = color; + + geometry.reserve_vertices(its.vertices.size()); + for (Vec3f vertex : its.vertices) + geometry.add_vertex(vertex); + + geometry.reserve_indices(its.indices.size() * 3); + for (Vec3i t : its.indices) + geometry.add_triangle(t[0], t[1], t[2]); + return geometry; +} +} // namespace + +void TextLinesModel::init(const Transform3d &text_tr, + const ModelVolumePtrs &volumes_to_slice, + FontProp::VerticalAlign align, + double line_height, + double offset, + unsigned count_lines) +{ + m_model.reset(); + m_lines.clear(); + + double first_line_center = offset + this->offset + get_align_y_offset(align, count_lines, line_height); + std::vector line_centers(count_lines); + for (size_t i = 0; i < count_lines; ++i) + line_centers[i] = static_cast(first_line_center - i * line_height); + + // contour transformation + Transform3d c_trafo = text_tr * get_rotation(); + Transform3d c_trafo_inv = c_trafo.inverse(); + + std::vector line_contours(count_lines); + for (const ModelVolume *volume : volumes_to_slice) { + MeshSlicingParams slicing_params; + slicing_params.trafo = c_trafo_inv * volume->get_matrix(); + for (size_t i = 0; i < count_lines; ++i) { + const Polygons polys = Slic3r::slice_mesh(volume->mesh().its, line_centers[i], slicing_params); + if (polys.empty()) + continue; + Polygons &contours = line_contours[i]; + contours.insert(contours.end(), polys.begin(), polys.end()); + } + } + + m_lines = select_closest_contour(line_contours); + assert(m_lines.size() == count_lines); + assert(line_centers.size() == count_lines); + for (size_t i = 0; i < count_lines; ++i) + m_lines[i].y = line_centers[i]; + + //* + GLModel::Geometry geometry = create_geometry(m_lines); + if (geometry.vertices_count() == 0 || geometry.indices_count() == 0) + return; + m_model.init_from(std::move(geometry)); + /*/ + // slower solution + ColorRGBA color(.7f, .7f, .7f, .7f); // Transparent Gray + m_model.set_color(color); + m_model.init_from(create_its(m_lines)); + //*/ +} + +void TextLinesModel::render(const Transform3d &text_world) +{ + if (!m_model.is_initialized()) + return; + + GUI_App &app = wxGetApp(); + const GLShaderProgram *shader = app.get_shader("flat"); + if (shader == nullptr) + return; + + const Camera &camera = app.plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * text_world); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + bool is_depth_test = glIsEnabled(GL_DEPTH_TEST); + if (!is_depth_test) + glsafe(::glEnable(GL_DEPTH_TEST)); + + bool is_blend = glIsEnabled(GL_BLEND); + if (!is_blend) + glsafe(::glEnable(GL_BLEND)); + // glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + m_model.render(); + + if (!is_depth_test) + glsafe(::glDisable(GL_DEPTH_TEST)); + if (!is_blend) + glsafe(::glDisable(GL_BLEND)); + + shader->stop_using(); +} + +double TextLinesModel::calc_line_height(const Slic3r::Emboss::FontFile &ff, const FontProp &fp) +{ + int line_height = Emboss::get_line_height(ff, fp); // In shape size + double scale = Emboss::get_shape_scale(fp, ff); + return line_height * scale; +} diff --git a/src/slic3r/GUI/TextLines.hpp b/src/slic3r/GUI/TextLines.hpp new file mode 100644 index 0000000000..2a5f8ca8fa --- /dev/null +++ b/src/slic3r/GUI/TextLines.hpp @@ -0,0 +1,48 @@ +#ifndef slic3r_TextLines_hpp_ +#define slic3r_TextLines_hpp_ + +#include +#include +#include +#include +#include "slic3r/GUI/GLModel.hpp" + +namespace Slic3r { +class ModelVolume; +typedef std::vector ModelVolumePtrs; +} + +namespace Slic3r::GUI { +class TextLinesModel +{ +public: + // line offset in y direction (up/down) + float offset = 0; + + /// + /// Initialize model and lines + /// + /// Transformation of text volume inside object (aka inside of instance) + /// Vector of volumes to be sliced + /// Vertical (Y) align of the text + /// Distance between lines [in mm] + /// Offset from baseline [in mm] + /// Count lines(slices over volumes) + void init(const Transform3d &text_tr, const ModelVolumePtrs& volumes_to_slice, FontProp::VerticalAlign align, double line_height, double offset, unsigned count_lines); + + void render(const Transform3d &text_world); + + bool is_init() const { return m_model.is_initialized(); } + void reset() { m_model.reset(); m_lines.clear(); } + const Slic3r::Emboss::TextLines &get_lines() const { return m_lines; } + + static double calc_line_height(const Slic3r::Emboss::FontFile& ff, const FontProp& fp); // return lineheight in mm +private: + Slic3r::Emboss::TextLines m_lines; + + // Keep model for visualization text lines + GLModel m_model; +}; + +} // namespace Slic3r::GUI +#endif // slic3r_TextLines_hpp_ \ No newline at end of file diff --git a/src/slic3r/Utils/EmbossStyleManager.cpp b/src/slic3r/Utils/EmbossStyleManager.cpp index 2653e5c1df..67aafcf8b9 100644 --- a/src/slic3r/Utils/EmbossStyleManager.cpp +++ b/src/slic3r/Utils/EmbossStyleManager.cpp @@ -295,6 +295,25 @@ void StyleManager::clear_glyphs_cache() void StyleManager::clear_imgui_font() { m_style_cache.atlas.Clear(); } +#include "slic3r/GUI/TextLines.hpp" +double StyleManager::get_line_height() +{ + assert(is_active_font()); + if (!is_active_font()) + return -1; + const auto &ffc = get_font_file_with_cache(); + assert(ffc.has_value()); + if (!ffc.has_value()) + return -1; + const auto &ff_ptr = ffc.font_file; + assert(ff_ptr != nullptr); + if (ff_ptr == nullptr) + return -1; + const FontProp &fp = get_font_prop(); + const FontFile &ff = *ff_ptr; + return TextLinesModel::calc_line_height(ff, fp); +} + ImFont *StyleManager::get_imgui_font() { if (!is_active_font()) return nullptr; diff --git a/src/slic3r/Utils/EmbossStyleManager.hpp b/src/slic3r/Utils/EmbossStyleManager.hpp index 492d3f51c4..4f318c1a02 100644 --- a/src/slic3r/Utils/EmbossStyleManager.hpp +++ b/src/slic3r/Utils/EmbossStyleManager.hpp @@ -108,6 +108,10 @@ public: // remove cached imgui font for actual selected font void clear_imgui_font(); + // calculate line height + // not const because access to font file which could be created. + double get_line_height(); /* const */ + // getters for private data const Style *get_stored_style() const; diff --git a/tests/libslic3r/test_emboss.cpp b/tests/libslic3r/test_emboss.cpp index 29d42ed484..cd287776e8 100644 --- a/tests/libslic3r/test_emboss.cpp +++ b/tests/libslic3r/test_emboss.cpp @@ -318,7 +318,9 @@ nor why it should choose to collapse on Betelgeuse Seven\"."; Emboss::FontFileWithCache ffwc(std::move(font)); FontProp fp{line_height, depth}; - ExPolygons shapes = Emboss::text2shapes(ffwc, text.c_str(), fp); + + auto was_canceled = []() { return false; }; + ExPolygons shapes = Emboss::text2shapes(ffwc, text.c_str(), fp, was_canceled); REQUIRE(!shapes.empty()); Emboss::ProjectZ projection(depth);