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);