diff --git a/resources/icons/add_modifier.svg b/resources/icons/add_modifier.svg index c3cfaabbb2..1b3d77f683 100644 --- a/resources/icons/add_modifier.svg +++ b/resources/icons/add_modifier.svg @@ -1,13 +1,4 @@ - - - - - - - + + + diff --git a/resources/icons/add_negative.svg b/resources/icons/add_negative.svg index 99e37b7d72..b38cf471d9 100644 --- a/resources/icons/add_negative.svg +++ b/resources/icons/add_negative.svg @@ -1,15 +1,4 @@ - - - - - - - - - + + + diff --git a/resources/icons/add_part.svg b/resources/icons/add_part.svg index 5f0afdcc35..4f7fd4e281 100644 --- a/resources/icons/add_part.svg +++ b/resources/icons/add_part.svg @@ -1,19 +1,4 @@ - - - - - - - - - - - - + + + diff --git a/resources/icons/add_text_modifier.svg b/resources/icons/add_text_modifier.svg index 0a8376741f..1a9f465c5c 100644 --- a/resources/icons/add_text_modifier.svg +++ b/resources/icons/add_text_modifier.svg @@ -1,4 +1,4 @@ - - + + diff --git a/resources/icons/add_text_negative.svg b/resources/icons/add_text_negative.svg index 866dd076e0..fc07bf4993 100644 --- a/resources/icons/add_text_negative.svg +++ b/resources/icons/add_text_negative.svg @@ -1,4 +1,4 @@ - - + + diff --git a/resources/icons/add_text_part.svg b/resources/icons/add_text_part.svg index 4a75a5d833..5bd190d24c 100644 --- a/resources/icons/add_text_part.svg +++ b/resources/icons/add_text_part.svg @@ -1,4 +1,4 @@ - - + + diff --git a/resources/icons/burn.svg b/resources/icons/burn.svg new file mode 100644 index 0000000000..e7b58cd83f --- /dev/null +++ b/resources/icons/burn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/question.svg b/resources/icons/question.svg index 5463243c68..06d4d0e82a 100644 --- a/resources/icons/question.svg +++ b/resources/icons/question.svg @@ -1,10 +1,4 @@ - - - - - - - + + + diff --git a/resources/icons/reflection_x.svg b/resources/icons/reflection_x.svg new file mode 100644 index 0000000000..7b9e18cb5b --- /dev/null +++ b/resources/icons/reflection_x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/reflection_y.svg b/resources/icons/reflection_y.svg new file mode 100644 index 0000000000..97581af53a --- /dev/null +++ b/resources/icons/reflection_y.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/refresh.svg b/resources/icons/refresh.svg new file mode 100644 index 0000000000..c9dedef26f --- /dev/null +++ b/resources/icons/refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/svg_modifier.svg b/resources/icons/svg_modifier.svg new file mode 100644 index 0000000000..97cf900472 --- /dev/null +++ b/resources/icons/svg_modifier.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/svg_negative.svg b/resources/icons/svg_negative.svg new file mode 100644 index 0000000000..5335a7847e --- /dev/null +++ b/resources/icons/svg_negative.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/svg_part.svg b/resources/icons/svg_part.svg new file mode 100644 index 0000000000..5686024889 --- /dev/null +++ b/resources/icons/svg_part.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/localization/list.txt b/resources/localization/list.txt index df800c95a6..8895f4a932 100644 --- a/resources/localization/list.txt +++ b/resources/localization/list.txt @@ -44,7 +44,6 @@ src/slic3r/GUI/GCodeViewer.cpp src/slic3r/GUI/Gizmos/GLGizmoCut.cpp src/slic3r/GUI/Gizmos/GLGizmoCut.hpp src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp -src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -62,6 +61,7 @@ src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp src/slic3r/GUI/Gizmos/GLGizmosManager.cpp src/slic3r/GUI/GLCanvas3D.cpp src/slic3r/GUI/GUI.cpp diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 3fdcda0c5c..f05e522c23 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -121,45 +121,77 @@ inline std::tuple coordinate_aligned_ray_hit_count(size_t } template -inline std::vector> get_intersections_with_line(size_t node_idx, - const TreeType &tree, - const std::vector &lines, - const LineType &line, - const typename TreeType::BoundingBox &line_bb) +inline void insert_intersections_with_line(std::vector> &result, + size_t node_idx, + const TreeType &tree, + const std::vector &lines, + const LineType &line, + const typename TreeType::BoundingBox &line_bb) { const auto &node = tree.node(node_idx); assert(node.is_valid()); if (node.is_leaf()) { VectorType intersection_pt; if (line_alg::intersection(line, lines[node.idx], &intersection_pt)) { - return {std::pair(intersection_pt, node.idx)}; - } else { - return {}; + result.emplace_back(intersection_pt, node.idx); } - } else { - size_t left_node_idx = node_idx * 2 + 1; - size_t right_node_idx = left_node_idx + 1; - const auto &node_left = tree.node(left_node_idx); - const auto &node_right = tree.node(right_node_idx); - assert(node_left.is_valid()); - assert(node_right.is_valid()); - - std::vector> result; - - if (node_left.bbox.intersects(line_bb)) { - std::vector> intersections = - get_intersections_with_line(left_node_idx, tree, lines, line, line_bb); - result.insert(result.end(), intersections.begin(), intersections.end()); - } - - if (node_right.bbox.intersects(line_bb)) { - std::vector> intersections = - get_intersections_with_line(right_node_idx, tree, lines, line, line_bb); - result.insert(result.end(), intersections.begin(), intersections.end()); - } - - return result; + return; } + + size_t left_node_idx = node_idx * 2 + 1; + size_t right_node_idx = left_node_idx + 1; + const auto &node_left = tree.node(left_node_idx); + const auto &node_right = tree.node(right_node_idx); + assert(node_left.is_valid()); + assert(node_right.is_valid()); + + if (node_left.bbox.intersects(line_bb)) { + insert_intersections_with_line(result, left_node_idx, tree, lines, line, line_bb); + } + + if (node_right.bbox.intersects(line_bb)) { + insert_intersections_with_line(result, right_node_idx, tree, lines, line, line_bb); + } + + //// NOTE: Non recursive implementation - for my case was slower ;-( + // std::vector node_indicies_for_check; // evaluation queue + // size_t approx_size = static_cast(std::ceil(std::sqrt(tree.nodes().size()))); + // node_indicies_for_check.reserve(approx_size); + // do { + // const auto &node = tree.node(node_index); + // assert(node.is_valid()); + // if (node.is_leaf()) { + // VectorType intersection_pt; + // if (line_alg::intersection(line, lines[node.idx], &intersection_pt)) + // result.emplace_back(intersection_pt, node.idx); + // node_index = 0;// clear next node + // } else { + // size_t left_node_idx = node_index * 2 + 1; + // size_t right_node_idx = left_node_idx + 1; + // const auto &node_left = tree.node(left_node_idx); + // const auto &node_right = tree.node(right_node_idx); + // assert(node_left.is_valid()); + // assert(node_right.is_valid()); + + // // Set next node index + // node_index = 0; // clear next node + // if (node_left.bbox.intersects(line_bb)) + // node_index = left_node_idx; + + // if (node_right.bbox.intersects(line_bb)) { + // if (node_index == 0) + // node_index = right_node_idx; + // else + // node_indicies_for_check.push_back(right_node_idx); // store for later evaluation + // } + // } + + // if (node_index == 0 && !node_indicies_for_check.empty()) { + // // no direct next node take one from queue + // node_index = node_indicies_for_check.back(); + // node_indicies_for_check.pop_back(); + // } + //} while (node_index != 0); } } // namespace detail @@ -277,7 +309,9 @@ inline std::vector> get_intersections_with_line(co auto line_bb = typename TreeType::BoundingBox(line.a, line.a); line_bb.extend(line.b); - auto intersections = detail::get_intersections_with_line(0, tree, lines, line, line_bb); + std::vector> intersections; // result + detail::insert_intersections_with_line(intersections, 0, tree, lines, line, line_bb); + if (sorted) { using Floating = typename std::conditional::value, typename LineType::Scalar, double>::type; @@ -293,7 +327,6 @@ inline std::vector> get_intersections_with_line(co intersections[i] = points_with_sq_distance[i].second; } } - return intersections; } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index bd3a426181..085cae66cb 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -69,9 +69,11 @@ set(SLIC3R_SOURCES ElephantFootCompensation.hpp Emboss.cpp Emboss.hpp + EmbossShape.hpp enum_bitmask.hpp ExPolygon.cpp ExPolygon.hpp + ExPolygonSerialize.hpp ExPolygonsIndex.cpp ExPolygonsIndex.hpp Extruder.cpp diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 099cdbeb79..1c2e1b9600 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -282,8 +282,8 @@ bool has_duplicate_points(const ClipperLib::PolyTree &polytree) // Offset CCW contours outside, CW contours (holes) inside. // Don't calculate union of the output paths. -template -static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +template +static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType endType = ClipperLib::etClosedPolygon) { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); @@ -375,11 +375,11 @@ inline ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &inpu return PolyTreeToExPolygons(clipper_union(input, do_union ? ClipperLib::pftNonZero : ClipperLib::pftEvenOdd)); } -template -static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +template +static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type = ClipperLib::etOpenButt) { assert(offset > 0); - return raw_offset(std::forward(paths), offset, joinType, miterLimit); + return raw_offset(std::forward(paths), offset, joinType, miterLimit, end_type); } template @@ -432,10 +432,17 @@ Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, Cli Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) { return PolyTreeToExPolygons(offset_paths(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit)); } -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit))); } -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit))); } +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) + { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit, end_type))); } +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) + { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit, end_type))); } + +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type, double miter_limit){ + assert(line_width > 1.f); return to_polygons(clipper_union( + raw_offset(ClipperUtils::SinglePathProvider(polygon.points), line_width/2, join_type, miter_limit, ClipperLib::etClosedLine)));} +Polygons contour_to_polygons(const Polygons &polygons, const float line_width, ClipperLib::JoinType join_type, double miter_limit){ + assert(line_width > 1.f); return to_polygons(clipper_union( + raw_offset(ClipperUtils::PolygonsProvider(polygons), line_width/2, join_type, miter_limit, ClipperLib::etClosedLine)));} // returns number of expolygons collected (0 or 1). static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 7d6f00b65a..454ceb74c7 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -34,6 +34,9 @@ class BoundingBox; static constexpr const float ClipperSafetyOffset = 10.f; static constexpr const Slic3r::ClipperLib::JoinType DefaultJoinType = Slic3r::ClipperLib::jtMiter; + +static constexpr const Slic3r::ClipperLib::EndType DefaultEndType = Slic3r::ClipperLib::etOpenButt; + //FIXME evaluate the default miter limit. 3 seems to be extreme, Cura uses 1.2. // Mitter Limit 3 is useful for perimeter generator, where sharp corners are extruded without needing a gap fill. // However such a high limit causes issues with large positive or negative offsets, where a sharp corner @@ -341,8 +344,8 @@ Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, Clipp // offset Polylines // Wherever applicable, please use the expand() / shrink() variants instead, they convey their purpose better. // Input polygons for negative offset shall be "normalized": There must be no overlap / intersections between the input polygons. -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit); -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit); +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); @@ -354,6 +357,10 @@ Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float d Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::ExPolygons offset_ex(const Slic3r::SurfacesPtr &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); +// convert stroke to path by offsetting of contour +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); +Polygons contour_to_polygons(const Polygons &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); + inline Slic3r::Polygons union_safety_offset (const Slic3r::Polygons &polygons) { return offset (polygons, ClipperSafetyOffset); } inline Slic3r::Polygons union_safety_offset (const Slic3r::ExPolygons &expolygons) { return offset (expolygons, ClipperSafetyOffset); } inline Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::Polygons &polygons) { return offset_ex(polygons, ClipperSafetyOffset); } diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp index 13868d9aee..b2cf3d9659 100644 --- a/src/libslic3r/CutSurface.cpp +++ b/src/libslic3r/CutSurface.cpp @@ -453,16 +453,21 @@ ProjectionDistances choose_best_distance( /// /// For each point selected closest distance /// All patches -/// All patches +/// Shape to cut +/// Bound of shapes +/// +/// +/// +/// /// Mask of used patch std::vector select_patches(const ProjectionDistances &best_distances, const SurfacePatches &patches, - - const ExPolygons &shapes, - const ExPolygonsIndices &s2i, - const VCutAOIs &cutAOIs, - const CutMeshes &meshes, - const Project &projection); + const ExPolygons &shapes, + const BoundingBox &shapes_bb, + const ExPolygonsIndices &s2i, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes, + const Project &projection); /// /// Merge two surface cuts together @@ -605,8 +610,7 @@ SurfaceCut Slic3r::cut_surface(const ExPolygons &shapes, // Use only outline points // for each point select best projection priv::ProjectionDistances best_projection = priv::choose_best_distance(distances, shapes, start, s2i, patches); - std::vector use_patch = priv::select_patches(best_projection, patches, - shapes, s2i,model_cuts, cgal_models, projection); + std::vector use_patch = priv::select_patches(best_projection, patches, shapes, shapes_bb, s2i, model_cuts, cgal_models, projection); SurfaceCut result = merge_patches(patches, use_patch); //*/ @@ -1917,6 +1921,26 @@ uint32_t priv::find_closest_point_index(const Point &p, const std::vector &mask) { SearchData sd = create_search_data(shapes, mask); + if (sd.tree.nodes().size() == 0){ + // no lines in expolygon, check whether exist point to start + double closest_square_distance = INFINITY; + uint32_t closest_id = -1; + for (uint32_t i = 0; i < mask.size(); i++) + if (mask[i]){ + ExPolygonsIndex ei = s2i.cvt(i); + const Point& s_p = ei.is_contour()? + shapes[ei.expolygons_index].contour[ei.point_index]: + shapes[ei.expolygons_index].holes[ei.hole_index()][ei.point_index]; + double square_distance = (p - s_p).cast().squaredNorm(); + if (closest_id >= mask.size() || + closest_square_distance > square_distance) { + closest_id = i; + closest_square_distance = square_distance; + } + } + assert(closest_id < mask.size()); + return closest_id; + } size_t line_idx = std::numeric_limits::max(); Vec2d hit_point; Vec2d p_d = p.cast(); @@ -2222,7 +2246,11 @@ priv::ProjectionDistances priv::choose_best_distance( // Select point from shapes(text contour) which is closest to center (all in 2d) uint32_t unfinished_index = find_closest_point_index(start, shapes, s2i, mask_distances); - + assert(unfinished_index < s2i.get_count()); + if (unfinished_index >= s2i.get_count()) + // no point to select + return result; + #ifdef DEBUG_OUTPUT_DIR Connections connections; connections.reserve(shapes.size()); @@ -3198,15 +3226,17 @@ bool priv::is_over_whole_expoly(const CutAOI &cutAOI, std::vector priv::select_patches(const ProjectionDistances &best_distances, const SurfacePatches &patches, - - const ExPolygons &shapes, - const ExPolygonsIndices &s2i, - const VCutAOIs &cutAOIs, - const CutMeshes &meshes, - const Project &projection) + const ExPolygons &shapes, + const BoundingBox &shapes_bb, + const ExPolygonsIndices &s2i, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes, + const Project &projection) { // extension to cover numerical mistake made by back projection patch from 3d to 2d - const float extend_delta = 5.f / Emboss::SHAPE_SCALE; // [Font points scaled by Emboss::SHAPE_SCALE] + // Calculated as one percent of average size(width and height) + Point s = shapes_bb.size(); + const float extend_delta = (s.x() + s.y())/ float(2 * 100); // vector of patches for shape std::vector> used_shapes_patches(shapes.size()); diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index a0fc0d7c4d..0ff5ffc0ba 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -27,18 +27,23 @@ // to get approx center of normal text line const double ASCENT_CENTER = 1/3.; // 0.5 is above small letter +// every glyph's shape point is divided by SHAPE_SCALE - increase precission of fixed point value +// stored in fonts (to be able represents curve by sequence of lines) +static constexpr double SHAPE_SCALE = 0.001; // SCALING_FACTOR promile is fine enough +static unsigned MAX_HEAL_ITERATION_OF_TEXT = 10; + using namespace Slic3r; using namespace Emboss; using fontinfo_opt = std::optional; -// for try approach to heal shape by Clipper::Closing -//#define HEAL_WITH_CLOSING +// NOTE: approach to heal shape by Clipper::Closing is not working // functionality to remove all spikes from shape +// Potentionaly useable for eliminate spike in layer //#define REMOVE_SPIKES // do not expose out of this file stbtt_ data types -namespace priv{ +namespace{ using Polygon = Slic3r::Polygon; bool is_valid(const FontFile &font, unsigned int index); fontinfo_opt load_font_info(const unsigned char *data, unsigned int index = 0); @@ -48,7 +53,7 @@ std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_lett const Glyph* get_glyph(int unicode, const FontFile &font, const FontProp &font_prop, Glyphs &cache, fontinfo_opt &font_info_opt); -EmbossStyle create_style(std::wstring name, std::wstring path); +EmbossStyle create_style(const std::wstring &name, const std::wstring &path); // scale and convert float to int coordinate Point to_point(const stbtt__point &point); @@ -57,14 +62,7 @@ Point to_point(const stbtt__point &point); void remove_bad(Polygons &polygons); void remove_bad(ExPolygons &expolygons); -// helpr for heal shape -// Return true when erase otherwise false -bool remove_same_neighbor(Polygon &points); -bool remove_same_neighbor(Polygons &polygons); -bool remove_same_neighbor(ExPolygons &expolygons); - // Try to remove self intersection by subtracting rect 2x2 px -bool remove_self_intersections(ExPolygons &shape, unsigned max_iteration = 10); ExPolygon create_bounding_rect(const ExPolygons &shape); void remove_small_islands(ExPolygons &shape, double minimal_area); @@ -94,17 +92,17 @@ struct SpikeDesc /// /// Size of spike width after cut of the tip, has to be grater than 2.5 /// When spike has same or more pixels with width less than 1 pixel - SpikeDesc(double bevel_size, double pixel_spike_length = 6) - { + SpikeDesc(double bevel_size, double pixel_spike_length = 6): // create min angle given by spike_length // Use it as minimal height of 1 pixel base spike - double angle = 2. * atan2(pixel_spike_length, .5); // [rad] - cos_angle = std::fabs(cos(angle)); + cos_angle(std::fabs(std::cos( + /*angle*/ 2. * std::atan2(pixel_spike_length, .5) + ))), // When remove spike this angle is set. // Value must be grater than min_angle - half_bevel = bevel_size / 2; - } + half_bevel(bevel_size / 2) + {} }; // return TRUE when remove point. It could create polygon with 2 points. @@ -120,9 +118,9 @@ void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc); void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc); #endif -}; - -bool priv::remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &spike_desc) { +// spike ... very sharp corner - when not removed cause iteration of heal process +// index ... index of duplicit point in polygon +bool remove_when_spike(Slic3r::Polygon &polygon, size_t index, const SpikeDesc &spike_desc) { std::optional add; bool do_erase = false; @@ -206,9 +204,10 @@ bool priv::remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &sp return false; } -void priv::remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates) { - - auto check = [](Polygon &polygon, const Point &d) -> bool { +void remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates) { + if (duplicates.empty()) + return; + auto check = [](Slic3r::Polygon &polygon, const Point &d) -> bool { double spike_bevel = 1 / SHAPE_SCALE; double spike_length = 5.; const static SpikeDesc sd(spike_bevel, spike_length); @@ -238,14 +237,14 @@ void priv::remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &dup remove_bad(expolygons); } -bool priv::is_valid(const FontFile &font, unsigned int index) { +bool is_valid(const FontFile &font, unsigned int index) { if (font.data == nullptr) return false; if (font.data->empty()) return false; if (index >= font.infos.size()) return false; return true; } -fontinfo_opt priv::load_font_info( +fontinfo_opt load_font_info( const unsigned char *data, unsigned int index) { int font_offset = stbtt_GetFontOffsetForIndex(data, index); @@ -263,14 +262,14 @@ fontinfo_opt priv::load_font_info( return font_info; } -void priv::remove_bad(Polygons &polygons) { +void remove_bad(Polygons &polygons) { polygons.erase( std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.size() < 3; }), polygons.end()); } -void priv::remove_bad(ExPolygons &expolygons) { +void remove_bad(ExPolygons &expolygons) { expolygons.erase( std::remove_if(expolygons.begin(), expolygons.end(), [](const ExPolygon &p) { return p.contour.size() < 3; }), @@ -280,56 +279,7 @@ void priv::remove_bad(ExPolygons &expolygons) { remove_bad(expolygon.holes); } -bool priv::remove_same_neighbor(Slic3r::Polygon &polygon) -{ - Points &points = polygon.points; - if (points.empty()) return false; - auto last = std::unique(points.begin(), points.end()); - - // remove first and last neighbor duplication - if (const Point& last_point = *(last - 1); - last_point == points.front()) { - --last; - } - - // no duplicits - if (last == points.end()) return false; - - points.erase(last, points.end()); - return true; -} - -bool priv::remove_same_neighbor(Polygons &polygons) { - if (polygons.empty()) return false; - bool exist = false; - for (Polygon& polygon : polygons) - exist |= remove_same_neighbor(polygon); - // remove empty polygons - polygons.erase( - std::remove_if(polygons.begin(), polygons.end(), - [](const Polygon &p) { return p.points.size() <= 2; }), - polygons.end()); - return exist; -} - -bool priv::remove_same_neighbor(ExPolygons &expolygons) { - if(expolygons.empty()) return false; - bool remove_from_holes = false; - bool remove_from_contour = false; - for (ExPolygon &expoly : expolygons) { - remove_from_contour |= remove_same_neighbor(expoly.contour); - remove_from_holes |= remove_same_neighbor(expoly.holes); - } - // Removing of expolygons without contour - if (remove_from_contour) - expolygons.erase( - std::remove_if(expolygons.begin(), expolygons.end(), - [](const ExPolygon &p) { return p.contour.points.size() <=2; }), - expolygons.end()); - return remove_from_holes || remove_from_contour; -} - -Points priv::collect_close_points(const ExPolygons &expolygons, double distance) { +Points collect_close_points(const ExPolygons &expolygons, double distance) { if (expolygons.empty()) return {}; if (distance < 0.) return {}; @@ -378,13 +328,15 @@ Points priv::collect_close_points(const ExPolygons &expolygons, double distance) return res; } +} // end namespace + bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double distance) { if (expolygons.empty()) return false; if (distance < 0.) return false; // ExPolygons can't contain same neigbours - priv::remove_same_neighbor(expolygons); + remove_same_neighbor(expolygons); // IMPROVE: use int(insted of double) lines and tree const ExPolygonsIndices ids(expolygons); @@ -481,72 +433,20 @@ bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double dist return true; } -bool priv::remove_self_intersections(ExPolygons &shape, unsigned max_iteration) { - if (shape.empty()) - return true; - - Pointfs intersections_f = intersection_points(shape); - if (intersections_f.empty()) - return true; - - // create loop permanent memory - Polygons holes; - Points intersections; - - while (--max_iteration) { - // convert intersections into Points - assert(intersections.empty()); - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return Point(std::floor(p.x()), std::floor(p.y())); }); - - // intersections should be unique poits - std::sort(intersections.begin(), intersections.end()); - auto it = std::unique(intersections.begin(), intersections.end()); - intersections.erase(it, intersections.end()); - - assert(holes.empty()); - holes.reserve(intersections.size()); - - // Fix self intersection in result by subtracting hole 2x2 - for (const Point &p : intersections) { - Polygon hole(priv::pts_2x2); - hole.translate(p); - holes.push_back(hole); - } - // Union of overlapped holes is not neccessary - // Clipper calculate winding number separately for each input parameter - // if (holes.size() > 1) holes = Slic3r::union_(holes); - shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::Yes); - - // TODO: find where diff ex could create same neighbor - priv::remove_same_neighbor(shape); - - // find new intersections made by diff_ex - intersections_f = intersection_points(shape); - if (intersections_f.empty()) - return true; - else { - // clear permanent vectors - holes.clear(); - intersections.clear(); - } - } - assert(max_iteration == 0); - assert(!intersections_f.empty()); - return false; -} - -ExPolygons Emboss::heal_shape(const Polygons &shape) +HealedExPolygons Emboss::heal_polygons(const Polygons &shape, bool is_non_zero, unsigned int max_iteration) { + const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::PolyFillType fill_type = is_non_zero ? + ClipperLib::pftNonZero : ClipperLib::pftEvenOdd; + // When edit this code check that font 'ALIENATE.TTF' and glyph 'i' still work // fix of self intersections // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm - ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), ClipperLib::pftNonZero); - const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), fill_type); ClipperLib::CleanPolygons(paths, clean_distance); Polygons polygons = to_polygons(paths); - polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.size() < 3; }), polygons.end()); + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), + [](const Polygon &p) { return p.size() < 3; }), polygons.end()); // Do not remove all duplicates but do it better way // Overlap all duplicit points by rectangle 3x3 @@ -554,140 +454,233 @@ ExPolygons Emboss::heal_shape(const Polygons &shape) if (!duplicits.empty()) { polygons.reserve(polygons.size() + duplicits.size()); for (const Point &p : duplicits) { - Polygon rect_3x3(priv::pts_3x3); + Polygon rect_3x3(pts_3x3); rect_3x3.translate(p); polygons.push_back(rect_3x3); } } - - // TrueTypeFonts use non zero winding number - // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html - ExPolygons res = Slic3r::union_ex(polygons, ClipperLib::pftNonZero); - heal_shape(res); - return res; + ExPolygons res = Slic3r::union_ex(polygons, fill_type); + bool is_healed = heal_expolygons(res, max_iteration); + return {res, is_healed}; } -#include "libslic3r/SVG.hpp" -void priv::visualize_heal(const std::string &svg_filepath, const ExPolygons &expolygons) { - Points pts = to_points(expolygons); + +bool Emboss::heal_expolygons(ExPolygons &shape, unsigned max_iteration) +{ + return ::heal_dupl_inter(shape, max_iteration); +} + +#include "libslic3r/SVG.hpp" // for visualize_heal +namespace { + +Points get_unique_intersections(const Slic3r::IntersectionsLines &intersections) +{ + Points result; + if (intersections.empty()) + return result; + + // convert intersections into Points + result.reserve(intersections.size()); + std::transform(intersections.begin(), intersections.end(), std::back_inserter(result), + [](const Slic3r::IntersectionLines &i) { return Point( + std::floor(i.intersection.x()), + std::floor(i.intersection.y())); + }); + // intersections should be unique poits + std::sort(result.begin(), result.end()); + auto it = std::unique(result.begin(), result.end()); + result.erase(it, result.end()); + return result; +} + +void visualize_heal(const std::string &svg_filepath, const ExPolygons &expolygons) +{ + Points pts = to_points(expolygons); BoundingBox bb(pts); - //double svg_scale = SHAPE_SCALE / unscale(1.); - // bb.scale(svg_scale); + // double svg_scale = SHAPE_SCALE / unscale(1.); + // bb.scale(svg_scale); SVG svg(svg_filepath, bb); svg.draw(expolygons); - - Points duplicits = collect_duplicates(pts); - svg.draw(duplicits, "black", 7 / SHAPE_SCALE); - Pointfs intersections_f = intersection_points(expolygons); - Points intersections; - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return p.cast(); }); - svg.draw(intersections, "red", 8 / SHAPE_SCALE); + Points duplicits = collect_duplicates(pts); + int black_size = std::max(bb.size().x(), bb.size().y()) / 20; + svg.draw(duplicits, "black", black_size); + + Slic3r::IntersectionsLines intersections_f = get_intersections(expolygons); + Points intersections = get_unique_intersections(intersections_f); + svg.draw(intersections, "red", black_size * 1.2); } -bool Emboss::heal_shape(ExPolygons &shape, unsigned max_iteration) +Polygons get_holes_with_points(const Polygons &holes, const Points &points) { - return priv::heal_dupl_inter(shape, max_iteration); + Polygons result; + for (const Slic3r::Polygon &hole : holes) + for (const Point &p : points) + for (const Point &h : hole) + if (p == h) { + result.push_back(hole); + break; + } + return result; } -#ifndef HEAL_WITH_CLOSING -bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) +/// +/// Fill holes which create duplicits or intersections +/// When healing hole creates trouble in shape again try to heal by an union instead of diff_ex +/// +/// Holes which was substracted from shape previous +/// Current duplicates in shape +/// Current intersections in shape +/// Partialy healed shape[could be modified] +/// True when modify shape otherwise False +bool fill_trouble_holes(const Polygons &holes, const Points &duplicates, const Points &intersections, ExPolygons &shape) +{ + if (holes.empty()) + return false; + if (duplicates.empty() && intersections.empty()) + return false; + + Polygons fill = get_holes_with_points(holes, duplicates); + append(fill, get_holes_with_points(holes, intersections)); + if (fill.empty()) + return false; + + shape = union_ex(shape, fill); + return true; +} + +// extend functionality from Points.cpp --> collect_duplicates +// with address of duplicated points +struct Duplicate { + Point point; + std::vector indices; +}; +using Duplicates = std::vector; +Duplicates collect_duplicit_indices(const ExPolygons &expoly) +{ + Points pts = to_points(expoly); + + // initialize original index locations + std::vector idx(pts.size()); + iota(idx.begin(), idx.end(), 0); + std::sort(idx.begin(), idx.end(), + [&pts](uint32_t i1, uint32_t i2) { return pts[i1] < pts[i2]; }); + + Duplicates result; + const Point *prev = &pts[idx.front()]; + for (size_t i = 1; i < idx.size(); ++i) { + uint32_t index = idx[i]; + const Point *act = &pts[index]; + if (*prev == *act) { + // duplicit point + if (!result.empty() && result.back().point == *act) { + // more than 2 points with same coordinate + result.back().indices.push_back(index); + } else { + uint32_t prev_index = idx[i-1]; + result.push_back({*act, {prev_index, index}}); + } + continue; + } + prev = act; + } + return result; +} + +Points get_points(const Duplicates& duplicate_indices) +{ + Points result; + if (duplicate_indices.empty()) + return result; + + // convert intersections into Points + result.reserve(duplicate_indices.size()); + std::transform(duplicate_indices.begin(), duplicate_indices.end(), std::back_inserter(result), + [](const Duplicate &d) { return d.point; }); + return result; +} + +bool heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) { if (shape.empty()) return true; + remove_same_neighbor(shape); // create loop permanent memory Polygons holes; - Points intersections; - while (--max_iteration) { - priv::remove_same_neighbor(shape); - Pointfs intersections_f = intersection_points(shape); - - // convert intersections into Points - assert(intersections.empty()); - intersections.reserve(intersections_f.size()); - std::transform(intersections_f.begin(), intersections_f.end(), std::back_inserter(intersections), - [](const Vec2d &p) { return Point(std::floor(p.x()), std::floor(p.y())); }); - - // intersections should be unique poits - std::sort(intersections.begin(), intersections.end()); - auto it = std::unique(intersections.begin(), intersections.end()); - intersections.erase(it, intersections.end()); - - Points duplicates = collect_duplicates(to_points(shape)); - // duplicates are already uniqua and sorted - + while (--max_iteration) { + Duplicates duplicate_indices = collect_duplicit_indices(shape); + //Points duplicates = collect_duplicates(to_points(shape)); + IntersectionsLines intersections = get_intersections(shape); + // Check whether shape is already healed - if (intersections.empty() && duplicates.empty()) + if (intersections.empty() && duplicate_indices.empty()) return true; - assert(holes.empty()); - holes.reserve(intersections.size() + duplicates.size()); + Points duplicate_points = get_points(duplicate_indices); + Points intersection_points = get_unique_intersections(intersections); - remove_spikes_in_duplicates(shape, duplicates); + if (fill_trouble_holes(holes, duplicate_points, intersection_points, shape)) { + holes.clear(); + continue; + } + + holes.clear(); + holes.reserve(intersections.size() + duplicate_points.size()); + + remove_spikes_in_duplicates(shape, duplicate_points); // Fix self intersection in result by subtracting hole 2x2 - for (const Point &p : intersections) { - Polygon hole(priv::pts_2x2); + for (const Point &p : intersection_points) { + Polygon hole(pts_2x2); hole.translate(p); holes.push_back(hole); } // Fix duplicit points by hole 3x3 around duplicit point - for (const Point &p : duplicates) { - Polygon hole(priv::pts_3x3); + for (const Point &p : duplicate_points) { + Polygon hole(pts_3x3); hole.translate(p); holes.push_back(hole); } - shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::Yes); - - // prepare for next loop - holes.clear(); - intersections.clear(); + shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::No); + // ApplySafetyOffset::Yes is incompatible with function fill_trouble_holes } - //priv::visualize_heal("C:/data/temp/heal.svg", shape); - assert(false); - shape = {priv::create_bounding_rect(shape)}; - return false; -} -#else -bool priv::heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) -{ - priv::remove_same_neighbor(shape); + // Create partialy healed output + Duplicates duplicates = collect_duplicit_indices(shape); + IntersectionsLines intersections = get_intersections(shape); + if (duplicates.empty() && intersections.empty()){ + // healed in the last loop + return true; + } + + // visualize_heal("C:/data/temp/heal.svg", shape); + assert(false); // Can not heal this shape + // investigate how to heal better way - const float delta = 2.f; - const ClipperLib::JoinType joinType = ClipperLib::JoinType::jtRound; - - // remove double points - while (max_iteration) { - --max_iteration; - - // if(!priv::remove_self_intersections(shape, max_iteration)) break; - shape = Slic3r::union_ex(shape); - shape = Slic3r::closing_ex(shape, delta, joinType); - - // double minimal_area = 1000; - // priv::remove_small_islands(shape, minimal_area); - - // check that duplicates and intersections do NOT exists - Points duplicits = collect_duplicates(to_points(shape)); - Pointfs intersections_f = intersection_points(shape); - if (duplicits.empty() && intersections_f.empty()) - return true; + ExPolygonsIndices ei(shape); + std::vector is_healed(shape.size(), {true}); + for (const Duplicate &duplicate : duplicates){ + for (uint32_t i : duplicate.indices) + is_healed[ei.cvt(i).expolygons_index] = false; + } + for (const IntersectionLines &intersection : intersections) { + is_healed[ei.cvt(intersection.line_index1).expolygons_index] = false; + is_healed[ei.cvt(intersection.line_index2).expolygons_index] = false; } - // priv::visualize_heal("C:/data/temp/heal.svg", shape); - assert(false); - shape = {priv::create_bounding_rect(shape)}; + for (size_t shape_index = 0; shape_index < shape.size(); shape_index++) { + if (!is_healed[shape_index]) { + // exchange non healed expoly with bb rect + ExPolygon &expoly = shape[shape_index]; + expoly = create_bounding_rect({expoly}); + } + } return false; } -#endif // !HEAL_WITH_CLOSING -ExPolygon priv::create_bounding_rect(const ExPolygons &shape) { +ExPolygon create_bounding_rect(const ExPolygons &shape) { BoundingBox bb = get_extents(shape); Point size = bb.size(); if (size.x() < 10) @@ -711,7 +704,7 @@ ExPolygon priv::create_bounding_rect(const ExPolygons &shape) { return ExPolygon(rect, hole); } -void priv::remove_small_islands(ExPolygons &expolygons, double minimal_area) { +void remove_small_islands(ExPolygons &expolygons, double minimal_area) { if (expolygons.empty()) return; @@ -729,7 +722,7 @@ void priv::remove_small_islands(ExPolygons &expolygons, double minimal_area) { } } -std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness) +std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness) { int glyph_index = stbtt_FindGlyphIndex(&font_info, unicode_letter); if (glyph_index == 0) { @@ -787,12 +780,18 @@ std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicod std::reverse(pts.begin(), pts.end()); glyph_polygons.emplace_back(pts); } - if (!glyph_polygons.empty()) - glyph.shape = Emboss::heal_shape(glyph_polygons); + if (!glyph_polygons.empty()) { + unsigned max_iteration = 10; + // TrueTypeFonts use non zero winding number + // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + bool is_non_zero = true; + glyph.shape = Emboss::heal_polygons(glyph_polygons, is_non_zero, max_iteration); + } return glyph; } -const Glyph* priv::get_glyph( +const Glyph* get_glyph( int unicode, const FontFile & font, const FontProp & font_prop, @@ -809,7 +808,7 @@ const Glyph* priv::get_glyph( if (!font_info_opt.has_value()) { - font_info_opt = priv::load_font_info(font.data->data(), font_index); + font_info_opt = load_font_info(font.data->data(), font_index); // can load font info? if (!font_info_opt.has_value()) return nullptr; } @@ -819,27 +818,24 @@ const Glyph* priv::get_glyph( // Fix for very small flatness because it create huge amount of points from curve if (flatness < RESOLUTION) flatness = RESOLUTION; - std::optional glyph_opt = - priv::get_glyph(*font_info_opt, unicode, flatness); + std::optional glyph_opt = get_glyph(*font_info_opt, unicode, flatness); // IMPROVE: multiple loadig glyph without data // has definition inside of font? if (!glyph_opt.has_value()) return nullptr; + Glyph &glyph = *glyph_opt; if (font_prop.char_gap.has_value()) - glyph_opt->advance_width += *font_prop.char_gap; + glyph.advance_width += *font_prop.char_gap; // scale glyph size - glyph_opt->advance_width = - static_cast(glyph_opt->advance_width / SHAPE_SCALE); - glyph_opt->left_side_bearing = - static_cast(glyph_opt->left_side_bearing / SHAPE_SCALE); + glyph.advance_width = static_cast(glyph.advance_width / SHAPE_SCALE); + glyph.left_side_bearing = static_cast(glyph.left_side_bearing / SHAPE_SCALE); - if (!glyph_opt->shape.empty()) { + if (!glyph.shape.empty()) { if (font_prop.boldness.has_value()) { - float delta = *font_prop.boldness / SHAPE_SCALE / - font_prop.size_in_mm; - glyph_opt->shape = Slic3r::union_ex(offset_ex(glyph_opt->shape, delta)); + float delta = static_cast(*font_prop.boldness / SHAPE_SCALE / font_prop.size_in_mm); + glyph.shape = Slic3r::union_ex(offset_ex(glyph.shape, delta)); } if (font_prop.skew.has_value()) { double ratio = *font_prop.skew; @@ -847,28 +843,30 @@ const Glyph* priv::get_glyph( for (Slic3r::Point &p : polygon.points) p.x() += static_cast(std::round(p.y() * ratio)); }; - for (ExPolygon &expolygon : glyph_opt->shape) { + for (ExPolygon &expolygon : glyph.shape) { skew(expolygon.contour); for (Polygon &hole : expolygon.holes) skew(hole); } } } - auto it = cache.insert({unicode, std::move(*glyph_opt)}); - assert(it.second); - return &it.first->second; + auto [it, success] = cache.try_emplace(unicode, std::move(glyph)); + assert(success); + return &it->second; } -EmbossStyle priv::create_style(std::wstring name, std::wstring path) { +EmbossStyle create_style(const std::wstring& name, const std::wstring& path) { return { boost::nowide::narrow(name.c_str()), boost::nowide::narrow(path.c_str()), EmbossStyle::Type::file_path, FontProp() }; } -Point priv::to_point(const stbtt__point &point) { +Point to_point(const stbtt__point &point) { return Point(static_cast(std::round(point.x / SHAPE_SCALE)), static_cast(std::round(point.y / SHAPE_SCALE))); } +} // namespace + #ifdef _WIN32 #include #include @@ -998,7 +996,7 @@ EmbossStyles Emboss::get_font_list_by_register() { if (pos >= font_name_w.size()) continue; // remove TrueType text from name font_name_w = std::wstring(font_name_w, 0, pos); - font_list.emplace_back(priv::create_style(font_name_w, path_w)); + font_list.emplace_back(create_style(font_name_w, path_w)); } while (result != ERROR_NO_MORE_ITEMS); delete[] font_name; delete[] fileTTF_name; @@ -1033,7 +1031,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() { EmbossStyles font_list; for (const std::wstring &font_name : font_names) { - font_list.emplace_back(priv::create_style(font_name, L"")); + font_list.emplace_back(create_style(font_name, L"")); } return font_list; } @@ -1055,7 +1053,7 @@ EmbossStyles Emboss::get_font_list_by_folder() { if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; std::wstring file_name(fd.cFileName); // TODO: find font name instead of filename - result.emplace_back(priv::create_style(file_name, search_dir + file_name)); + result.emplace_back(create_style(file_name, search_dir + file_name)); } while (::FindNextFile(hFind, &fd)); ::FindClose(hFind); } @@ -1089,7 +1087,7 @@ std::unique_ptr Emboss::create_font_file( std::vector infos; infos.reserve(c_size); for (unsigned int i = 0; i < c_size; ++i) { - auto font_info = priv::load_font_info(data->data(), i); + auto font_info = load_font_info(data->data(), i); if (!font_info.has_value()) return nullptr; const stbtt_fontinfo *info = &(*font_info); @@ -1209,16 +1207,16 @@ std::optional Emboss::letter2glyph(const FontFile &font, int letter, float flatness) { - if (!priv::is_valid(font, font_index)) return {}; - auto font_info_opt = priv::load_font_info(font.data->data(), font_index); + if (!is_valid(font, font_index)) return {}; + auto font_info_opt = load_font_info(font.data->data(), font_index); if (!font_info_opt.has_value()) return {}; - return priv::get_glyph(*font_info_opt, letter, flatness); + return get_glyph(*font_info_opt, letter, flatness); } const FontFile::Info &Emboss::get_font_info(const FontFile &font, const FontProp &prop) { unsigned int font_index = prop.collection_number.value_or(0); - assert(priv::is_valid(font, font_index)); + assert(is_valid(font, font_index)); return font.infos[font_index]; } @@ -1249,7 +1247,7 @@ ExPolygons letter2shapes( 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); + const Glyph *space = get_glyph(int(' '), font, font_prop, cache, font_info_cache); if (space == nullptr) return {}; cursor.x() += count_spaces * space->advance_width; @@ -1262,7 +1260,7 @@ ExPolygons letter2shapes( 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); + const Glyph *glyph_ptr = (it != cache.end()) ? &it->second : get_glyph(unicode, font, font_prop, cache, font_info_cache); if (glyph_ptr == nullptr) return {}; @@ -1281,20 +1279,64 @@ ExPolygons letter2shapes( 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) +/// Union shape defined by glyphs +HealedExPolygons Slic3r::union_ex(const ExPolygonsWithIds &shapes, unsigned max_heal_iteration) { - 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; - for (ExPolygons &shapes : vshapes) { - if (shapes.empty()) + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) continue; - expolygons_append(result, std::move(shapes)); + expolygons_append(result, shape.expoly); } - result = Slic3r::union_ex(result); - heal_shape(result); - return result; + result = union_ex(result); + + bool is_healed = heal_expolygons(result, max_heal_iteration); + return {result, is_healed}; +} + +HealedExPolygons Slic3r::union_with_delta(const ExPolygonsWithIds &shapes, float delta, unsigned max_heal_iteration) +{ + // unify to one expolygons + ExPolygons expolygons; + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) + continue; + expolygons_append(expolygons, offset_ex(shape.expoly, delta)); + } + ExPolygons result = union_ex(expolygons); + result = offset_ex(result, -delta); + bool is_healed = heal_expolygons(result, max_heal_iteration); + return {result, is_healed}; +} + +void Slic3r::translate(ExPolygonsWithIds &expolygons_with_ids, const Point &p) +{ + for (ExPolygonsWithId &expolygons_with_id : expolygons_with_ids) + translate(expolygons_with_id.expoly, p); +} + +BoundingBox Slic3r::get_extents(const ExPolygonsWithIds &expolygons_with_ids) +{ + BoundingBox bb; + for (const ExPolygonsWithId &expolygons_with_id : expolygons_with_ids) + bb.merge(get_extents(expolygons_with_id.expoly)); + return bb; +} + +void Slic3r::center(ExPolygonsWithIds &e) +{ + BoundingBox bb = get_extents(e); + translate(e, -bb.center()); +} + +HealedExPolygons 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); + ExPolygonsWithIds vshapes = text2vshapes(font_with_cache, text_w, font_prop, was_canceled); + + float delta = static_cast(1. / SHAPE_SCALE); + return union_with_delta(vshapes, delta, MAX_HEAL_ITERATION_OF_TEXT); } namespace { @@ -1306,21 +1348,21 @@ namespace { /// To detect end of lines - to be able horizontal center the line /// Containe Horizontal and vertical alignment /// Needed for scale and font size -void align_shape(std::vector &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font); +void align_shape(ExPolygonsWithIds &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font); } -std::vector Emboss::text2vshapes(FontFileWithCache &font_with_cache, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled){ +ExPolygonsWithIds 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)) + if (!is_valid(font, font_index)) return {}; unsigned counter = 0; Point cursor(0, 0); fontinfo_opt font_info_cache; - std::vector result; + ExPolygonsWithIds result; result.reserve(text.size()); for (wchar_t letter : text) { if (++counter == CANCEL_CHECK) { @@ -1328,7 +1370,8 @@ std::vector Emboss::text2vshapes(FontFileWithCache &font_with_cache, if (was_canceled()) return {}; } - result.emplace_back(letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache)); + unsigned id = static_cast(letter); + result.push_back({id, letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache)}); } align_shape(result, text, font_prop, font); @@ -1369,8 +1412,14 @@ unsigned Emboss::get_count_lines(const std::string &text) return get_count_lines(ws); } -void Emboss::apply_transformation(const FontProp &font_prop, Transform3d &transformation){ - apply_transformation(font_prop.angle, font_prop.distance, transformation); +unsigned Emboss::get_count_lines(const ExPolygonsWithIds &shapes) { + if (shapes.empty()) + return 0; // no glyphs + unsigned result = 1; // one line is minimum + for (const ExPolygonsWithId &shape_id : shapes) + if (shape_id.id == ENTER_UNICODE) + ++result; + return result; } void Emboss::apply_transformation(const std::optional& angle, const std::optional& distance, Transform3d &transformation) { @@ -1387,7 +1436,7 @@ void Emboss::apply_transformation(const std::optional& angle, const std:: bool Emboss::is_italic(const FontFile &font, unsigned int font_index) { if (font_index >= font.infos.size()) return false; - fontinfo_opt font_info_opt = priv::load_font_info(font.data->data(), font_index); + fontinfo_opt font_info_opt = load_font_info(font.data->data(), font_index); if (!font_info_opt.has_value()) return false; stbtt_fontinfo *info = &(*font_info_opt); @@ -1427,14 +1476,14 @@ std::string Emboss::create_range_text(const std::string &text, unsigned int font_index, bool *exist_unknown) { - if (!priv::is_valid(font, font_index)) return {}; + if (!is_valid(font, font_index)) return {}; std::wstring ws = boost::nowide::widen(text); // need remove symbols not contained in font std::sort(ws.begin(), ws.end()); - auto font_info_opt = priv::load_font_info(font.data->data(), 0); + auto font_info_opt = load_font_info(font.data->data(), 0); if (!font_info_opt.has_value()) return {}; const stbtt_fontinfo *font_info = &(*font_info_opt); @@ -1463,7 +1512,7 @@ std::string Emboss::create_range_text(const std::string &text, return boost::nowide::narrow(ws); } -double Emboss::get_shape_scale(const FontProp &fp, const FontFile &ff) +double Emboss::get_text_shape_scale(const FontProp &fp, const FontFile &ff) { const FontFile::Info &info = get_font_info(ff, fp); double scale = fp.size_in_mm / (double) info.unit_per_em; @@ -1471,7 +1520,7 @@ double Emboss::get_shape_scale(const FontProp &fp, const FontFile &ff) return scale * SHAPE_SCALE; } -namespace priv { +namespace { void add_quad(uint32_t i1, uint32_t i2, @@ -1612,7 +1661,7 @@ indexed_triangle_set polygons2model_duplicit( } return result; } -} // namespace priv +} // namespace indexed_triangle_set Emboss::polygons2model(const ExPolygons &shape2d, const IProjection &projection) @@ -1620,16 +1669,13 @@ indexed_triangle_set Emboss::polygons2model(const ExPolygons &shape2d, Points points = to_points(shape2d); Points duplicits = collect_duplicates(points); return (duplicits.empty()) ? - priv::polygons2model_unique(shape2d, projection, points) : - priv::polygons2model_duplicit(shape2d, projection, points, duplicits); + polygons2model_unique(shape2d, projection, points) : + polygons2model_duplicit(shape2d, projection, points, duplicits); } std::pair Emboss::ProjectZ::create_front_back(const Point &p) const { - Vec3d front( - p.x() * SHAPE_SCALE, - p.y() * SHAPE_SCALE, - 0.); + Vec3d front(p.x(), p.y(), 0.); return std::make_pair(front, project(front)); } @@ -1641,8 +1687,7 @@ Vec3d Emboss::ProjectZ::project(const Vec3d &point) const } std::optional Emboss::ProjectZ::unproject(const Vec3d &p, double *depth) const { - if (depth != nullptr) *depth /= SHAPE_SCALE; - return Vec2d(p.x() / SHAPE_SCALE, p.y() / SHAPE_SCALE); + return Vec2d(p.x(), p.y()); } @@ -1672,23 +1717,21 @@ std::optional Emboss::calc_up(const Transform3d &tr, double up_limit) Vec3d normal = tr_linear.col(2); // scaled matrix has base with different size normal.normalize(); - Vec3d suggested = suggest_up(normal); + Vec3d suggested = suggest_up(normal, up_limit); assert(is_approx(suggested.squaredNorm(), 1.)); Vec3d up = tr_linear.col(1); // tr * UnitY() - up.normalize(); - - double dot = suggested.dot(up); - if (dot >= 1. || dot <= -1.) - return {}; // zero angle - + up.normalize(); Matrix3d m; m.row(0) = up; m.row(1) = suggested; m.row(2) = normal; double det = m.determinant(); - - return -atan2(det, dot); + double dot = suggested.dot(up); + double res = -atan2(det, dot); + if (is_approx(res, 0.)) + return {}; + return res; } Transform3d Emboss::create_transformation_onto_surface(const Vec3d &position, @@ -1969,7 +2012,7 @@ int32_t get_align_x_offset(FontProp::HorizontalAlign align, const BoundingBox &s return 0; } -void align_shape(std::vector &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font) +void align_shape(ExPolygonsWithIds &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font) { // Shapes have to match letters in text assert(shapes.size() == text.length()); @@ -1978,22 +2021,22 @@ void align_shape(std::vector &shapes, const std::wstring &text, cons int y_offset = get_align_y_offset(prop.align.second, count_lines, font, prop); // Speed up for left aligned text - //if (prop.align.first == FontProp::HorizontalAlign::left){ - // // already horizontaly aligned - // for (ExPolygons shape : shapes) - // for (ExPolygon &s : shape) - // s.translate(Point(0, y_offset)); - // return; - //} + if (prop.align.first == FontProp::HorizontalAlign::left){ + // already horizontaly aligned + for (ExPolygonsWithId& shape : shapes) + for (ExPolygon &s : shape.expoly) + s.translate(Point(0, y_offset)); + return; + } BoundingBox shape_bb; - for (const ExPolygons& shape: shapes) - shape_bb.merge(get_extents(shape)); + for (const ExPolygonsWithId& shape: shapes) + shape_bb.merge(get_extents(shape.expoly)); 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])); + line_bb.merge(get_extents(shapes[j].expoly)); return line_bb; }; @@ -2007,7 +2050,7 @@ void align_shape(std::vector &shapes, const std::wstring &text, cons offset.x() = get_align_x_offset(prop.align.first, shape_bb, get_line_bb(i + 1)); continue; } - ExPolygons &shape = shapes[i]; + ExPolygons &shape = shapes[i].expoly; for (ExPolygon &s : shape) s.translate(offset); } @@ -2016,13 +2059,13 @@ void align_shape(std::vector &shapes, const std::wstring &text, cons double Emboss::get_align_y_offset_in_mm(FontProp::VerticalAlign align, unsigned count_lines, const FontFile &ff, const FontProp &fp){ float offset_in_font_point = get_align_y_offset(align, count_lines, ff, fp); - double scale = get_shape_scale(fp, ff); + double scale = get_text_shape_scale(fp, ff); return scale * offset_in_font_point; } #ifdef REMOVE_SPIKES #include -void priv::remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) +void remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) { enum class Type { add, // Move with point B on A-side and add new point on C-side @@ -2159,14 +2202,14 @@ void priv::remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) } } -void priv::remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc) +void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc) { for (Polygon &polygon : polygons) remove_spikes(polygon, spike_desc); remove_bad(polygons); } -void priv::remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) +void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) { for (ExPolygon &expolygon : expolygons) { remove_spikes(expolygon.contour, spike_desc); diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp index 2e60b98b9e..a498b60afb 100644 --- a/src/libslic3r/Emboss.hpp +++ b/src/libslic3r/Emboss.hpp @@ -12,21 +12,25 @@ #include // indexed_triangle_set #include "Polygon.hpp" #include "ExPolygon.hpp" +#include "EmbossShape.hpp" // ExPolygonsWithIds #include "BoundingBox.hpp" #include "TextConfiguration.hpp" namespace Slic3r { +// Extend expolygons with information whether it was successfull healed +struct HealedExPolygons{ + ExPolygons expolygons; + bool is_healed; + operator ExPolygons&() { return expolygons; } +}; + /// /// class with only static function add ability to engraved OR raised /// text OR polygons onto model surface /// namespace Emboss { - // every glyph's shape point is divided by SHAPE_SCALE - increase precission of fixed point value - // stored in fonts (to be able represents curve by sequence of lines) - static constexpr double SHAPE_SCALE = 0.001; // SCALING_FACTOR promile is fine enough - /// /// Collect fonts registred inside OS /// @@ -156,20 +160,23 @@ 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, 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;}); + HealedExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function &was_canceled = []() {return false;}); + ExPolygonsWithIds text2vshapes(FontFileWithCache &font, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled = []() {return false;}); + const unsigned ENTER_UNICODE = static_cast('\n'); /// Sum of character '\n' unsigned get_count_lines(const std::wstring &ws); unsigned get_count_lines(const std::string &text); + unsigned get_count_lines(const ExPolygonsWithIds &shape); /// /// Fix duplicit points and self intersections in polygons. /// Also try to reduce amount of points and remove useless polygon parts /// - /// Define wanted precision of shape after heal - /// Healed shapes - ExPolygons heal_shape(const Polygons &shape); + /// Fill type ClipperLib::pftNonZero for overlapping otherwise + /// Look at heal_expolygon()::max_iteration + /// Healed shapes with flag is fully healed + HealedExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true, unsigned max_iteration = 10); /// /// NOTE: call Slic3r::union_ex before this call @@ -183,7 +190,7 @@ namespace Emboss /// Heal could create another issue, /// After healing it is checked again until shape is good or maximal count of iteration /// True when shapes is good otherwise False - bool heal_shape(ExPolygons &shape, unsigned max_iteration = 10); + bool heal_expolygons(ExPolygons &shape, unsigned max_iteration = 10); /// /// Divide line segments in place near to point @@ -199,10 +206,9 @@ namespace Emboss /// /// Use data from font property to modify transformation /// - /// Z-move as surface distance(FontProp::distance) - /// Z-rotation as angle to Y axis(FontProp::angle) + /// Z-rotation as angle to Y axis + /// Z-move as surface distance /// In / Out transformation to modify by property - void apply_transformation(const FontProp &font_prop, Transform3d &transformation); void apply_transformation(const std::optional &angle, const std::optional &distance, Transform3d &transformation); /// @@ -230,7 +236,7 @@ namespace Emboss /// Property of font /// Font data /// Conversion to mm - double get_shape_scale(const FontProp &fp, const FontFile &ff); + double get_text_shape_scale(const FontProp &fp, const FontFile &ff); /// /// getter of font info by collection defined in prop @@ -245,7 +251,7 @@ namespace Emboss /// /// Infos for collections /// Collection index + Additional line gap - /// Line height with spacing in ExPolygon size + /// Line height with spacing in scaled font points (same as ExPolygons) int get_line_height(const FontFile &font, const FontProp &prop); /// @@ -338,7 +344,7 @@ namespace Emboss class ProjectZ : public IProjection { public: - ProjectZ(double depth) : m_depth(depth) {} + explicit ProjectZ(double depth) : m_depth(depth) {} // Inherited via IProject std::pair create_front_back(const Point &p) const override; Vec3d project(const Vec3d &point) const override; @@ -462,5 +468,15 @@ namespace Emboss std::vector calculate_angles(int32_t distance, const PolygonPoints& polygon_points, const Polygon &polygon); } // namespace Emboss + +/////////////////////// +// Move to ExPolygonsWithIds Utils +void translate(ExPolygonsWithIds &e, const Point &p); +BoundingBox get_extents(const ExPolygonsWithIds &e); +void center(ExPolygonsWithIds &e); +HealedExPolygons union_ex(const ExPolygonsWithIds &shapes, unsigned max_heal_iteration); +// delta .. safe offset before union (use as boolean close) +// NOTE: remove unprintable spaces between neighbor curves (made by linearization of curve) +HealedExPolygons union_with_delta(const ExPolygonsWithIds &shapes, float delta, unsigned max_heal_iteration); } // namespace Slic3r #endif // slic3r_Emboss_hpp_ diff --git a/src/libslic3r/EmbossShape.hpp b/src/libslic3r/EmbossShape.hpp new file mode 100644 index 0000000000..1c7fdfe795 --- /dev/null +++ b/src/libslic3r/EmbossShape.hpp @@ -0,0 +1,134 @@ +#ifndef slic3r_EmbossShape_hpp_ +#define slic3r_EmbossShape_hpp_ + +#include +#include +#include // unique_ptr +#include +#include +#include +#include +#include +#include "Point.hpp" // Transform3d +#include "ExPolygon.hpp" +#include "ExPolygonSerialize.hpp" +#include "nanosvg/nanosvg.h" // NSVGimage + +namespace Slic3r { + +struct EmbossProjection +{ + // Emboss depth, Size in local Z direction + double depth = 1.; // [in loacal mm] + // NOTE: User should see and modify mainly world size not local + + // Flag that result volume use surface cutted from source objects + bool use_surface = false; + + // enum class Align { + // left, + // right, + // center, + // top_left, + // top_right, + // top_center, + // bottom_left, + // bottom_right, + // bottom_center + // }; + //// change pivot of volume + //// When not set, center is used and is not stored + // std::optional align; + + // compare TextStyle + bool operator==(const EmbossProjection &other) const { + return depth == other.depth && use_surface == other.use_surface; + } + + // undo / redo stack recovery + template void serialize(Archive &ar) { ar(depth, use_surface); } +}; + +// Help structure to identify expolygons grups +// e.g. emboss -> per glyph -> identify character +struct ExPolygonsWithId +{ + // Identificator for shape + // In text it separate letters and the name is unicode value of letter + // Is svg it is id of path + unsigned id; + + // shape defined by integer point contain only lines + // Curves are converted to sequence of lines + ExPolygons expoly; + + // flag whether expolygons are fully healed(without duplication) + bool is_healed = true; +}; +using ExPolygonsWithIds = std::vector; + +/// +/// Contain plane shape information to be able emboss it and edit it +/// +struct EmbossShape +{ + // shapes to to emboss separately over surface + ExPolygonsWithIds shapes_with_ids; + + // scale of shape, multiplier to get 3d point in mm from integer shape + double scale = SCALING_FACTOR; + + // Define how to emboss shape + EmbossProjection projection; + + // !!! Volume stored in .3mf has transformed vertices. + // (baked transformation into vertices position) + // Only place for fill this is when load from .3mf + // This is correction for volume transformation + // Stored_Transform3d * fix_3mf_tr = Transform3d_before_store_to_3mf + std::optional fix_3mf_tr; + + struct SvgFile { + // File(.svg) path on local computer + // When empty can't reload from disk + std::string path; + + // File path into .3mf(.zip) + // When empty svg is not stored into .3mf file yet. + // and will create dialog to delete private data on save. + std::string path_in_3mf; + + // Loaded svg file data. + // !!! It is not serialized on undo/redo stack + std::shared_ptr image = nullptr; + + // Loaded string data from file + std::shared_ptr file_data = nullptr; + }; + SvgFile svg_file; + + // flag whether during cration of union expolygon final shape was fully correct + // correct mean without selfintersection and duplicate(double) points + bool is_healed = true; + + // undo / redo stack recovery + template void save(Archive &ar) const + { + ar(shapes_with_ids, scale, projection, svg_file.path, svg_file.path_in_3mf); + cereal::save(ar, fix_3mf_tr); + } + template void load(Archive &ar) + { + ar(shapes_with_ids, scale, projection, svg_file.path, svg_file.path_in_3mf); + cereal::load(ar, fix_3mf_tr); + } +}; + +} // namespace Slic3r + +// Serialization through the Cereal library +namespace cereal { +template void serialize(Archive &ar, Slic3r::ExPolygonsWithId &o) { ar(o.id, o.expoly); } +}; // namespace cereal + +#endif // slic3r_EmbossShape_hpp_ diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index 196e417622..e962096cf4 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -469,6 +469,24 @@ bool has_duplicate_points(const ExPolygons &expolys) #endif } +bool remove_same_neighbor(ExPolygons &expolygons) +{ + if (expolygons.empty()) + return false; + bool remove_from_holes = false; + bool remove_from_contour = false; + for (ExPolygon &expoly : expolygons) { + remove_from_contour |= remove_same_neighbor(expoly.contour); + remove_from_holes |= remove_same_neighbor(expoly.holes); + } + // Removing of expolygons without contour + if (remove_from_contour) + expolygons.erase(std::remove_if(expolygons.begin(), expolygons.end(), + [](const ExPolygon &p) { return p.contour.points.size() <= 2; }), + expolygons.end()); + return remove_from_holes || remove_from_contour; +} + bool remove_sticks(ExPolygon &poly) { return remove_sticks(poly.contour) || remove_sticks(poly.holes); diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 9c9a7b7ba1..96c8ac735f 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -383,6 +383,11 @@ inline Points to_points(const ExPolygon &expoly) return out; } +inline void translate(ExPolygons &expolys, const Point &p) { + for (ExPolygon &expoly : expolys) + expoly.translate(p); +} + inline void polygons_append(Polygons &dst, const ExPolygon &src) { dst.reserve(dst.size() + src.holes.size() + 1); @@ -472,6 +477,9 @@ std::vector get_extents_vector(const ExPolygons &polygons); bool has_duplicate_points(const ExPolygon &expoly); bool has_duplicate_points(const ExPolygons &expolys); +// Return True when erase some otherwise False. +bool remove_same_neighbor(ExPolygons &expolys); + bool remove_sticks(ExPolygon &poly); void keep_largest_contour_only(ExPolygons &polygons); diff --git a/src/libslic3r/ExPolygonSerialize.hpp b/src/libslic3r/ExPolygonSerialize.hpp new file mode 100644 index 0000000000..712d4706d5 --- /dev/null +++ b/src/libslic3r/ExPolygonSerialize.hpp @@ -0,0 +1,28 @@ +#ifndef slic3r_ExPolygonSerialize_hpp_ +#define slic3r_ExPolygonSerialize_hpp_ + +#include "ExPolygon.hpp" +#include "Point.hpp" // Cereal serialization of Point +#include +#include + +/// +/// External Cereal serialization of ExPolygons +/// + +// Serialization through the Cereal library +#include +namespace cereal { + +template +void serialize(Archive &archive, Slic3r::Polygon &polygon) { + archive(polygon.points); +} + +template +void serialize(Archive &archive, Slic3r::ExPolygon &expoly) { + archive(expoly.contour, expoly.holes); +} + +} // namespace Slic3r +#endif // slic3r_ExPolygonSerialize_hpp_ diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index d8129d7baa..998218a2f9 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -41,6 +41,10 @@ namespace pt = boost::property_tree; #include "miniz_extension.hpp" #include "TextConfiguration.hpp" +#include "EmbossShape.hpp" +#include "ExPolygonSerialize.hpp" + +#include "NSVGUtils.hpp" #include @@ -162,12 +166,8 @@ static constexpr const char *FONT_DESCRIPTOR_TYPE_ATTR = "font_descriptor_type"; static constexpr const char *CHAR_GAP_ATTR = "char_gap"; static constexpr const char *LINE_GAP_ATTR = "line_gap"; static constexpr const char *LINE_HEIGHT_ATTR = "line_height"; -static constexpr const char *DEPTH_ATTR = "depth"; -static constexpr const char *USE_SURFACE_ATTR = "use_surface"; static constexpr const char *BOLDNESS_ATTR = "boldness"; static constexpr const char *SKEW_ATTR = "skew"; -static constexpr const char *DISTANCE_ATTR = "distance"; -static constexpr const char *ANGLE_ATTR = "angle"; 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"; @@ -178,6 +178,19 @@ static constexpr const char *FONT_FACE_NAME_ATTR = "face_name"; static constexpr const char *FONT_STYLE_ATTR = "style"; static constexpr const char *FONT_WEIGHT_ATTR = "weight"; +// Store / load of EmbossShape +static constexpr const char *SHAPE_TAG = "slic3rpe:shape"; +static constexpr const char *SHAPE_SCALE_ATTR = "scale"; +static constexpr const char *UNHEALED_ATTR = "unhealed"; +static constexpr const char *SVG_FILE_PATH_ATTR = "filepath"; +static constexpr const char *SVG_FILE_PATH_IN_3MF_ATTR = "filepath3mf"; + +// EmbossProjection +static constexpr const char *DEPTH_ATTR = "depth"; +static constexpr const char *USE_SURFACE_ATTR = "use_surface"; +// static constexpr const char *FIX_TRANSFORMATION_ATTR = "transform"; + + const unsigned int VALID_OBJECT_TYPES_COUNT = 1; const char* VALID_OBJECT_TYPES[] = { @@ -424,6 +437,7 @@ namespace Slic3r { MetadataList metadata; RepairedMeshErrors mesh_stats; std::optional text_configuration; + std::optional shape_configuration; VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id) : first_triangle_id(first_triangle_id) , last_triangle_id(last_triangle_id) @@ -461,7 +475,7 @@ namespace Slic3r { typedef std::map IdToCutObjectInfoMap; typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap; - + using PathToEmbossShapeFileMap = std::map>; // Version of the 3mf file unsigned int m_version; bool m_check_version; @@ -491,6 +505,7 @@ namespace Slic3r { IdToLayerConfigRangesMap m_layer_config_ranges; IdToSlaSupportPointsMap m_sla_support_points; IdToSlaDrainHolesMap m_sla_drain_holes; + PathToEmbossShapeFileMap m_path_to_emboss_shape_files; std::string m_curr_metadata_name; std::string m_curr_characters; std::string m_name; @@ -517,6 +532,7 @@ namespace Slic3r { } bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); + bool _is_svg_shape_file(const std::string &filename) const; bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); @@ -528,6 +544,7 @@ namespace Slic3r { void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); + void _extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat); // handlers to parse the .model file void _handle_start_model_xml_element(const char* name, const char** attributes); @@ -578,6 +595,7 @@ namespace Slic3r { bool _handle_end_metadata(); bool _handle_start_text_configuration(const char** attributes, unsigned int num_attributes); + bool _handle_start_shape_configuration(const char **attributes, unsigned int num_attributes); bool _create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); @@ -754,6 +772,9 @@ namespace Slic3r { add_error("Archive does not contain a valid model config"); return false; } + } + else if (_is_svg_shape_file(name)) { + _extract_embossed_svg_shape_file(name, archive, stat); } } } @@ -929,6 +950,10 @@ namespace Slic3r { return true; } + bool _3MF_Importer::_is_svg_shape_file(const std::string &name) const { + return boost::starts_with(name, MODEL_FOLDER) && boost::ends_with(name, ".svg"); + } + bool _3MF_Importer::_extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) { if (stat.m_uncomp_size == 0) { @@ -1361,6 +1386,29 @@ namespace Slic3r { } } + void _3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){ + assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end()); + auto file = std::make_unique(stat.m_uncomp_size, '\0'); + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void *) file->data(), stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading svg shape for emboss"); + return; + } + + // store for case svg is loaded before volume + m_path_to_emboss_shape_files[filename] = std::move(file); + + // find embossed volume, for case svg is loaded after volume + for (const ModelObject* object : m_model->objects) + for (ModelVolume *volume : object->volumes) { + std::optional &es = volume->emboss_shape; + if (!es.has_value()) + continue; + if (filename.compare(es->svg_file.path_in_3mf) == 0) + es->svg_file.file_data = m_path_to_emboss_shape_files[filename]; + } + } + bool _3MF_Importer::_extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model) { if (stat.m_uncomp_size == 0) { @@ -1560,6 +1608,8 @@ namespace Slic3r { res = _handle_start_config_volume_mesh(attributes, num_attributes); else if (::strcmp(METADATA_TAG, name) == 0) res = _handle_start_config_metadata(attributes, num_attributes); + else if (::strcmp(SHAPE_TAG, name) == 0) + res = _handle_start_shape_configuration(attributes, num_attributes); else if (::strcmp(TEXT_TAG, name) == 0) res = _handle_start_text_configuration(attributes, num_attributes); @@ -1912,8 +1962,15 @@ namespace Slic3r { { public: TextConfigurationSerialization() = delete; + + using TypeToName = boost::bimap; + static const TypeToName type_to_name; - static const boost::bimap type_to_name; + using HorizontalAlignToName = boost::bimap; + static const HorizontalAlignToName horizontal_align_to_name; + + using VerticalAlignToName = boost::bimap; + static const VerticalAlignToName vertical_align_to_name; static EmbossStyle::Type get_type(std::string_view type) { const auto& to_type = TextConfigurationSerialization::type_to_name.right; @@ -1932,24 +1989,68 @@ namespace Slic3r { } static void to_xml(std::stringstream &stream, const TextConfiguration &tc); - static void create_fix_and_store(std::stringstream &stream, TextConfiguration tc, const ModelVolume& volume); static std::optional read(const char **attributes, unsigned int num_attributes); + static EmbossShape read_old(const char **attributes, unsigned int num_attributes); }; bool _3MF_Importer::_handle_start_text_configuration(const char **attributes, unsigned int num_attributes) { IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); if (object == m_objects_metadata.end()) { - add_error("Cannot assign volume mesh to a valid object"); + add_error("Can not assign volume mesh to a valid object"); return false; } if (object->second.volumes.empty()) { - add_error("Cannot assign mesh to a valid volume"); + add_error("Can not assign mesh to a valid volume"); return false; } ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); volume.text_configuration = TextConfigurationSerialization::read(attributes, num_attributes); - return volume.text_configuration.has_value(); + if (!volume.text_configuration.has_value()) + return false; + + // Is 3mf version with shapes? + if (volume.shape_configuration.has_value()) + return true; + + // Back compatibility for 3mf version without shapes + volume.shape_configuration = TextConfigurationSerialization::read_old(attributes, num_attributes); + return true; + } + + // Definition of read/write method for EmbossShape + static void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume, mz_zip_archive &archive); + static std::optional read_emboss_shape(const char **attributes, unsigned int num_attributes); + + bool _3MF_Importer::_handle_start_shape_configuration(const char **attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Can not assign volume mesh to a valid object"); + return false; + } + auto &volumes = object->second.volumes; + if (volumes.empty()) { + add_error("Can not assign mesh to a valid volume"); + return false; + } + ObjectMetadata::VolumeMetadata &volume = volumes.back(); + volume.shape_configuration = read_emboss_shape(attributes, num_attributes); + if (!volume.shape_configuration.has_value()) + return false; + + // Fill svg file content into shape_configuration + EmbossShape::SvgFile &svg = volume.shape_configuration->svg_file; + const std::string &path = svg.path_in_3mf; + if (path.empty()) // do not contain svg file + return true; + + auto it = m_path_to_emboss_shape_files.find(path); + if (it == m_path_to_emboss_shape_files.end()) + return true; // svg file is not loaded yet + + svg.file_data = it->second; + return true; } bool _3MF_Importer::_create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) @@ -2232,22 +2333,11 @@ namespace Slic3r { volume->supported_facets.shrink_to_fit(); volume->seam_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit(); - auto &tc = volume_data.text_configuration; - if (tc.has_value()) { + + if (auto &es = volume_data.shape_configuration; es.has_value()) + volume->emboss_shape = std::move(es); + if (auto &tc = volume_data.text_configuration; tc.has_value()) volume->text_configuration = std::move(tc); - - //// Transformation before store to 3mf - //const Transform3d &pre_trmat = *tc->fix_3mf_tr; - //// Cannot use source tranformation - //// When store transformed againg to 3mf it is not modified !!! - //// const Transform3d &pre_trmat = volume->source.transform.get_matrix(); - - //// create fix transformation - //assert(tc->fix_3mf_tr.has_value()); - //volume->text_configuration->fix_3mf_tr = - // pre_trmat.inverse() * - // volume->get_transformation().get_matrix(); - } // apply the remaining volume's metadata for (const Metadata& metadata : volume_data.metadata) { @@ -2379,6 +2469,7 @@ namespace Slic3r { bool save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64); static void add_transformation(std::stringstream &stream, const Transform3d &tr); private: + void _publish(Model &model); bool _save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data); bool _add_content_types_file_to_archive(mz_zip_archive& archive); bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); @@ -3322,11 +3413,14 @@ namespace Slic3r { for (const std::string& key : volume->config.keys()) { stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; } - - // stores volume's text data - const auto &tc = volume->text_configuration; - if (tc.has_value()) - TextConfigurationSerialization::create_fix_and_store(stream, *tc, *volume); + + if (const std::optional &es = volume->emboss_shape; + es.has_value()) + to_xml(stream, *es, *volume, archive); + + if (const std::optional &tc = volume->text_configuration; + tc.has_value()) + TextConfigurationSerialization::to_xml(stream, *tc); // stores mesh's statistics const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; @@ -3497,27 +3591,74 @@ bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, return res; } +namespace{ + +// Conversion with bidirectional map +// F .. first, S .. second +template +F bimap_cvt(const boost::bimap &bmap, S s, const F & def_value) { + const auto &map = bmap.right; + auto found_item = map.find(s); + + // only for back and forward compatibility + assert(found_item != map.end()); + if (found_item == map.end()) + return def_value; + + return found_item->second; +} + +template +S bimap_cvt(const boost::bimap &bmap, F f, const S &def_value) +{ + const auto &map = bmap.left; + auto found_item = map.find(f); + + // only for back and forward compatibility + assert(found_item != map.end()); + if (found_item == map.end()) + return def_value; + + return found_item->second; +} + +} // namespace + /// /// TextConfiguration serialization /// -using TypeToName = boost::bimap; -const TypeToName TextConfigurationSerialization::type_to_name = - boost::assign::list_of +const TextConfigurationSerialization::TypeToName TextConfigurationSerialization::type_to_name = + boost::assign::list_of (EmbossStyle::Type::file_path, "file_name") (EmbossStyle::Type::wx_win_font_descr, "wxFontDescriptor_Windows") (EmbossStyle::Type::wx_lin_font_descr, "wxFontDescriptor_Linux") (EmbossStyle::Type::wx_mac_font_descr, "wxFontDescriptor_MacOsX"); +const TextConfigurationSerialization::HorizontalAlignToName TextConfigurationSerialization::horizontal_align_to_name = + boost::assign::list_of + (FontProp::HorizontalAlign::left, "left") + (FontProp::HorizontalAlign::center, "center") + (FontProp::HorizontalAlign::right, "right"); + +const TextConfigurationSerialization::VerticalAlignToName TextConfigurationSerialization::vertical_align_to_name = + boost::assign::list_of + (FontProp::VerticalAlign::top, "top") + (FontProp::VerticalAlign::center, "middle") + (FontProp::VerticalAlign::bottom, "bottom"); + + void TextConfigurationSerialization::to_xml(std::stringstream &stream, const TextConfiguration &tc) { stream << " <" << TEXT_TAG << " "; stream << TEXT_DATA_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(tc.text) << "\" "; // font item - const EmbossStyle &fi = tc.style; - stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(fi.name) << "\" "; - stream << FONT_DESCRIPTOR_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(fi.path) << "\" "; - stream << FONT_DESCRIPTOR_TYPE_ATTR << "=\"" << TextConfigurationSerialization::get_name(fi.type) << "\" "; + const EmbossStyle &style = tc.style; + stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(style.name) << "\" "; + stream << FONT_DESCRIPTOR_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(style.path) << "\" "; + constexpr std::string_view dafault_type{"undefined"}; + std::string_view style_type = bimap_cvt(type_to_name, style.type, dafault_type); + stream << FONT_DESCRIPTOR_TYPE_ATTR << "=\"" << style_type << "\" "; // font property const FontProp &fp = tc.style.prop; @@ -3527,21 +3668,14 @@ void TextConfigurationSerialization::to_xml(std::stringstream &stream, const Tex stream << LINE_GAP_ATTR << "=\"" << *fp.line_gap << "\" "; stream << LINE_HEIGHT_ATTR << "=\"" << fp.size_in_mm << "\" "; - stream << DEPTH_ATTR << "=\"" << fp.emboss << "\" "; - if (fp.use_surface) - stream << USE_SURFACE_ATTR << "=\"" << 1 << "\" "; if (fp.boldness.has_value()) stream << BOLDNESS_ATTR << "=\"" << *fp.boldness << "\" "; if (fp.skew.has_value()) stream << SKEW_ATTR << "=\"" << *fp.skew << "\" "; - if (fp.distance.has_value()) - stream << DISTANCE_ATTR << "=\"" << *fp.distance << "\" "; - if (fp.angle.has_value()) - stream << ANGLE_ATTR << "=\"" << *fp.angle << "\" "; 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) << "\" "; + stream << HORIZONTAL_ALIGN_ATTR << "=\"" << bimap_cvt(horizontal_align_to_name, fp.align.first, dafault_type) << "\" "; + stream << VERTICAL_ALIGN_ATTR << "=\"" << bimap_cvt(vertical_align_to_name, fp.align.second, dafault_type) << "\" "; if (fp.collection_number.has_value()) stream << COLLECTION_NUMBER_ATTR << "=\"" << *fp.collection_number << "\" "; // font descriptor @@ -3554,52 +3688,51 @@ void TextConfigurationSerialization::to_xml(std::stringstream &stream, const Tex if (fp.weight.has_value()) stream << FONT_WEIGHT_ATTR << "=\"" << *fp.weight << "\" "; - // FIX of baked transformation - assert(tc.fix_3mf_tr.has_value()); - stream << TRANSFORM_ATTR << "=\""; - _3MF_Exporter::add_transformation(stream, *tc.fix_3mf_tr); - stream << "\" "; - stream << "/>\n"; // end TEXT_TAG } +namespace { -void TextConfigurationSerialization::create_fix_and_store( - std::stringstream &stream, TextConfiguration tc, const ModelVolume &volume) -{ - const auto& vertices = volume.mesh().its.vertices; - assert(!vertices.empty()); - if (vertices.empty()) { - to_xml(stream, tc); - return; +FontProp::HorizontalAlign read_horizontal_align(const char **attributes, unsigned int num_attributes, const TextConfigurationSerialization::HorizontalAlignToName& horizontal_align_to_name){ + std::string horizontal_align_str = get_attribute_value_string(attributes, num_attributes, HORIZONTAL_ALIGN_ATTR); + + // Back compatibility + // PS 2.6.0 do not have align + if (horizontal_align_str.empty()) + return FontProp::HorizontalAlign::center; + + // Back compatibility + // PS 2.6.1 store indices(0|1|2) instead of text for align + if (horizontal_align_str.length() == 1) { + int horizontal_align_int = 0; + if(boost::spirit::qi::parse(horizontal_align_str.c_str(), horizontal_align_str.c_str() + 1, boost::spirit::qi::int_, horizontal_align_int)) + return static_cast(horizontal_align_int); } - // IMPROVE: check if volume was modified (translated, rotated OR scaled) - // when no change do not calculate transformation only store original fix matrix - - // Create transformation used after load actual stored volume - const Transform3d &actual_trmat = volume.get_transformation().get_matrix(); - Vec3d min = actual_trmat * vertices.front().cast(); - Vec3d max = min; - for (const Vec3f &v : vertices) { - Vec3d vd = actual_trmat * v.cast(); - for (size_t i = 0; i < 3; ++i) { - if (min[i] > vd[i]) min[i] = vd[i]; - if (max[i] < vd[i]) max[i] = vd[i]; - } - } - Vec3d center = (max + min) / 2; - Transform3d post_trmat = Transform3d::Identity(); - post_trmat.translate(center); - - Transform3d fix_trmat = actual_trmat.inverse() * post_trmat; - if (!tc.fix_3mf_tr.has_value()) { - tc.fix_3mf_tr = fix_trmat; - } else if (!fix_trmat.isApprox(Transform3d::Identity(), 1e-5)) { - tc.fix_3mf_tr = *tc.fix_3mf_tr * fix_trmat; - } - to_xml(stream, tc); + return bimap_cvt(horizontal_align_to_name, std::string_view(horizontal_align_str), FontProp::HorizontalAlign::center); } + +FontProp::VerticalAlign read_vertical_align(const char **attributes, unsigned int num_attributes, const TextConfigurationSerialization::VerticalAlignToName& vertical_align_to_name){ + std::string vertical_align_str = get_attribute_value_string(attributes, num_attributes, VERTICAL_ALIGN_ATTR); + + // Back compatibility + // PS 2.6.0 do not have align + if (vertical_align_str.empty()) + return FontProp::VerticalAlign::center; + + // Back compatibility + // PS 2.6.1 store indices(0|1|2) instead of text for align + if (vertical_align_str.length() == 1) { + int vertical_align_int = 0; + if(boost::spirit::qi::parse(vertical_align_str.c_str(), vertical_align_str.c_str() + 1, boost::spirit::qi::int_, vertical_align_int)) + return static_cast(vertical_align_int); + } + + return bimap_cvt(vertical_align_to_name, std::string_view(vertical_align_str), FontProp::VerticalAlign::center); +} + +} // namespace + std::optional TextConfigurationSerialization::read(const char **attributes, unsigned int num_attributes) { FontProp fp; @@ -3613,28 +3746,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; - float distance = get_attribute_value_float(attributes, num_attributes, DISTANCE_ATTR); - if (std::fabs(distance) > std::numeric_limits::epsilon()) - fp.distance = distance; - int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); - if (use_surface == 1) fp.use_surface = true; - float angle = get_attribute_value_float(attributes, num_attributes, ANGLE_ATTR); - if (std::fabs(angle) > std::numeric_limits::epsilon()) - fp.angle = angle; 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)); + fp.align = FontProp::Align( + read_horizontal_align(attributes, num_attributes, horizontal_align_to_name), + read_vertical_align(attributes, num_attributes, vertical_align_to_name)); int collection_number = get_attribute_value_int(attributes, num_attributes, COLLECTION_NUMBER_ATTR); if (collection_number > 0) fp.collection_number = static_cast(collection_number); fp.size_in_mm = get_attribute_value_float(attributes, num_attributes, LINE_HEIGHT_ATTR); - fp.emboss = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); std::string family = get_attribute_value_string(attributes, num_attributes, FONT_FAMILY_ATTR); if (!family.empty()) fp.family = family; @@ -3648,10 +3770,123 @@ std::optional TextConfigurationSerialization::read(const char std::string style_name = get_attribute_value_string(attributes, num_attributes, STYLE_NAME_ATTR); std::string font_descriptor = get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_ATTR); std::string type_str = get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_TYPE_ATTR); - EmbossStyle::Type type = TextConfigurationSerialization::get_type(type_str); - EmbossStyle fi{ style_name, std::move(font_descriptor), type, std::move(fp) }; + EmbossStyle::Type type = bimap_cvt(type_to_name, std::string_view{type_str}, EmbossStyle::Type::undefined); std::string text = get_attribute_value_string(attributes, num_attributes, TEXT_DATA_ATTR); + EmbossStyle es{style_name, std::move(font_descriptor), type, std::move(fp)}; + return TextConfiguration{std::move(es), std::move(text)}; +} + +EmbossShape TextConfigurationSerialization::read_old(const char **attributes, unsigned int num_attributes) +{ + EmbossShape es; + std::string fix_tr_mat_str = get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); + if (!fix_tr_mat_str.empty()) + es.fix_3mf_tr = get_transform_from_3mf_specs_string(fix_tr_mat_str); + + + if (get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR) == 1) + es.projection.use_surface = true; + + es.projection.depth = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + + int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) + es.projection.use_surface = true; + + return es; +} + +namespace { +Transform3d create_fix(const std::optional &prev, const ModelVolume &volume) +{ + // IMPROVE: check if volume was modified (translated, rotated OR scaled) + // when no change do not calculate transformation only store original fix matrix + + // Create transformation used after load actual stored volume + const Transform3d &actual_trmat = volume.get_matrix(); + + const auto &vertices = volume.mesh().its.vertices; + Vec3d min = actual_trmat * vertices.front().cast(); + Vec3d max = min; + for (const Vec3f &v : vertices) { + Vec3d vd = actual_trmat * v.cast(); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > vd[i]) + min[i] = vd[i]; + if (max[i] < vd[i]) + max[i] = vd[i]; + } + } + Vec3d center = (max + min) / 2; + Transform3d post_trmat = Transform3d::Identity(); + post_trmat.translate(center); + + Transform3d fix_trmat = actual_trmat.inverse() * post_trmat; + if (!prev.has_value()) + return fix_trmat; + + // check whether fix somehow differ previous + if (fix_trmat.isApprox(Transform3d::Identity(), 1e-5)) + return *prev; + + return *prev * fix_trmat; +} + +bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const ModelVolume &volume, mz_zip_archive &archive){ + if (svg.path_in_3mf.empty()) + return true; // EmbossedText OR unwanted store .svg file into .3mf (protection of copyRight) + + if (!svg.path.empty()) + stream << SVG_FILE_PATH_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(svg.path) << "\" "; + stream << SVG_FILE_PATH_IN_3MF_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(svg.path_in_3mf) << "\" "; + + const std::string &file_data = *svg.file_data; + return mz_zip_writer_add_mem(&archive, svg.path_in_3mf.c_str(), + (const void *) file_data.c_str(), file_data.size(), MZ_DEFAULT_COMPRESSION); +} + +} // namespace + +void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume, mz_zip_archive &archive) +{ + stream << " <" << SHAPE_TAG << " "; + if(!to_xml(stream, es.svg_file, volume, archive)) + BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf"; + + stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" "; + + if (!es.is_healed) + stream << UNHEALED_ATTR << "=\"" << 1 << "\" "; + + // projection + const EmbossProjection &p = es.projection; + stream << DEPTH_ATTR << "=\"" << p.depth << "\" "; + if (p.use_surface) + stream << USE_SURFACE_ATTR << "=\"" << 1 << "\" "; + + // FIX of baked transformation + Transform3d fix = create_fix(es.fix_3mf_tr, volume); + stream << TRANSFORM_ATTR << "=\""; + _3MF_Exporter::add_transformation(stream, fix); + stream << "\" "; + + stream << "/>\n"; // end SHAPE_TAG +} + +std::optional read_emboss_shape(const char **attributes, unsigned int num_attributes) { + double scale = get_attribute_value_float(attributes, num_attributes, SHAPE_SCALE_ATTR); + int unhealed = get_attribute_value_int(attributes, num_attributes, UNHEALED_ATTR); + bool is_healed = unhealed != 1; + + EmbossProjection projection; + projection.depth = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + if (is_approx(projection.depth, 0.)) + projection.depth = 10.; + + int use_surface = get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) + projection.use_surface = true; std::optional fix_tr_mat; std::string fix_tr_mat_str = get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); @@ -3659,7 +3894,12 @@ std::optional TextConfigurationSerialization::read(const char fix_tr_mat = get_transform_from_3mf_specs_string(fix_tr_mat_str); } - return TextConfiguration{std::move(fi), std::move(text), std::move(fix_tr_mat)}; + std::string file_path = get_attribute_value_string(attributes, num_attributes, SVG_FILE_PATH_ATTR); + std::string file_path_3mf = get_attribute_value_string(attributes, num_attributes, SVG_FILE_PATH_IN_3MF_ATTR); + ExPolygonsWithIds shapes; // TODO: need to implement + + EmbossShape::SvgFile svg{file_path, file_path_3mf}; + return EmbossShape{shapes, scale, std::move(projection), std::move(fix_tr_mat), std::move(svg), is_healed}; } diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 4afc270f95..c5d81bda54 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -316,6 +316,13 @@ template T angle_to_0_2PI(T angle) return angle; } +template void to_range_pi_pi(T &angle){ + if (angle > T(PI) || angle <= -T(PI)) { + int count = static_cast(std::round(angle / (2 * PI))); + angle -= static_cast(count * 2 * PI); + assert(angle <= T(PI) && angle > -T(PI)); + } +} void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval); diff --git a/src/libslic3r/IntersectionPoints.cpp b/src/libslic3r/IntersectionPoints.cpp index c0a0430f9e..bdf66d5905 100644 --- a/src/libslic3r/IntersectionPoints.cpp +++ b/src/libslic3r/IntersectionPoints.cpp @@ -3,173 +3,47 @@ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #include "IntersectionPoints.hpp" +#include -//#define USE_CGAL_SWEEP_LINE -#ifdef USE_CGAL_SWEEP_LINE +//NOTE: using CGAL SweepLines is slower !!! (example in git history) -#include -#include -#include -#include -#include - -using NT = CGAL::Quotient; -using Kernel = CGAL::Cartesian; -using P2 = Kernel::Point_2; -using Traits_2 = CGAL::Arr_segment_traits_2; -using Segment = Traits_2::Curve_2; -using Segments = std::vector; - -namespace priv { - -P2 convert(const Slic3r::Point &p) { return P2(p.x(), p.y()); } -Slic3r::Vec2d convert(const P2 &p) +namespace { +using namespace Slic3r; +IntersectionsLines compute_intersections(const Lines &lines) { - return Slic3r::Vec2d(CGAL::to_double(p.x()), CGAL::to_double(p.y())); -} + if (lines.size() < 3) + return {}; -Slic3r::Pointfs compute_intersections(const Segments &segments) -{ - std::vector intersections; - // Compute all intersection points. - CGAL::compute_intersection_points(segments.begin(), segments.end(), - std::back_inserter(intersections)); - if (intersections.empty()) return {}; - Slic3r::Pointfs pts; - pts.reserve(intersections.size()); - for (const P2 &p : intersections) pts.push_back(convert(p)); - return pts; -} + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + IntersectionsLines result; + for (uint32_t li = 0; li < lines.size()-1; ++li) { + const Line &l = lines[li]; + auto intersections = AABBTreeLines::get_intersections_with_line(lines, tree, l); + for (const auto &[p, node_index] : intersections) { + if (node_index - 1 <= li) + continue; + if (const Line &l_ = lines[node_index]; + l_.a == l.a || + l_.a == l.b || + l_.b == l.a || + l_.b == l.b ) + // it is duplicit point not intersection + continue; -void add_polygon(const Slic3r::Polygon &polygon, Segments &segments) -{ - if (polygon.points.size() < 2) return; - P2 prev_point = priv::convert(polygon.last_point()); - for (const Slic3r::Point &p : polygon.points) { - P2 act_point = priv::convert(p); - if (prev_point == act_point) continue; - segments.emplace_back(prev_point, act_point); - prev_point = act_point; - } -} -Slic3r::Pointfs Slic3r::intersection_points(const Lines &lines) -{ - return priv::compute_intersections2(lines); - Segments segments; - segments.reserve(lines.size()); - for (Line l : lines) - segments.emplace_back(priv::convert(l.a), priv::convert(l.b)); - return priv::compute_intersections(segments); -} + // NOTE: fix AABBTree to compute intersection with double preccission!! + Vec2d intersection_point = p.cast(); -Slic3r::Pointfs Slic3r::intersection_points(const Polygon &polygon) -{ - Segments segments; - segments.reserve(polygon.points.size()); - priv::add_polygon(polygon, segments); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const Polygons &polygons) -{ - Segments segments; - segments.reserve(count_points(polygons)); - for (const Polygon &polygon : polygons) - priv::add_polygon(polygon, segments); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygon &expolygon) -{ - Segments segments; - segments.reserve(count_points(expolygon)); - priv::add_polygon(expolygon.contour, segments); - for (const Polygon &hole : expolygon.holes) - priv::add_polygon(hole, segments); - return priv::compute_intersections(segments); -} - -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygons &expolygons) -{ - Segments segments; - segments.reserve(count_points(expolygons)); - for (const ExPolygon &expolygon : expolygons) { - priv::add_polygon(expolygon.contour, segments); - for (const Polygon &hole : expolygon.holes) - priv::add_polygon(hole, segments); - } - return priv::compute_intersections(segments); -} - - -} // namespace priv - -#else // USE_CGAL_SWEEP_LINE - -// use bounding boxes -#include - -namespace priv { -//FIXME O(n^2) complexity! -Slic3r::Pointfs compute_intersections(const Slic3r::Lines &lines) -{ - using namespace Slic3r; - // IMPROVE0: BoundingBoxes of Polygons - // IMPROVE1: Polygon's neighbor lines can't intersect - // e.g. use indices to Point to find same points - // IMPROVE2: Use BentleyOttmann algorithm - // https://doc.cgal.org/latest/Surface_sweep_2/index.html -- CGAL implementation is significantly slower - // https://stackoverflow.com/questions/4407493/is-there-a-robust-c-implementation-of-the-bentley-ottmann-algorithm - Pointfs pts; - Point i; - for (size_t li = 0; li < lines.size(); ++li) { - const Line &l = lines[li]; - const Point &a = l.a; - const Point &b = l.b; - Point min(std::min(a.x(), b.x()), std::min(a.y(), b.y())); - Point max(std::max(a.x(), b.x()), std::max(a.y(), b.y())); - BoundingBox bb(min, max); - for (size_t li_ = li + 1; li_ < lines.size(); ++li_) { - const Line &l_ = lines[li_]; - const Point &a_ = l_.a; - const Point &b_ = l_.b; - if (a == b_ || b == a_ || a == a_ || b == b_) continue; - Point min_(std::min(a_.x(), b_.x()), std::min(a_.y(), b_.y())); - Point max_(std::max(a_.x(), b_.x()), std::max(a_.y(), b_.y())); - BoundingBox bb_(min_, max_); - // intersect of BB compare min max - if (bb.overlap(bb_) && - l.intersection(l_, &i)) - pts.push_back(i.cast()); + result.push_back(IntersectionLines{li, static_cast(node_index), intersection_point}); } } - return pts; + return result; } -} // namespace priv +} // namespace -Slic3r::Pointfs Slic3r::intersection_points(const Lines &lines) -{ - return priv::compute_intersections(lines); +namespace Slic3r { +IntersectionsLines get_intersections(const Lines &lines) { return compute_intersections(lines); } +IntersectionsLines get_intersections(const Polygon &polygon) { return compute_intersections(to_lines(polygon)); } +IntersectionsLines get_intersections(const Polygons &polygons) { return compute_intersections(to_lines(polygons)); } +IntersectionsLines get_intersections(const ExPolygon &expolygon) { return compute_intersections(to_lines(expolygon)); } +IntersectionsLines get_intersections(const ExPolygons &expolygons) { return compute_intersections(to_lines(expolygons)); } } - -Slic3r::Pointfs Slic3r::intersection_points(const Polygon &polygon) -{ - return priv::compute_intersections(to_lines(polygon)); -} - -Slic3r::Pointfs Slic3r::intersection_points(const Polygons &polygons) -{ - return priv::compute_intersections(to_lines(polygons)); -} - -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygon &expolygon) -{ - return priv::compute_intersections(to_lines(expolygon)); -} - -Slic3r::Pointfs Slic3r::intersection_points(const ExPolygons &expolygons) -{ - return priv::compute_intersections(to_lines(expolygons)); -} - -#endif // USE_CGAL_SWEEP_LINE diff --git a/src/libslic3r/IntersectionPoints.hpp b/src/libslic3r/IntersectionPoints.hpp index c4a3e00f02..cf06c718a0 100644 --- a/src/libslic3r/IntersectionPoints.hpp +++ b/src/libslic3r/IntersectionPoints.hpp @@ -9,13 +9,19 @@ namespace Slic3r { +struct IntersectionLines { + uint32_t line_index1; + uint32_t line_index2; + Vec2d intersection; +}; +using IntersectionsLines = std::vector; + // collect all intersecting points -//FIXME O(n^2) complexity! -Pointfs intersection_points(const Lines &lines); -Pointfs intersection_points(const Polygon &polygon); -Pointfs intersection_points(const Polygons &polygons); -Pointfs intersection_points(const ExPolygon &expolygon); -Pointfs intersection_points(const ExPolygons &expolygons); +IntersectionsLines get_intersections(const Lines &lines); +IntersectionsLines get_intersections(const Polygon &polygon); +IntersectionsLines get_intersections(const Polygons &polygons); +IntersectionsLines get_intersections(const ExPolygon &expolygon); +IntersectionsLines get_intersections(const ExPolygons &expolygons); } // namespace Slic3r #endif // slic3r_IntersectionPoints_hpp_ \ No newline at end of file diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index dc2a57b050..5afd77883e 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -26,6 +26,7 @@ #include "CustomGCode.hpp" #include "enum_bitmask.hpp" #include "TextConfiguration.hpp" +#include "EmbossShape.hpp" #include #include @@ -829,6 +830,10 @@ public: // Contain information how to re-create volume std::optional text_configuration; + // Is set only when volume is Embossed Shape + // Contain 2d information about embossed shape to be editabled + std::optional emboss_shape; + // A parent object owning this modifier volume. ModelObject* get_object() const { return this->object; } ModelVolumeType type() const { return m_type; } @@ -840,6 +845,7 @@ public: bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } bool is_text() const { return text_configuration.has_value(); } + bool is_svg() const { return emboss_shape.has_value() && !text_configuration.has_value(); } bool is_the_only_one_part() const; // behave like an object t_model_material_id material_id() const { return m_material_id; } void reset_extra_facets(); @@ -998,8 +1004,7 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), - cut_info(other.cut_info), - text_configuration(other.text_configuration) + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1020,8 +1025,7 @@ private: // Providing a new mesh, therefore this volume will get a new unique ID assigned. ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation), - cut_info(other.cut_info), - text_configuration(other.text_configuration) + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1069,6 +1073,7 @@ private: cereal::load_by_value(ar, mmu_segmentation_facets); cereal::load_by_value(ar, config); cereal::load(ar, text_configuration); + cereal::load(ar, emboss_shape); assert(m_mesh); if (has_convex_hull) { cereal::load_optional(ar, m_convex_hull); @@ -1086,6 +1091,7 @@ private: cereal::save_by_value(ar, mmu_segmentation_facets); cereal::save_by_value(ar, config); cereal::save(ar, text_configuration); + cereal::save(ar, emboss_shape); if (has_convex_hull) cereal::save_optional(ar, m_convex_hull); } diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp index d4560c3e1b..21fde6b9d0 100644 --- a/src/libslic3r/NSVGUtils.cpp +++ b/src/libslic3r/NSVGUtils.cpp @@ -3,87 +3,522 @@ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #include "NSVGUtils.hpp" +#include +#include // to_chars + +#include #include "ClipperUtils.hpp" +#include "Emboss.hpp" // heal for shape -using namespace Slic3r; +namespace { +using namespace Slic3r; // Polygon +// see function nsvg__lineTo(NSVGparser* p, float x, float y) +bool is_line(const float *p, float precision = 1e-4f); +// convert curve in path to lines +struct LinesPath{ + Polygons polygons; + Polylines polylines; }; +LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m); +HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); +HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); +} // namespace -// inspired by nanosvgrast.h function nsvgRasterize -> nsvg__flattenShape -> nsvg__flattenCubicBez -// https://github.com/memononen/nanosvg/blob/f0a3e1034dd22e2e87e5db22401e44998383124e/src/nanosvgrast.h#L335 -void NSVGUtils::flatten_cubic_bez(Polygon &polygon, - float tessTol, - Vec2f p1, - Vec2f p2, - Vec2f p3, - Vec2f p4, - int level) +namespace Slic3r { + +ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m) { - Vec2f p12 = (p1 + p2) * 0.5f; - Vec2f p23 = (p2 + p3) * 0.5f; - Vec2f p34 = (p3 + p4) * 0.5f; - Vec2f p123 = (p12 + p23) * 0.5f; + ExPolygonsWithIds result; + size_t shape_id = 0; + for (NSVGshape *shape_ptr = image.shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next, ++shape_id) { + const NSVGshape &shape = *shape_ptr; + if (!(shape.flags & NSVG_FLAGS_VISIBLE)) + continue; - Vec2f pd = p4 - p1; - Vec2f pd2 = p2 - p4; - float d2 = std::abs(pd2.x() * pd.y() - pd2.y() * pd.x()); - Vec2f pd3 = p3 - p4; - float d3 = std::abs(pd3.x() * pd.y() - pd3.y() * pd.x()); + bool is_fill_used = shape.fill.type != NSVG_PAINT_NONE; + bool is_stroke_used = + shape.stroke.type != NSVG_PAINT_NONE && + shape.strokeWidth > 1e-5f; + + if (!is_fill_used && !is_stroke_used) + continue; + + const LinesPath lines_path = linearize_path(shape.paths, param); + + if (is_fill_used) { + unsigned unique_id = static_cast(2 * shape_id); + HealedExPolygons expoly = fill_to_expolygons(lines_path, shape, param); + result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); + } + if (is_stroke_used) { + unsigned unique_id = static_cast(2 * shape_id + 1); + HealedExPolygons expoly = stroke_to_expolygons(lines_path, shape, param); + result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); + } + } + + // SVG is used as centered + // Do not disturb user by settings of pivot position + center(result); + return result; +} + +Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m) +{ + Polygons result; + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) + continue; + if (shape->fill.type == NSVG_PAINT_NONE) + continue; + const LinesPath lines_path = linearize_path(shape->paths, param); + polygons_append(result, lines_path.polygons); + // close polyline to create polygon + polygons_append(result, to_polygons(lines_path.polylines)); + } + return result; +} + +void bounds(const NSVGimage &image, Vec2f& min, Vec2f &max) +{ + for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) + for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { + if (min.x() > path->bounds[0]) + min.x() = path->bounds[0]; + if (min.y() > path->bounds[1]) + min.y() = path->bounds[1]; + if (max.x() < path->bounds[2]) + max.x() = path->bounds[2]; + if (max.y() < path->bounds[3]) + max.y() = path->bounds[3]; + } +} + +NSVGimage_ptr nsvgParseFromFile(const std::string &filename, const char *units, float dpi) +{ + NSVGimage *image = ::nsvgParseFromFile(filename.c_str(), units, dpi); + return {image, &nsvgDelete}; +} + +std::unique_ptr read_from_disk(const std::string &path) +{ + boost::nowide::ifstream fs{path}; + if (!fs.is_open()) + return nullptr; + std::stringstream ss; + ss << fs.rdbuf(); + return std::make_unique(ss.str()); +} + +NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units, float dpi){ + // NOTE: nsvg parser consume data from input(char *) + size_t size = file_data.size(); + // file data could be big, so it is allocated on heap + std::unique_ptr data_copy(new char[size+1]); + memcpy(data_copy.get(), file_data.c_str(), size); + data_copy[size] = '\0'; // data for nsvg must be null terminated + NSVGimage *image = ::nsvgParse(data_copy.get(), units, dpi); + return {image, &nsvgDelete}; +} + +size_t get_shapes_count(const NSVGimage &image) +{ + size_t count = 0; + for (NSVGshape * s = image.shapes; s != NULL; s = s->next) + ++count; + return count; +} + +//void save(const NSVGimage &image, std::ostream &data) +//{ +// data << ""; +// +// // tl .. top left +// Vec2f tl(std::numeric_limits::max(), std::numeric_limits::max()); +// // br .. bottom right +// Vec2f br(std::numeric_limits::min(), std::numeric_limits::min()); +// bounds(image, tl, br); +// +// tl.x() = std::floor(tl.x()); +// tl.y() = std::floor(tl.y()); +// +// br.x() = std::ceil(br.x()); +// br.y() = std::ceil(br.y()); +// Vec2f s = br - tl; +// Point size = s.cast(); +// +// data << "\n"; +// data << "\n"; +// +// std::array buffer; +// auto write_point = [&tl, &buffer](std::string &d, const float *p) { +// float x = p[0] - tl.x(); +// float y = p[1] - tl.y(); +// auto to_string = [&buffer](float f) -> std::string { +// auto [ptr, ec] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), f); +// if (ec != std::errc{}) +// return "0"; +// return std::string(buffer.data(), ptr); +// }; +// d += to_string(x) + "," + to_string(y) + " "; +// }; +// +// for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { +// enum struct Type { move, line, curve, close }; // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d +// Type type = Type::move; +// std::string d = "M "; // move on start point +// for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { +// if (path->npts <= 1) +// continue; +// +// if (type == Type::close) { +// type = Type::move; +// // NOTE: After close must be a space +// d += " M "; // move on start point +// } +// write_point(d, path->pts); +// size_t path_size = static_cast(path->npts - 1); +// +// if (path->closed) { +// // Do not use last point in path it is duplicit +// if (path->npts <= 4) +// continue; +// path_size = static_cast(path->npts - 4); +// } +// +// for (size_t i = 0; i < path_size; i += 3) { +// const float *p = &path->pts[i * 2]; +// if (!::is_line(p)) { +// if (type != Type::curve) { +// type = Type::curve; +// d += "C "; // start sequence of triplets defining curves +// } +// write_point(d, &p[2]); +// write_point(d, &p[4]); +// } else { +// +// if (type != Type::line) { +// type = Type::line; +// d += "L "; // start sequence of line points +// } +// } +// write_point(d, &p[6]); +// } +// if (path->closed) { +// type = Type::close; +// d += "Z"; // start sequence of line points +// } +// } +// if (type != Type::close) { +// //type = Type::close; +// d += "Z"; // closed path +// } +// data << "\n"; +// } +// data << "\n"; +//} +// +//bool save(const NSVGimage &image, const std::string &svg_file_path) +//{ +// std::ofstream file{svg_file_path}; +// if (!file.is_open()) +// return false; +// save(image, file); +// return true; +//} +} // namespace Slic3r + +namespace { +using namespace Slic3r; // Polygon + Vec2f + +Point::coord_type to_coor(float val, double scale) { return static_cast(std::round(val * scale)); } + +bool need_flattening(float tessTol, const Vec2f &p1, const Vec2f &p2, const Vec2f &p3, const Vec2f &p4) { + // f .. first + // s .. second + auto det = [](const Vec2f &f, const Vec2f &s) { + return std::fabs(f.x() * s.y() - f.y() * s.x()); + }; + + Vec2f pd = (p4 - p1); + Vec2f pd2 = (p2 - p4); + float d2 = det(pd2, pd); + Vec2f pd3 = (p3 - p4); + float d3 = det(pd3, pd); float d23 = d2 + d3; - if ((d23 * d23) < tessTol * (pd.x() * pd.x() + pd.y() * pd.y())) { - polygon.points.emplace_back(p4.x(), p4.y()); + return (d23 * d23) >= tessTol * pd.squaredNorm(); +} + +// see function nsvg__lineTo(NSVGparser* p, float x, float y) +bool is_line(const float *p, float precision){ + //Vec2f p1(p[0], p[1]); + //Vec2f p2(p[2], p[3]); + //Vec2f p3(p[4], p[5]); + //Vec2f p4(p[6], p[7]); + float dx_3 = (p[6] - p[0]) / 3.f; + float dy_3 = (p[7] - p[1]) / 3.f; + + return + is_approx(p[2], p[0] + dx_3, precision) && + is_approx(p[4], p[6] - dx_3, precision) && + is_approx(p[3], p[1] + dy_3, precision) && + is_approx(p[5], p[7] - dy_3, precision); +} + +/// +/// Convert cubic curve to lines +/// Inspired by nanosvgrast.h function nsvgRasterize -> nsvg__flattenShape -> nsvg__flattenCubicBez +/// https://github.com/memononen/nanosvg/blob/f0a3e1034dd22e2e87e5db22401e44998383124e/src/nanosvgrast.h#L335 +/// +/// Result points +/// Tesselation tolerance +/// Curve point +/// Curve point +/// Curve point +/// Curve point +/// Actual depth of recursion +void flatten_cubic_bez(Points &points, float tessTol, const Vec2f& p1, const Vec2f& p2, const Vec2f& p3, const Vec2f& p4, int level) +{ + if (!need_flattening(tessTol, p1, p2, p3, p4)) { + Point::coord_type x = static_cast(std::round(p4.x())); + Point::coord_type y = static_cast(std::round(p4.y())); + points.emplace_back(x, y); return; } --level; - if (level == 0) return; + if (level == 0) + return; + + Vec2f p12 = (p1 + p2) * 0.5f; + Vec2f p23 = (p2 + p3) * 0.5f; + Vec2f p34 = (p3 + p4) * 0.5f; + Vec2f p123 = (p12 + p23) * 0.5f; Vec2f p234 = (p23 + p34) * 0.5f; Vec2f p1234 = (p123 + p234) * 0.5f; - flatten_cubic_bez(polygon, tessTol, p1, p12, p123, p1234, level); - flatten_cubic_bez(polygon, tessTol, p1234, p234, p34, p4, level); + flatten_cubic_bez(points, tessTol, p1, p12, p123, p1234, level); + flatten_cubic_bez(points, tessTol, p1234, p234, p34, p4, level); } -Polygons NSVGUtils::to_polygons(NSVGimage *image, float tessTol, int max_level) +LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m) { - Polygons polygons; - for (NSVGshape *shape = image->shapes; shape != NULL; - shape = shape->next) { - if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; - Slic3r::Polygon polygon; - if (shape->fill.type != NSVG_PAINT_NONE) { - for (NSVGpath *path = shape->paths; path != NULL; - path = path->next) { - // Flatten path - polygon.points.emplace_back(path->pts[0], path->pts[1]); - size_t path_size = (path->npts > 1) ? - static_cast(path->npts - 1) : 0; - for (size_t i = 0; i < path_size; i += 3) { - float *p = &path->pts[i * 2]; - Vec2f p1(p[0], p[1]), p2(p[2], p[3]), p3(p[4], p[5]), - p4(p[6], p[7]); - flatten_cubic_bez(polygon, tessTol, p1, p2, p3, p4, - max_level); - } - if (path->closed && !polygon.empty()) { - polygons.push_back(polygon); - polygon = Slic3r::Polygon(); + LinesPath result; + Polygons &polygons = result.polygons; + Polylines &polylines = result.polylines; + + // multiple use of allocated memmory for points between paths + Points points; + for (NSVGpath *path = first_path; path != NULL; path = path->next) { + // Flatten path + Point::coord_type x = to_coor(path->pts[0], param.scale); + Point::coord_type y = to_coor(path->pts[1], param.scale); + points.emplace_back(x, y); + size_t path_size = (path->npts > 1) ? static_cast(path->npts - 1) : 0; + for (size_t i = 0; i < path_size; i += 3) { + const float *p = &path->pts[i * 2]; + if (is_line(p)) { + // point p4 + Point::coord_type xx = to_coor(p[6], param.scale); + Point::coord_type yy = to_coor(p[7], param.scale); + points.emplace_back(xx, yy); + continue; + } + Vec2f p1(p[0], p[1]); + Vec2f p2(p[2], p[3]); + Vec2f p3(p[4], p[5]); + Vec2f p4(p[6], p[7]); + flatten_cubic_bez(points, param.tesselation_tolerance, + p1 * param.scale, p2 * param.scale, p3 * param.scale, p4 * param.scale, + param.max_level); + } + assert(!points.empty()); + if (points.empty()) + continue; + + if (param.is_y_negative) + for (Point &p : points) + p.y() = -p.y(); + + if (path->closed) { + polygons.emplace_back(points); + } else { + polylines.emplace_back(points); + } + // prepare for new path - recycle alocated memory + points.clear(); + } + remove_same_neighbor(polygons); + remove_same_neighbor(polylines); + return result; +} + +HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) +{ + Polygons fill = lines_path.polygons; // copy + + // close polyline to create polygon + polygons_append(fill, to_polygons(lines_path.polylines)); + if (fill.empty()) + return {}; + + // if (shape->fillRule == NSVGfillRule::NSVG_FILLRULE_NONZERO) + bool is_non_zero = true; + if (shape.fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD) + is_non_zero = false; + + return Emboss::heal_polygons(fill, is_non_zero, param.max_heal_iteration); +} + +struct DashesParam{ + // first dash length + float dash_length = 1.f; // scaled + + // is current dash .. true + // is current space .. false + bool is_line = true; + + // current index to array + unsigned char dash_index = 0; + static constexpr size_t max_dash_array_size = 8; // limitation of nanosvg strokeDashArray + std::array dash_array; // scaled + unsigned char dash_count = 0; // count of values in array + + explicit DashesParam(const NSVGshape &shape, double scale) : + dash_count(shape.strokeDashCount) + { + assert(dash_count > 0); + assert(dash_count <= max_dash_array_size); // limitation of nanosvg strokeDashArray + for (size_t i = 0; i < dash_count; ++i) + dash_array[i] = static_cast(shape.strokeDashArray[i] * scale); + + // Figure out dash offset. + float all_dash_length = 0; + for (unsigned char j = 0; j < dash_count; ++j) + all_dash_length += dash_array[j]; + + if (dash_count%2 == 1) // (shape.strokeDashCount & 1) + all_dash_length *= 2.0f; + + // Find location inside pattern + float dash_offset = fmodf(static_cast(shape.strokeDashOffset * scale), all_dash_length); + if (dash_offset < 0.0f) + dash_offset += all_dash_length; + + while (dash_offset > dash_array[dash_index]) { + dash_offset -= dash_array[dash_index]; + dash_index = (dash_index + 1) % shape.strokeDashCount; + is_line = !is_line; + } + + dash_length = dash_array[dash_index] - dash_offset; + } +}; + +Polylines to_dashes(const Polyline &polyline, const DashesParam& param) +{ + Polylines dashes; + Polyline dash; // cache for one dash in dashed line + Point prev_point; + + bool is_line = param.is_line; + unsigned char dash_index = param.dash_index; + float dash_length = param.dash_length; // current rest of dash distance + for (const Point &point : polyline.points) { + if (&point == &polyline.points.front()) { + // is first point + prev_point = point; // copy + continue; + } + + Point diff = point - prev_point; + float line_segment_length = diff.cast().norm(); + while (dash_length < line_segment_length) { + // Calculate intermediate point + float d = dash_length / line_segment_length; + Point move_point = diff * d; + Point intermediate = prev_point + move_point; + + // add Dash in stroke + if (is_line) { + if (dash.empty()) { + dashes.emplace_back(Points{prev_point, intermediate}); + } else { + dash.append(prev_point); + dash.append(intermediate); + dashes.push_back(dash); + dash.clear(); } } + + diff -= move_point; + line_segment_length -= dash_length; + prev_point = intermediate; + + // Advance dash pattern + is_line = !is_line; + dash_index = (dash_index + 1) % param.dash_count; + dash_length = param.dash_array[dash_index]; } - if (!polygon.empty()) - polygons.push_back(polygon); + + if (is_line) + dash.append(prev_point); + dash_length -= line_segment_length; + prev_point = point; // copy } - return polygons; + + // add last dash + if (is_line){ + assert(!dash.empty()); + dash.append(prev_point); // prev_point == polyline.points.back() + dashes.push_back(dash); + } + return dashes; } -ExPolygons NSVGUtils::to_ExPolygons(NSVGimage *image, - float tessTol, - int max_level) +HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) { - Polygons polygons = to_polygons(image, tessTol, max_level); + // convert stroke to polygon + ClipperLib::JoinType join_type = ClipperLib::JoinType::jtSquare; + switch (static_cast(shape.strokeLineJoin)) { + case NSVGlineJoin::NSVG_JOIN_BEVEL: join_type = ClipperLib::JoinType::jtSquare; break; + case NSVGlineJoin::NSVG_JOIN_MITER: join_type = ClipperLib::JoinType::jtMiter; break; + case NSVGlineJoin::NSVG_JOIN_ROUND: join_type = ClipperLib::JoinType::jtRound; break; + } - // Fix Y axis - for (Polygon &polygon : polygons) - for (Point &p : polygon.points) p.y() *= -1; + double mitter = shape.miterLimit * param.scale; + if (join_type == ClipperLib::JoinType::jtRound) { + // mitter is used as ArcTolerance + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm + mitter = std::pow(param.tesselation_tolerance, 1/3.); + } + float stroke_width = static_cast(shape.strokeWidth * param.scale); - return Slic3r::union_ex(polygons); -} \ No newline at end of file + ClipperLib::EndType end_type = ClipperLib::EndType::etOpenButt; + switch (static_cast(shape.strokeLineCap)) { + case NSVGlineCap::NSVG_CAP_BUTT: end_type = ClipperLib::EndType::etOpenButt; break; + case NSVGlineCap::NSVG_CAP_ROUND: end_type = ClipperLib::EndType::etOpenRound; break; + case NSVGlineCap::NSVG_CAP_SQUARE: end_type = ClipperLib::EndType::etOpenSquare; break; + } + + Polygons result; + if (shape.strokeDashCount > 0) { + DashesParam params(shape, param.scale); + Polylines dashes; + for (const Polyline &polyline : lines_path.polylines) + polylines_append(dashes, to_dashes(polyline, params)); + for (const Polygon &polygon : lines_path.polygons) + polylines_append(dashes, to_dashes(to_polyline(polygon), params)); + result = offset(dashes, stroke_width / 2, join_type, mitter, end_type); + } else { + result = contour_to_polygons(lines_path.polygons, stroke_width, join_type, mitter); + polygons_append(result, offset(lines_path.polylines, stroke_width / 2, join_type, mitter, end_type)); + } + + bool is_non_zero = true; + return Emboss::heal_polygons(result, is_non_zero, param.max_heal_iteration); +} + +} // namespace \ No newline at end of file diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp index ce71237397..8c3a27d40f 100644 --- a/src/libslic3r/NSVGUtils.hpp +++ b/src/libslic3r/NSVGUtils.hpp @@ -5,34 +5,81 @@ #ifndef slic3r_NSVGUtils_hpp_ #define slic3r_NSVGUtils_hpp_ +#include +#include +#include #include "Polygon.hpp" #include "ExPolygon.hpp" +#include "EmbossShape.hpp" // ExPolygonsWithIds #include "nanosvg/nanosvg.h" // load SVG file +// Helper function to work with nano svg namespace Slic3r { -// Helper function to work with nano svg -class NSVGUtils +/// +/// Paramreters for conversion curve from SVG to lines in Polygon +/// +struct NSVGLineParams { -public: - NSVGUtils() = delete; + // Smaller will divide curve to more lines + // NOTE: Value is in image scale + double tesselation_tolerance = 10.f; - // inspired by nanosvgrast.h function nsvgRasterize->nsvg__flattenShape - static void flatten_cubic_bez(Polygon &polygon, - float tessTol, - Vec2f p1, - Vec2f p2, - Vec2f p3, - Vec2f p4, - int level); - // convert svg image to ExPolygons - static ExPolygons to_ExPolygons(NSVGimage *image, - float tessTol = 10., - int max_level = 10); - // convert svg paths to Polygons - static Polygons to_polygons(NSVGimage *image, - float tessTol = 10., - int max_level = 10); + // Maximal depth of recursion for conversion curve to lines + int max_level = 10; + + // Multiplicator of point coors + // NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer --> Slic3r::Point + double scale = 1. / SCALING_FACTOR; + + // Flag wether y is negative, when true than y coor is multiplied by -1 + bool is_y_negative = true; + + // Is used only with rounded Stroke + double arc_tolerance = 1.; + + // Maximal count of heal iteration + unsigned max_heal_iteration = 10; + + explicit NSVGLineParams(double tesselation_tolerance): + tesselation_tolerance(tesselation_tolerance), + arc_tolerance(std::pow(tesselation_tolerance, 1/3.)) + {} }; + +/// +/// Convert .svg opened by nanoSvg to shapes stored in expolygons with ids +/// +/// Parsed svg file by NanoSvg +/// Smaller will divide curve to more lines +/// NOTE: Value is in image scale +/// Maximal depth for conversion curve to lines +/// Multiplicator of point coors +/// NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer +/// Shapes from svg image - fill + stroke +ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m); + +// help functions - prepare to be tested +/// Flag is y negative, when true than y coor is multiplied by -1 +Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m); + +void bounds(const NSVGimage &image, Vec2f &min, Vec2f &max); + +// read text data from file +std::unique_ptr read_from_disk(const std::string &path); + +using NSVGimage_ptr = std::unique_ptr; +NSVGimage_ptr nsvgParseFromFile(const std::string &svg_file_path, const char *units = "mm", float dpi = 96.0f); +NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units = "mm", float dpi = 96.0f); + +/// +/// Iterate over shapes and calculate count +/// +/// Contain pointer to first shape +/// Count of shapes +size_t get_shapes_count(const NSVGimage &image); + +//void save(const NSVGimage &image, std::ostream &data); +//bool save(const NSVGimage &image, const std::string &svg_file_path); } // namespace Slic3r #endif // slic3r_NSVGUtils_hpp_ diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index bf6f9ef1f9..085075a611 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -163,6 +163,21 @@ Pointf3s transform(const Pointf3s& points, const Transform3d& t); /// Is positive determinant inline bool has_reflection(const Transform3d &transform) { return transform.matrix().determinant() < 0; } +/// +/// Getter on base of transformation matrix +/// +/// column index +/// source transformation +/// Base of transformation matrix +inline const Vec3d get_base(unsigned index, const Transform3d &transform) { return transform.linear().col(index); } +inline const Vec3d get_x_base(const Transform3d &transform) { return get_base(0, transform); } +inline const Vec3d get_y_base(const Transform3d &transform) { return get_base(1, transform); } +inline const Vec3d get_z_base(const Transform3d &transform) { return get_base(2, transform); } +inline const Vec3d get_base(unsigned index, const Transform3d::LinearPart &transform) { return transform.col(index); } +inline const Vec3d get_x_base(const Transform3d::LinearPart &transform) { return get_base(0, transform); } +inline const Vec3d get_y_base(const Transform3d::LinearPart &transform) { return get_base(1, transform); } +inline const Vec3d get_z_base(const Transform3d::LinearPart &transform) { return get_base(2, transform); } + template using Vec = Eigen::Matrix; class Point : public Vec2crd @@ -559,6 +574,27 @@ inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) inline Point align_to_grid(Point coord, Point spacing, Point base) { return Point(align_to_grid(coord.x(), spacing.x(), base.x()), align_to_grid(coord.y(), spacing.y(), base.y())); } +// MinMaxLimits +template struct MinMax { T min; T max;}; +template +static bool apply(std::optional &val, const MinMax &limit) { + if (!val.has_value()) return false; + return apply(*val, limit); +} +template +static bool apply(T &val, const MinMax &limit) +{ + if (val > limit.max) { + val = limit.max; + return true; + } + if (val < limit.min) { + val = limit.min; + return true; + } + return false; +} + } // namespace Slic3r // start Boost diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index fe8a616ddf..2cbfeb6c71 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -448,6 +448,38 @@ bool has_duplicate_points(const Polygons &polys) #endif } +bool remove_same_neighbor(Polygon &polygon) +{ + Points &points = polygon.points; + if (points.empty()) + return false; + auto last = std::unique(points.begin(), points.end()); + + // remove first and last neighbor duplication + if (const Point &last_point = *(last - 1); last_point == points.front()) { + --last; + } + + // no duplicits + if (last == points.end()) + return false; + + points.erase(last, points.end()); + return true; +} + +bool remove_same_neighbor(Polygons &polygons) +{ + if (polygons.empty()) + return false; + bool exist = false; + for (Polygon &polygon : polygons) + exist |= remove_same_neighbor(polygon); + // remove empty polygons + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.points.size() <= 2; }), polygons.end()); + return exist; +} + static inline bool is_stick(const Point &p1, const Point &p2, const Point &p3) { Point v1 = p2 - p1; diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index 17e70d224a..6cfb121ba2 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -119,6 +119,10 @@ inline bool has_duplicate_points(Polygon &&poly) { return has_duplicate_poi inline bool has_duplicate_points(const Polygon &poly) { return has_duplicate_points(poly.points); } bool has_duplicate_points(const Polygons &polys); +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polygon &polygon); +bool remove_same_neighbor(Polygons &polygons); + inline double total_length(const Polygons &polylines) { double total = 0; for (Polygons::const_iterator it = polylines.begin(); it != polylines.end(); ++it) @@ -253,6 +257,18 @@ inline Polylines to_polylines(Polygons &&polys) return polylines; } +// close polyline to polygon (connect first and last point in polyline) +inline Polygons to_polygons(const Polylines &polylines) +{ + Polygons out; + out.reserve(polylines.size()); + for (const Polyline &polyline : polylines) { + if (polyline.size()) + out.emplace_back(polyline.points); + } + return out; +} + inline Polygons to_polygons(const VecOfPoints &paths) { Polygons out; diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index 25d880cb9c..3a5c91cdc9 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -214,6 +214,33 @@ BoundingBox get_extents(const Polylines &polylines) return bb; } +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polyline &polyline) { + Points &points = polyline.points; + if (points.empty()) + return false; + auto last = std::unique(points.begin(), points.end()); + + // no duplicits + if (last == points.end()) + return false; + + points.erase(last, points.end()); + return true; +} + +bool remove_same_neighbor(Polylines &polylines){ + if (polylines.empty()) + return false; + bool exist = false; + for (Polyline &polyline : polylines) + exist |= remove_same_neighbor(polyline); + // remove empty polylines + polylines.erase(std::remove_if(polylines.begin(), polylines.end(), [](const Polyline &p) { return p.points.size() <= 1; }), polylines.end()); + return exist; +} + + const Point& leftmost_point(const Polylines &polylines) { if (polylines.empty()) diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index cb5b691fcf..8c7503648d 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -99,6 +99,10 @@ inline bool operator!=(const Polyline &lhs, const Polyline &rhs) { return lhs.po extern BoundingBox get_extents(const Polyline &polyline); extern BoundingBox get_extents(const Polylines &polylines); +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polyline &polyline); +bool remove_same_neighbor(Polylines &polylines); + inline double total_length(const Polylines &polylines) { double total = 0; for (const Polyline &pl : polylines) diff --git a/src/libslic3r/TextConfiguration.hpp b/src/libslic3r/TextConfiguration.hpp index 91076e26a7..8da67cdd0a 100644 --- a/src/libslic3r/TextConfiguration.hpp +++ b/src/libslic3r/TextConfiguration.hpp @@ -30,15 +30,6 @@ struct FontProp // When not set value is zero and is not stored std::optional line_gap; // [in font point] - // Z depth of text - float emboss; // [in mm] - - // Flag that text should use surface cutted from object - // FontProp::distance should without value - // FontProp::emboss should be positive number - // Note: default value is false - bool use_surface; - // positive value mean wider character shape // negative value mean tiner character shape // When not set value is zero and is not stored @@ -49,17 +40,6 @@ struct FontProp // When not set value is zero and is not stored std::optional skew; // [ration x:y] - // distance from surface point - // used for move over model surface - // When not set value is zero and is not stored - std::optional distance; // [in mm] - - // Angle of rotation around emboss direction (Z axis) - // It is calculate on the fly from volume world transformation - // only StyleManager keep actual value for comparision with style - // When not set value is zero and is not stored - std::optional angle; // [in radians] form -Pi to Pi - // Parameter for True Type Font collections // Select index of font in collection std::optional collection_number; @@ -74,7 +54,7 @@ struct FontProp // change pivot of text // When not set, center is used and is not stored Align align = Align(HorizontalAlign::center, VerticalAlign::center); - + ////// // Duplicit data to wxFontDescriptor // used for store/load .3mf file @@ -96,54 +76,38 @@ 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), per_glyph(false) + FontProp(float line_height = 10.f) : size_in_mm(line_height), per_glyph(false) {} bool operator==(const FontProp& other) const { return char_gap == other.char_gap && line_gap == other.line_gap && - use_surface == other.use_surface && per_glyph == other.per_glyph && align == other.align && - is_approx(emboss, other.emboss) && is_approx(size_in_mm, other.size_in_mm) && is_approx(boldness, other.boldness) && - is_approx(skew, other.skew) && - is_approx(distance, other.distance) && - is_approx(angle, other.angle); + is_approx(skew, other.skew); } // undo / redo stack recovery template void save(Archive &ar) const { - ar(emboss, use_surface, size_in_mm, per_glyph, align.first, align.second); + ar(size_in_mm, per_glyph, align.first, align.second); cereal::save(ar, char_gap); cereal::save(ar, line_gap); cereal::save(ar, boldness); cereal::save(ar, skew); - cereal::save(ar, distance); - cereal::save(ar, angle); cereal::save(ar, collection_number); - cereal::save(ar, family); - cereal::save(ar, face_name); - cereal::save(ar, style); - cereal::save(ar, weight); } template void load(Archive &ar) { - ar(emboss, use_surface, size_in_mm, per_glyph, align.first, align.second); + ar(size_in_mm, per_glyph, align.first, align.second); cereal::load(ar, char_gap); cereal::load(ar, line_gap); cereal::load(ar, boldness); cereal::load(ar, skew); - cereal::load(ar, distance); - cereal::load(ar, angle); cereal::load(ar, collection_number); - cereal::load(ar, family); - cereal::load(ar, face_name); - cereal::load(ar, style); - cereal::load(ar, weight); } }; @@ -197,9 +161,7 @@ struct EmbossStyle } // undo / redo stack recovery - template void serialize(Archive &ar){ - ar(name, path, type, prop); - } + template void serialize(Archive &ar){ ar(name, path, type, prop); } }; // Emboss style name inside vector is unique @@ -220,21 +182,8 @@ struct TextConfiguration // Embossed text value std::string text = "None"; - // !!! Volume stored in .3mf has transformed vertices. - // (baked transformation into vertices position) - // Only place for fill this is when load from .3mf - // This is correct volume transformation - std::optional fix_3mf_tr; - // undo / redo stack recovery - template void save(Archive &ar) const{ - ar(text, style); - cereal::save(ar, fix_3mf_tr); - } - template void load(Archive &ar){ - ar(text, style); - cereal::load(ar, fix_3mf_tr); - } + template void serialize(Archive &ar) { ar(style, text); } }; } // namespace Slic3r diff --git a/src/libslic3r/Triangulation.cpp b/src/libslic3r/Triangulation.cpp index 5c45122593..c75a531973 100644 --- a/src/libslic3r/Triangulation.cpp +++ b/src/libslic3r/Triangulation.cpp @@ -65,7 +65,7 @@ inline bool has_self_intersection( lines.reserve(constrained_half_edges.size()); for (const auto &he : constrained_half_edges) lines.emplace_back(points[he.first], points[he.second]); - return !intersection_points(lines).empty(); + return !get_intersections(lines).empty(); } } // namespace priv diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 17bc0052ed..8f9900bbf2 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -76,6 +76,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoSeam.hpp GUI/Gizmos/GLGizmoSimplify.cpp GUI/Gizmos/GLGizmoSimplify.hpp + GUI/Gizmos/GLGizmoSVG.cpp + GUI/Gizmos/GLGizmoSVG.hpp GUI/Gizmos/GLGizmoMmuSegmentation.cpp GUI/Gizmos/GLGizmoMmuSegmentation.hpp GUI/Gizmos/GLGizmoMeasure.cpp @@ -274,8 +276,6 @@ set(SLIC3R_GUI_SOURCES Utils/Duet.hpp Utils/EmbossStyleManager.cpp Utils/EmbossStyleManager.hpp - Utils/EmbossStylesSerializable.cpp - Utils/EmbossStylesSerializable.hpp Utils/FlashAir.cpp Utils/FlashAir.hpp Utils/FontConfigHelp.cpp diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index eda398f680..86505c1dea 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3939,7 +3939,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) type == GLGizmosManager::EType::Move || type == GLGizmosManager::EType::Rotate || type == GLGizmosManager::EType::Scale || - type == GLGizmosManager::EType::Emboss) ) { + type == GLGizmosManager::EType::Emboss|| + type == GLGizmosManager::EType::Svg) ) { for (int hover_volume_id : m_hover_volume_idxs) { const GLVolume &hover_gl_volume = *m_volumes.volumes[hover_volume_id]; int object_idx = hover_gl_volume.object_idx(); @@ -3948,12 +3949,20 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) int hover_volume_idx = hover_gl_volume.volume_idx(); if (hover_volume_idx < 0 || static_cast(hover_volume_idx) >= hover_object->volumes.size()) continue; const ModelVolume* hover_volume = hover_object->volumes[hover_volume_idx]; - if (!hover_volume->text_configuration.has_value()) continue; - m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); - if (type != GLGizmosManager::EType::Emboss) - m_gizmos.open_gizmo(GLGizmosManager::EType::Emboss); - wxGetApp().obj_list()->update_selections(); - return; + + if (hover_volume->text_configuration.has_value()) { + m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); + if (type != GLGizmosManager::EType::Emboss) + m_gizmos.open_gizmo(GLGizmosManager::EType::Emboss); + wxGetApp().obj_list()->update_selections(); + return; + } else if (hover_volume->emboss_shape.has_value()) { + m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); + if (type != GLGizmosManager::EType::Svg) + m_gizmos.open_gizmo(GLGizmosManager::EType::Svg); + wxGetApp().obj_list()->update_selections(); + return; + } } } @@ -7892,10 +7901,10 @@ const ModelVolume *get_model_volume(const GLVolume &v, const Model &model) return ret; } -const ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects) +ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects) { for (const ModelObject *obj : objects) - for (const ModelVolume *vol : obj->volumes) + for (ModelVolume *vol : obj->volumes) if (vol->id() == volume_id) return vol; return nullptr; diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 6c1b74f815..0660ca7cc9 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1126,7 +1126,7 @@ private: }; const ModelVolume *get_model_volume(const GLVolume &v, const Model &model); -const ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects); +ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects); ModelVolume *get_model_volume(const GLVolume &v, const ModelObjectPtrs &objects); ModelVolume *get_model_volume(const GLVolume &v, const ModelObject &object); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 5e8c121100..22b052c777 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -18,6 +18,7 @@ #include "Selection.hpp" #include "format.hpp" #include "Gizmos/GLGizmoEmboss.hpp" +#include "Gizmos/GLGizmoSVG.hpp" #include #include "slic3r/Utils/FixModelByWin10.hpp" @@ -174,6 +175,12 @@ static const constexpr std::array, 3> TEXT {L("Add negative text"), "add_text_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME {L("Add text modifier"), "add_text_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER }}; +// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important +static const constexpr std::array, 3> SVG_VOLUME_ICONS{{ + {L("Add SVG part"), "svg_part"}, // ~ModelVolumeType::MODEL_PART + {L("Add negative SVG"), "svg_negative"}, // ~ModelVolumeType::NEGATIVE_VOLUME + {L("Add SVG modifier"), "svg_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER +}}; static Plater* plater() { @@ -445,7 +452,7 @@ std::vector MenuFactory::get_volume_bitmaps() { std::vector volume_bmps; volume_bmps.reserve(ADD_VOLUME_MENU_ITEMS.size()); - for (auto item : ADD_VOLUME_MENU_ITEMS) + for (const auto& item : ADD_VOLUME_MENU_ITEMS) volume_bmps.push_back(get_bmp_bundle(item.second)); return volume_bmps; } @@ -454,7 +461,16 @@ std::vector MenuFactory::get_text_volume_bitmaps() { std::vector volume_bmps; volume_bmps.reserve(TEXT_VOLUME_ICONS.size()); - for (auto item : TEXT_VOLUME_ICONS) + for (const auto& item : TEXT_VOLUME_ICONS) + volume_bmps.push_back(get_bmp_bundle(item.second)); + return volume_bmps; +} + +std::vector MenuFactory::get_svg_volume_bitmaps() +{ + std::vector volume_bmps; + volume_bmps.reserve(SVG_VOLUME_ICONS.size()); + for (const auto &item : SVG_VOLUME_ICONS) volume_bmps.push_back(get_bmp_bundle(item.second)); return volume_bmps; } @@ -491,6 +507,7 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty } append_menu_item_add_text(sub_menu, type); + append_menu_item_add_svg(sub_menu, type); if (mode >= comAdvanced) { sub_menu->AppendSeparator(); @@ -501,42 +518,57 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty return sub_menu; } -void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item/* = true*/) -{ - auto add_text = [type](wxCommandEvent& evt) { - GLCanvas3D* canvas = plater()->canvas3D(); - GLGizmosManager& mng = canvas->get_gizmos_manager(); - GLGizmoBase* gizmo = mng.get_gizmo(GLGizmosManager::Emboss); - GLGizmoEmboss* emboss = dynamic_cast(gizmo); - assert(emboss != nullptr); - if (emboss == nullptr) return; - +static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) { + auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { + const GLCanvas3D *canvas = plater()->canvas3D(); + const GLGizmosManager &mng = canvas->get_gizmos_manager(); + GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type); + ModelVolumeType volume_type = type; // no selected object means create new object if (volume_type == ModelVolumeType::INVALID) volume_type = ModelVolumeType::MODEL_PART; auto screen_position = canvas->get_popup_menu_position(); - if (screen_position.has_value()) { - emboss->create_volume(volume_type, *screen_position); - } else { - emboss->create_volume(volume_type); - } + if (gizmo_type == GLGizmosManager::Emboss) { + auto emboss = dynamic_cast(gizmo_base); + assert(emboss != nullptr); + if (emboss == nullptr) return; + if (screen_position.has_value()) { + emboss->create_volume(volume_type, *screen_position); + } else { + emboss->create_volume(volume_type); + } + } else if (gizmo_type == GLGizmosManager::Svg) { + auto svg = dynamic_cast(gizmo_base); + assert(svg != nullptr); + if (svg == nullptr) return; + if (screen_position.has_value()) { + svg->create_volume(volume_type, *screen_position); + } else { + svg->create_volume(volume_type); + } + } }; - if ( type == ModelVolumeType::MODEL_PART - || type == ModelVolumeType::NEGATIVE_VOLUME - || type == ModelVolumeType::PARAMETER_MODIFIER - || type == ModelVolumeType::INVALID // cannot use gizmo without selected object - ) { - wxString item_name = is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": "; - item_name += _L("Text"); + if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER || + type == ModelVolumeType::INVALID // cannot use gizmo without selected object + ) { + wxString item_name = wxString(is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": ") + name; menu->AppendSeparator(); const std::string icon_name = is_submenu_item ? "" : ADD_VOLUME_MENU_ITEMS[int(type)].second; - append_menu_item(menu, wxID_ANY, item_name, "", add_text, icon_name, menu); + append_menu_item(menu, wxID_ANY, item_name, "", add_, icon_name, menu); } } +void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item/* = true*/){ + append_menu_itemm_add_(_L("Text"), GLGizmosManager::Emboss, menu, type, is_submenu_item); +} + +void MenuFactory::append_menu_item_add_svg(wxMenu *menu, ModelVolumeType type, bool is_submenu_item /* = true*/){ + append_menu_itemm_add_(_L("SVG"), GLGizmosManager::Svg, menu, type, is_submenu_item); +} + void MenuFactory::append_menu_items_add_volume(MenuType menu_type) { wxMenu* menu = menu_type == mtObjectFFF ? &m_object_menu : menu_type == mtObjectSLA ? &m_sla_object_menu : nullptr; @@ -989,15 +1021,18 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) wxString name = _L("Edit text"); auto can_edit_text = []() { - if (plater() != nullptr) { - const Selection& sel = plater()->get_selection(); - if (sel.volumes_count() == 1) { - const GLVolume* gl_vol = sel.get_first_volume(); - const ModelVolume* vol = plater()->model().objects[gl_vol->object_idx()]->volumes[gl_vol->volume_idx()]; - return vol->text_configuration.has_value(); - } - } - return false; + if (plater() == nullptr) + return false; + const Selection& selection = plater()->get_selection(); + if (selection.volumes_count() != 1) + return false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) + return false; + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume == nullptr) + return false; + return volume->is_text(); }; if (menu != &m_text_part_menu) { @@ -1009,7 +1044,7 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) } wxString description = _L("Ability to change text, font, size, ..."); - std::string icon = ""; + std::string icon = "cog"; auto open_emboss = [](const wxCommandEvent &) { GLGizmosManager &mng = plater()->canvas3D()->get_gizmos_manager(); if (mng.get_current_type() == GLGizmosManager::Emboss) @@ -1019,6 +1054,43 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) append_menu_item(menu, wxID_ANY, name, description, open_emboss, icon, nullptr, can_edit_text, m_parent); } +void MenuFactory::append_menu_item_edit_svg(wxMenu *menu) +{ + wxString name = _L("Edit SVG"); + auto can_edit_svg = []() { + if (plater() == nullptr) + return false; + const Selection& selection = plater()->get_selection(); + if (selection.volumes_count() != 1) + return false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) + return false; + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume == nullptr) + return false; + return volume->is_svg(); + }; + + if (menu != &m_svg_part_menu) { + const int menu_item_id = menu->FindItem(name); + if (menu_item_id != wxNOT_FOUND) + menu->Destroy(menu_item_id); + if (!can_edit_svg()) + return; + } + + wxString description = _L("Change SVG source file, projection, size, ..."); + std::string icon = "cog"; + auto open_svg = [](const wxCommandEvent &) { + GLGizmosManager &mng = plater()->canvas3D()->get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); // close() and reopen - move to be visible + mng.open_gizmo(GLGizmosManager::Svg); + }; + append_menu_item(menu, wxID_ANY, name, description, open_svg, icon, nullptr, can_edit_svg, m_parent); +} + MenuFactory::MenuFactory() { for (int i = 0; i < mtCount; i++) { @@ -1118,8 +1190,20 @@ void MenuFactory::create_text_part_menu() { wxMenu* menu = &m_text_part_menu; - append_menu_item_delete(menu); append_menu_item_edit_text(menu); + append_menu_item_delete(menu); + append_menu_item_fix_through_winsdk(menu); + append_menu_item_simplify(menu); + + append_immutable_part_menu_items(menu); +} + +void MenuFactory::create_svg_part_menu() +{ + wxMenu* menu = &m_svg_part_menu; + + append_menu_item_edit_svg(menu); + append_menu_item_delete(menu); append_menu_item_fix_through_winsdk(menu); append_menu_item_simplify(menu); @@ -1143,6 +1227,7 @@ void MenuFactory::init(wxWindow* parent) create_common_object_menu(&m_sla_object_menu); create_part_menu(); create_text_part_menu(); + create_svg_part_menu(); create_instance_menu(); } @@ -1165,6 +1250,7 @@ wxMenu* MenuFactory::object_menu() update_menu_items_instance_manipulation(mtObjectFFF); append_menu_item_invalidate_cut_info(&m_object_menu); append_menu_item_edit_text(&m_object_menu); + append_menu_item_edit_svg(&m_object_menu); return &m_object_menu; } @@ -1176,6 +1262,7 @@ wxMenu* MenuFactory::sla_object_menu() update_menu_items_instance_manipulation(mtObjectSLA); append_menu_item_invalidate_cut_info(&m_sla_object_menu); append_menu_item_edit_text(&m_sla_object_menu); + append_menu_item_edit_svg(&m_object_menu); return &m_sla_object_menu; } @@ -1196,6 +1283,12 @@ wxMenu* MenuFactory::text_part_menu() return &m_text_part_menu; } +wxMenu *MenuFactory::svg_part_menu() +{ + append_mutable_part_menu_items(&m_svg_part_menu); + return &m_svg_part_menu; +} + wxMenu* MenuFactory::instance_menu() { return &m_instance_menu; diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index b92b608bfd..945e22eca9 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -39,6 +39,7 @@ class MenuFactory public: static std::vector get_volume_bitmaps(); static std::vector get_text_volume_bitmaps(); + static std::vector get_svg_volume_bitmaps(); MenuFactory(); ~MenuFactory() = default; @@ -56,6 +57,7 @@ public: wxMenu* sla_object_menu(); wxMenu* part_menu(); wxMenu* text_part_menu(); + wxMenu* svg_part_menu(); wxMenu* instance_menu(); wxMenu* layer_menu(); wxMenu* multi_selection_menu(); @@ -72,6 +74,7 @@ private: MenuWithSeparators m_object_menu; MenuWithSeparators m_part_menu; MenuWithSeparators m_text_part_menu; + MenuWithSeparators m_svg_part_menu; MenuWithSeparators m_sla_object_menu; MenuWithSeparators m_default_menu; MenuWithSeparators m_instance_menu; @@ -87,10 +90,12 @@ private: void append_mutable_part_menu_items(wxMenu* menu); void create_part_menu(); void create_text_part_menu(); + void create_svg_part_menu(); void create_instance_menu(); wxMenu* append_submenu_add_generic(wxMenu* menu, ModelVolumeType type); void append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item = true); + void append_menu_item_add_svg(wxMenu *menu, ModelVolumeType type, bool is_submenu_item = true); void append_menu_items_add_volume(MenuType type); wxMenuItem* append_menu_item_layers_editing(wxMenu* menu); wxMenuItem* append_menu_item_settings(wxMenu* menu); @@ -112,6 +117,7 @@ private: // void append_menu_item_merge_to_single_object(wxMenu *menu); void append_menu_items_mirror(wxMenu *menu); void append_menu_item_edit_text(wxMenu *menu); + void append_menu_item_edit_svg(wxMenu *menu); void append_menu_items_instance_manipulation(wxMenu *menu); void update_menu_items_instance_manipulation(MenuType type); void append_menu_items_split(wxMenu *menu); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 6e185a1a0d..30774d38a5 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1040,7 +1040,11 @@ void ObjectList::show_context_menu(const bool evt_context_menu) get_selected_item_indexes(obj_idx, vol_idx, item); if (obj_idx < 0 || vol_idx < 0) return; - menu = object(obj_idx)->volumes[vol_idx]->text_configuration.has_value() ? plater->text_part_menu() : plater->part_menu(); + const ModelVolume *volume = object(obj_idx)->volumes[vol_idx]; + + menu = volume->is_text() ? plater->text_part_menu() : + volume->is_svg() ? plater->svg_part_menu() : + plater->part_menu(); } else menu = type & itInstance ? plater->instance_menu() : @@ -1779,12 +1783,7 @@ void ObjectList::load_shape_object_from_gallery(const wxArrayString& input_files wxGetApp().mainframe->update_title(); } -void ObjectList::load_mesh_object( - const TriangleMesh & mesh, - const std::string & name, - bool center, - const TextConfiguration *text_config /* = nullptr*/, - const Transform3d * transformation /* = nullptr*/) +void ObjectList::load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center) { PlaterAfterLoadAutoArrange plater_after_load_auto_arrange; // Add mesh to model as a new object @@ -1794,7 +1793,6 @@ void ObjectList::load_mesh_object( check_model_ids_validity(model); #endif /* _DEBUG */ - std::vector object_idxs; ModelObject* new_object = model.add_object(); new_object->name = name; new_object->add_instance(); // each object should have at list one instance @@ -1802,31 +1800,24 @@ void ObjectList::load_mesh_object( ModelVolume* new_volume = new_object->add_volume(mesh); new_object->sort_volumes(wxGetApp().app_config->get_bool("order_volumes")); new_volume->name = name; - if (text_config) - new_volume->text_configuration = *text_config; + // set a default extruder value, since user can't add it manually new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); new_object->invalidate_bounding_box(); - if (transformation) { - assert(!center); - Slic3r::Geometry::Transformation tr(*transformation); - new_object->instances[0]->set_transformation(tr); - } else { - auto bb = mesh.bounding_box(); - new_object->translate(-bb.center()); - new_object->instances[0]->set_offset( - center ? to_3d(wxGetApp().plater()->build_volume().bounding_volume2d().center(), -new_object->origin_translation.z()) : - bb.center()); - } + + auto bb = mesh.bounding_box(); + new_object->translate(-bb.center()); + new_object->instances[0]->set_offset( + center ? to_3d(wxGetApp().plater()->build_volume().bounding_volume2d().center(), -new_object->origin_translation.z()) : + bb.center()); new_object->ensure_on_bed(); - object_idxs.push_back(model.objects.size() - 1); #ifdef _DEBUG check_model_ids_validity(model); #endif /* _DEBUG */ - - paste_objects_into_list(object_idxs); + + paste_objects_into_list({model.objects.size() - 1}); #ifdef _DEBUG check_model_ids_validity(model); @@ -2999,6 +2990,7 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st volume_idx, volume->type(), volume->is_text(), + volume->is_svg(), get_warning_icon_name(volume->mesh().stats()), extruder2str(volume->config.has("extruder") ? volume->config.extruder() : 0)); add_settings_item(vol_item, &volume->config.get()); @@ -4275,7 +4267,8 @@ void ObjectList::change_part_type() types.emplace_back(ModelVolumeType::PARAMETER_MODIFIER); } - if (!volume->text_configuration.has_value()) { + // is not embossed(SVG or Text) + if (!volume->emboss_shape.has_value()) { for (const wxString& name : { _L("Support Blocker"), _L("Support Enforcer") }) names.Add(name); for (const ModelVolumeType type_id : { ModelVolumeType::SUPPORT_BLOCKER, ModelVolumeType::SUPPORT_ENFORCER }) diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index ae1f142dc4..28061cef46 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -31,7 +31,6 @@ class ModelConfig; class ModelObject; class ModelVolume; class TriangleMesh; -struct TextConfiguration; enum class ModelVolumeType : int; // FIXME: broken build on mac os because of this is missing: @@ -260,8 +259,7 @@ public: void load_shape_object(const std::string &type_name); void load_shape_object_from_gallery(); void load_shape_object_from_gallery(const wxArrayString& input_files); - void load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center = true, - const TextConfiguration* text_config = nullptr, const Transform3d* transformation = nullptr); + void load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center = true); bool del_object(const int obj_idx); bool del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index bfc957fe9b..ccac07f477 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -20,12 +20,9 @@ #include "slic3r/Utils/WxFontUtils.hpp" #include "slic3r/Utils/UndoRedo.hpp" -// TODO: remove include -#include "libslic3r/SVG.hpp" // debug store -#include "libslic3r/Geometry.hpp" // covex hull 2d +#include "libslic3r/Geometry.hpp" // to range pi pi #include "libslic3r/Timer.hpp" -#include "libslic3r/NSVGUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/ClipperUtils.hpp" // union_ex @@ -34,15 +31,24 @@ #include "libslic3r/BuildVolume.hpp" #include "imgui/imgui_stdlib.h" // using std::string for inputs -#include "nanosvg/nanosvg.h" // load SVG file #include #include #include #include #include // detection of change DPI +#include #include +#include // serialize deserialize facenames +#include +#include + +// cache font list by cereal +#include +#include +#include +#include #include #include // measure enumeration of fonts @@ -61,7 +67,6 @@ #define SHOW_FINE_POSITION // draw convex hull around volume #define DRAW_PLACE_TO_ADD_TEXT // Interactive draw of window position #define ALLOW_OPEN_NEAR_VOLUME -#define EXECUTE_PROCESS_ON_MAIN_THREAD // debug execution on main thread #endif // ALLOW_DEBUG_MODE //#define USE_PIXEL_SIZE_IN_WX_FONT @@ -71,19 +76,17 @@ using namespace Slic3r::Emboss; using namespace Slic3r::GUI; using namespace Slic3r::GUI::Emboss; -namespace priv { -template struct MinMax { T min; T max;}; +namespace { template struct Limit { // Limitation for view slider range in GUI MinMax gui; // Real limits for setting exacts values MinMax values; }; - // Variable keep limits for variables static const struct Limits { - MinMax emboss{0.01f, 1e4f}; // in mm + MinMax emboss{0.01, 1e4}; // in mm MinMax size_in_mm{0.1f, 1000.f}; // in mm Limit boldness{{-200.f, 200.f}, {-2e4f, 2e4f}}; // in font points Limit skew{{-1.f, 1.f}, {-100.f, 100.f}}; // ration without unit @@ -91,61 +94,8 @@ static const struct Limits MinMax line_gap{-20000, 20000}; // in font points // distance text object from surface MinMax angle{-180.f, 180.f}; // in degrees - - template - static bool apply(std::optional &val, const MinMax &limit) { - if (val.has_value()) - return apply(*val, limit); - return false; - } - template - static bool apply(T &val, const MinMax &limit) - { - if (val > limit.max) { - val = limit.max; - return true; - } - if (val < limit.min) { - val = limit.min; - return true; - } - return false; - } - } limits; -// Define where is up vector on model -constexpr double up_limit = 0.9; - -// Normalize radian angle from -PI to PI -template void to_range_pi_pi(T& angle) -{ - if (angle > PI || angle < -PI) { - int count = static_cast(std::round(angle / (2 * PI))); - angle -= static_cast(count * 2 * PI); - } -} -} // namespace priv -using namespace priv; - -GLGizmoEmboss::GLGizmoEmboss(GLCanvas3D &parent) - : GLGizmoBase(parent, M_ICON_FILENAME, -2) - , m_volume(nullptr) - , m_is_unknown_font(false) - , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) - , m_style_manager(m_imgui->get_glyph_ranges(), create_default_styles) - , m_job_cancel(nullptr) -{ - m_rotate_gizmo.set_group_id(0); - m_rotate_gizmo.set_force_local_coordinate(true); - // TODO: add suggestion to use https://fontawesome.com/ - // (copy & paste) unicode symbols from web - // paste HEX unicode into notepad move cursor after unicode press [alt] + [x] -} - -// Private namespace with helper function for create volume -namespace priv { - /// /// Prepare data for emboss /// @@ -156,68 +106,35 @@ namespace priv { /// Define type of volume - side of surface(in / out) /// Cancel for previous job /// Base data for emboss text -static DataBase create_emboss_data_base(const std::string &text, - StyleManager &style_manager, - TextLinesModel &text_lines, - const Selection &selection, - ModelVolumeType type, - 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); /// -/// Start job for add new volume to object with given transformation +/// Move window for edit emboss text near to embossed object +/// NOTE: embossed object must be selected /// -/// Define where to add -/// Volume transformation -/// Define text -/// Type of volume -static void start_create_volume_job(const ModelObject *object, - const Transform3d volume_trmat, - DataBase &emboss_data, - ModelVolumeType volume_type); +ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size); -/// -/// Start job for add new volume on surface of object defined by screen coor -/// -/// 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, + TextConfiguration &&text_configuration, const EmbossProjection& projection); + // Create shape from text + font configuration + EmbossShape &create_shape() override; + void write(ModelVolume &volume) const override; -/// -/// Find volume in selected object with closest convex hull to screen center. -/// Return -/// -/// Define where to search for closest -/// Canvas center(dependent on camera settings) -/// Actual objects -/// OUT: coordinate of controid of closest volume -/// OUT: closest volume -static void find_closest_volume(const Selection &selection, - const Vec2d &screen_center, - const Camera &camera, - const ModelObjectPtrs &objects, - Vec2d *closest_center, - const GLVolume **closest_volume); - -/// -/// Start job for add object with text into scene -/// -/// Define params of text -/// Screen coordinat, where to create new object laying on bed -static void start_create_object_job(DataBase &emboss_data, const Vec2d &coor); +private: + // Keep pointer on Data of font (glyph shapes) + FontFileWithCache m_font_file; + // font item is not used for create object + TextConfiguration m_text_configuration; +}; // Loaded icons enum // Have to match order of files in function GLGizmoEmboss::init_icons() @@ -252,104 +169,174 @@ enum class IconState : unsigned { activable = 0, hovered /*1*/, disabled /*2*/ } // selector for icon by enum const IconManager::Icon &get_icon(const IconManager::VIcons& icons, IconType type, IconState state); // short call of Slic3r::GUI::button -static bool draw_button(const IconManager::VIcons& icons, IconType type, bool disable = false); +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; + std::string name_truncated = ""; + size_t texture_index = 0; + // State for generation of texture + // when start generate create share pointers + std::shared_ptr> cancel = nullptr; + // R/W only on main thread - finalize of job + std::shared_ptr is_created = nullptr; +}; -} // namespace priv +// Implementation of forwarded struct +// Keep sorted list of loadable face names +struct Facenames +{ + // flag to keep need of enumeration fonts from OS + // false .. wants new enumeration check by Hash + // true .. already enumerated(During opened combo box) + bool is_init = false; -namespace { + bool has_truncated_names = false; + + // data of can_load() faces + std::vector faces = {}; + // Sorter set of Non valid face names in OS + std::vector bad = {}; + + // Configuration of font encoding + static const wxFontEncoding encoding = wxFontEncoding::wxFONTENCODING_SYSTEM; + + // Identify if preview texture exists + GLuint texture_id = 0; + + // protection for open too much font files together + // Gtk:ERROR:../../../../gtk/gtkiconhelper.c:494:ensure_surface_for_gicon: assertion failed (error == NULL): Failed to load + // /usr/share/icons/Yaru/48x48/status/image-missing.png: Error opening file /usr/share/icons/Yaru/48x48/status/image-missing.png: Too + // many open files (g-io-error-quark, 31) This variable must exist until no CreateFontImageJob is running + unsigned int count_opened_font_files = 0; + + // Configuration for texture height + const int count_cached_textures = 32; + + // index for new generated texture index(must be lower than count_cached_textures) + size_t texture_index = 0; + + // hash created from enumerated font from OS + // check when new font was installed + size_t hash = 0; + + // filtration pattern + std::string search = ""; + std::vector hide; // result of filtration +}; + +bool store(const Facenames &facenames); +bool load(Facenames &facenames); +void init_face_names(Facenames &facenames); +void init_truncated_names(Facenames &face_names, float max_width); + +// This configs holds GUI layout size given by translated texts. +// etc. When language changes, GUI is recreated and this class constructed again, +// so the change takes effect. (info by GLGizmoFdmSupports.hpp) +struct GuiCfg +{ + // Detect invalid config values when change monitor DPI + double screen_scale; + float main_toolbar_height; + + // Zero means it is calculated in init function + ImVec2 minimal_window_size = ImVec2(0, 0); + ImVec2 minimal_window_size_with_advance = ImVec2(0, 0); + ImVec2 minimal_window_size_with_collections = ImVec2(0, 0); + float height_of_volume_type_selector = 0.f; + float input_width = 0.f; + float delete_pos_x = 0.f; + float max_style_name_width = 0.f; + unsigned int icon_width = 0; + + // maximal width and height of style image + Vec2i max_style_image_size = Vec2i(0, 0); + + float indent = 0.f; + float input_offset = 0.f; + float advanced_input_offset = 0.f; + float lock_offset = 0.f; + + ImVec2 text_size; + + // maximal size of face name image + Vec2i face_name_size = Vec2i(0, 0); + float face_name_texture_offset_x = 0.f; + + // maximal texture generate jobs running at once + unsigned int max_count_opened_font_files = 10; + + // Only translations needed for calc GUI size + struct Translations + { + std::string font; + std::string height; + std::string depth; + + // advanced + std::string use_surface; + std::string per_glyph; + std::string alignment; + std::string char_gap; + std::string line_gap; + std::string boldness; + std::string skew_ration; + std::string from_surface; + std::string rotation; + std::string keep_up; + std::string collection; + }; + Translations translations; +}; +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 +struct GLGizmoEmboss::Facenames: public ::Facenames{}; +struct GLGizmoEmboss::GuiCfg: public ::GuiCfg{}; + +GLGizmoEmboss::GLGizmoEmboss(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -2) + , m_gui_cfg(nullptr) + , m_style_manager(m_imgui->get_glyph_ranges(), create_default_styles) + , m_face_names(std::make_unique()) + , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) +{ + m_rotate_gizmo.set_group_id(0); + m_rotate_gizmo.set_force_local_coordinate(true); + // to use https://fontawesome.com/ (copy & paste) unicode symbols from web + // paste HEX unicode into notepad move cursor after unicode press [alt] + [x] } -void GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d& mouse_pos) +bool GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d& mouse_pos) { if (!init_create(volume_type)) - return; + return false; - const GLVolume *gl_volume = get_first_hovered_gl_volume(m_parent); - DataBase emboss_data = priv::create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); - bool is_simple_mode = wxGetApp().get_mode() == comSimple; - if (gl_volume != nullptr && !is_simple_mode) { - // Try to cast ray into scene and find object for add volume - if (!priv::start_create_volume_on_surface_job(emboss_data, volume_type, mouse_pos, gl_volume, m_raycast_manager, m_text_lines, m_style_manager, m_parent)) { - // When model is broken. It could appear that hit miss the object. - // So add part near by in simmilar manner as right panel do - create_volume(volume_type); - } - } else { - // object is not under mouse position soo create object on plater - priv::start_create_object_job(emboss_data, mouse_pos); - } + // NOTE: change style manager - be carefull with order changes + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); + CreateVolumeParams input = create_input(m_parent, m_style_manager.get_style(), m_raycast_manager, volume_type); + return start_create_volume(input, std::move(base), mouse_pos); } // Designed for create volume without information of mouse in scene -void GLGizmoEmboss::create_volume(ModelVolumeType volume_type) +bool GLGizmoEmboss::create_volume(ModelVolumeType volume_type) { if (!init_create(volume_type)) - return; + return false; - // select position by camera position and view direction - const Selection &selection = m_parent.get_selection(); - int object_idx = selection.get_object_idx(); - - Size s = m_parent.get_canvas_size(); - Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); - DataBase emboss_data = priv::create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); - const ModelObjectPtrs &objects = selection.get_model()->objects; - bool is_simple_mode = wxGetApp().get_mode() == comSimple; - // No selected object so create new object - if (selection.is_empty() || object_idx < 0 || static_cast(object_idx) >= objects.size() || is_simple_mode) { - // create Object on center of screen - // when ray throw center of screen not hit bed it create object on center of bed - priv::start_create_object_job(emboss_data, screen_center); - return; - } - - // create volume inside of selected object - Vec2d coor; - const GLVolume *vol = nullptr; - const Camera &camera = wxGetApp().plater()->get_camera(); - priv::find_closest_volume(selection, screen_center, camera, objects, &coor, &vol); - if (vol == nullptr) { - priv::start_create_object_job(emboss_data, screen_center); - } else if (!priv::start_create_volume_on_surface_job(emboss_data, volume_type, coor, vol, m_raycast_manager, m_text_lines, m_style_manager, m_parent)) { - // in centroid of convex hull is not hit with object - // soo create transfomation on border of object - - // there is no point on surface so no use of surface will be applied - FontProp &prop = emboss_data.text_configuration.style.prop; - if (prop.use_surface) - prop.use_surface = false; - - // Transformation is inspired add generic volumes in ObjectList::load_generic_subobject - const ModelObject *obj = objects[vol->object_idx()]; - BoundingBoxf3 instance_bb = obj->instance_bounding_box(vol->instance_idx()); - // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. - Transform3d tr = vol->get_instance_transformation().get_matrix_no_offset().inverse(); - Vec3d offset_tr(0, // center of instance - Can't suggest width of text before it will be created - - instance_bb.size().y() / 2 - prop.size_in_mm / 2, // under - prop.emboss / 2 - instance_bb.size().z() / 2 // lay on bed - ); - Transform3d volume_trmat = tr * Eigen::Translation3d(offset_tr); - if (prop.per_glyph) { - init_new_text_line(m_text_lines, volume_trmat, *obj, m_style_manager); - emboss_data.text_lines = m_text_lines.get_lines(); - } - priv::start_create_volume_job(obj, volume_trmat, emboss_data, volume_type); - } + // NOTE: change style manager - be carefull with order changes + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); + CreateVolumeParams input = create_input(m_parent, m_style_manager.get_style(), m_raycast_manager, volume_type); + return start_create_volume_without_position(input, std::move(base)); } + void GLGizmoEmboss::on_shortcut_key() { set_volume_by_selection(); if (m_volume == nullptr) { @@ -357,14 +344,40 @@ void GLGizmoEmboss::on_shortcut_key() { // NOTE: After finish job for creation emboss Text volume, // GLGizmoEmboss will be opened create_volume(ModelVolumeType::MODEL_PART); - } else { + } + else { // shortcut is pressed when text is selected soo start edit it. - auto &mng = m_parent.get_gizmos_manager(); + auto& mng = m_parent.get_gizmos_manager(); if (mng.get_current_type() != GLGizmosManager::Emboss) mng.open_gizmo(GLGizmosManager::Emboss); } } +bool GLGizmoEmboss::re_emboss(const ModelVolume &text_volume, std::shared_ptr> job_cancel) +{ + assert(text_volume.text_configuration.has_value()); + assert(text_volume.emboss_shape.has_value()); + if (!text_volume.text_configuration.has_value() || + !text_volume.emboss_shape.has_value()) + return false; // not valid text volume to re emboss + const TextConfiguration &tc = *text_volume.text_configuration; + const EmbossShape &es = *text_volume.emboss_shape; + const ImWchar* ranges = ImGui::GetIO().Fonts->GetGlyphRangesDefault(); + + StyleManager style_manager(ranges, create_default_styles); + StyleManager::Style style{tc.style, es.projection}; + if (!style_manager.load_style(style)) + return false; // can't load font + + TextLinesModel text_lines; + const Selection &selection = wxGetApp().plater()->canvas3D()->get_selection(); + DataBasePtr base = create_emboss_data_base(tc.text, style_manager, text_lines, selection, text_volume.type(), job_cancel); + DataUpdate data{std::move(base), text_volume.id()}; + + RaycastManager raycast_manager; // Nothing is cached now, so It need to create raycasters + return start_update_volume(std::move(data), text_volume, selection, raycast_manager); +} + namespace{ ModelVolumePtrs prepare_volumes_to_slice(const ModelVolume &mv) { @@ -384,7 +397,13 @@ ModelVolumePtrs prepare_volumes_to_slice(const ModelVolume &mv) } return result; } + +TransformationType get_transformation_type(const Selection &selection) +{ + assert(selection.is_single_full_object() || selection.is_single_volume()); + return selection.is_single_volume() ? TransformationType::Local_Relative_Joint : TransformationType::Instance_Relative_Joint; // object } +} // namespace bool GLGizmoEmboss::do_mirror(size_t axis) { @@ -398,30 +417,47 @@ bool GLGizmoEmboss::do_mirror(size_t axis) if (m_parent.get_gizmos_manager().get_current_type() != GLGizmosManager::Emboss) return false; - const TextConfiguration &tc= *m_volume->text_configuration; - if(tc.style.prop.per_glyph){ - // init textlines before mirroring on mirrored text volume transformation - Transform3d tr = m_volume->get_matrix(); - const std::optional &fix_tr = tc.fix_3mf_tr; + const std::optional &tc = m_volume->text_configuration; + assert(tc.has_value()); + bool is_per_glyph = tc.has_value()? tc->style.prop.per_glyph : false; + + const std::optional &es = m_volume->emboss_shape; + assert(es.has_value()); + bool use_surface = es.has_value()? es->projection.use_surface : false; + if (!use_surface && !is_per_glyph) { + // do normal mirror with fix matrix + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_mirror_fnc = [&selection, &axis]() { selection.mirror((Axis) axis, get_transformation_type(selection)); }; + selection_transform(selection, selection_mirror_fnc, m_volume); + + m_parent.do_mirror(L("Set Mirror")); + wxGetApp().obj_manipul()->UpdateAndShow(true); + return true; + } + + Vec3d scale = Vec3d::Ones(); + scale[axis] = -1.; + + Transform3d tr = m_volume->get_matrix(); + if (es.has_value()) { + const std::optional &fix_tr = es->fix_3mf_tr; if (fix_tr.has_value()) tr = tr * (fix_tr->inverse()); + } - // mirror - Vec3d scale = Vec3d::Ones(); - scale[axis] = -1.; - tr = tr * Eigen::Scaling(scale); + // mirror + tr = tr * Eigen::Scaling(scale); - // collect volumes in object + if (is_per_glyph) { + // init textlines before mirroring on mirrored text volume transformation ModelVolumePtrs volumes = prepare_volumes_to_slice(*m_volume); m_text_lines.init(tr, volumes, m_style_manager, m_text_lines.get_lines().size()); } - // mirror - Transform3d tr = m_volume->get_matrix(); - Vec3d scale = Vec3d::Ones(); - scale[axis] = -1.; - tr = tr * Eigen::Scaling(scale); m_volume->set_transformation(tr); + // setting to volume is not visible for user(not GLVolume) // NOTE: Staff around volume transformation change is done in job finish return process(); } @@ -447,12 +483,12 @@ bool GLGizmoEmboss::init_create(ModelVolumeType volume_type) BOOST_LOG_TRIVIAL(error) << "Can't create text. Gizmo is not activabled."; return false; } - + // Check can't be inside is_activable() cause crash // steps to reproduce: start App -> key 't' -> key 'delete' if (wxGetApp().obj_list()->has_selected_cut_object()) { BOOST_LOG_TRIVIAL(error) << "Can't create text on cut object"; - return false; + return false; } m_style_manager.discard_style_changes(); @@ -462,16 +498,6 @@ bool GLGizmoEmboss::init_create(ModelVolumeType volume_type) return true; } -namespace { -TransformationType get_transformation_type(const Selection &selection) -{ - assert(selection.is_single_full_object() || selection.is_single_volume()); - return selection.is_single_volume() ? - TransformationType::Local_Relative_Joint : - TransformationType::Instance_Relative_Joint; // object -} -} // namespace - bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) { if (mouse_event.Moving()) return false; @@ -482,9 +508,9 @@ bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) if (mouse_event.Dragging()) { if (!m_rotate_start_angle.has_value()) { // when m_rotate_start_angle is not set mean it is not Dragging - // when angle_opt is not set mean angle is Zero - const std::optional &angle_opt = m_style_manager.get_font_prop().angle; - m_rotate_start_angle = angle_opt.has_value() ? *angle_opt : 0.f; + // when angle_opt is not set than angle is Zero + const std::optional &angle_opt = m_style_manager.get_style().angle; + m_rotate_start_angle = angle_opt.value_or(0.f); } double angle = m_rotate_gizmo.get_angle(); @@ -496,7 +522,7 @@ bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) angle += *m_rotate_start_angle; // move to range <-M_PI, M_PI> - priv::to_range_pi_pi(angle); + Geometry::to_range_pi_pi(angle); // set into activ style assert(m_style_manager.is_active_font()); @@ -504,7 +530,7 @@ bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) std::optional angle_opt; if (!is_approx(angle, 0.)) angle_opt = angle; - m_style_manager.get_font_prop().angle = angle_opt; + m_style_manager.get_style().angle = angle_opt; } volume_transformation_changing(); @@ -518,8 +544,9 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) if (m_volume == nullptr) return false; - std::optional up_limit; - if (m_keep_up) up_limit = priv::up_limit; + std::optional wanted_up_limit; + if (m_keep_up) + wanted_up_limit = up_limit; const Camera &camera = wxGetApp().plater()->get_camera(); bool was_dragging = m_surface_drag.has_value(); bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, up_limit); @@ -548,8 +575,7 @@ bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) assert(m_style_manager.is_active_font()); if (gl_volume == nullptr || !m_style_manager.is_active_font()) return res; - - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); + m_style_manager.get_style().angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); } volume_transformation_changing(); @@ -666,23 +692,34 @@ void GLGizmoEmboss::volume_transformation_changing() void GLGizmoEmboss::volume_transformation_changed() { - if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + if (m_volume == nullptr || + !m_volume->text_configuration.has_value() || + !m_volume->emboss_shape.has_value()) { assert(false); return; } + const TextConfiguration &tc = *m_volume->text_configuration; + const EmbossShape &es = *m_volume->emboss_shape; - const FontProp &prop = m_volume->text_configuration->style.prop; - if (prop.per_glyph) + bool per_glyph = tc.style.prop.per_glyph; + if (per_glyph) init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, m_text_lines.get_lines().size()); + bool use_surface = es.projection.use_surface; + // Update surface by new position - if (prop.use_surface || prop.per_glyph) + if (use_surface || 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"); } +std::string GLGizmoEmboss::get_action_snapshot_name() const { return _u8L("Embossing actions"); } + bool GLGizmoEmboss::on_init() { m_rotate_gizmo.init(); @@ -704,10 +741,10 @@ std::string GLGizmoEmboss::on_get_name() const { return _u8L("Emboss"); } void GLGizmoEmboss::on_render() { // no volume selected - if (m_volume == nullptr || - get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr) - return; const Selection &selection = m_parent.get_selection(); + if (m_volume == nullptr || + get_model_volume(m_volume_id, selection.get_model()->objects) == nullptr) + return; if (selection.is_empty()) return; // prevent get local coordinate system on multi volumes @@ -719,7 +756,7 @@ void GLGizmoEmboss::on_render() { if (m_text_lines.is_init()) { const Transform3d& tr = gl_volume_ptr->world_matrix(); - const auto &fix = m_volume->text_configuration->fix_3mf_tr; + const auto &fix = m_volume->emboss_shape->fix_3mf_tr; if (fix.has_value()) m_text_lines.render(tr * fix->inverse()); else @@ -820,15 +857,17 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) // Configuration creation double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); float main_toolbar_height = m_parent.get_main_toolbar_height(); - if (!m_gui_cfg.has_value() || // Exist configuration - first run + if (m_gui_cfg == nullptr || // Exist configuration - first run m_gui_cfg->screen_scale != screen_scale || // change of DPI m_gui_cfg->main_toolbar_height != main_toolbar_height // change size of view port ) { // Create cache for gui offsets - GuiCfg cfg = create_gui_configuration(); + ::GuiCfg cfg = create_gui_configuration(); cfg.screen_scale = screen_scale; cfg.main_toolbar_height = main_toolbar_height; - m_gui_cfg.emplace(std::move(cfg)); + + GuiCfg gui_cfg{std::move(cfg)}; + m_gui_cfg = std::make_unique(std::move(gui_cfg)); // set position near toolbar m_set_window_offset = ImVec2(-1.f, -1.f); @@ -882,7 +921,7 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) bool is_opened = true; ImGuiWindowFlags flag = ImGuiWindowFlags_NoCollapse; - if (ImGui::Begin(on_get_name().c_str(), &is_opened, flag)) { + if (ImGui::Begin(get_name().c_str(), &is_opened, flag)) { // Need to pop var before draw window ImGui::PopStyleVar(); // WindowMinSize draw_window(); @@ -902,21 +941,6 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) close(); } -namespace priv { -/// -/// Move window for edit emboss text near to embossed object -/// NOTE: embossed object must be selected -/// -static ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size); - -/// -/// Change position of emboss window -/// -/// -/// When True Only move to be full visible otherwise reset position -static void change_window_position(std::optional &output_window_offset, bool try_to_fix); -} // namespace priv - void GLGizmoEmboss::on_set_state() { // enable / disable bed from picking @@ -943,17 +967,11 @@ void GLGizmoEmboss::on_set_state() // change position of just opened emboss window if (m_allow_open_near_volume) { - m_set_window_offset = priv::calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); - } else { - if (m_gui_cfg.has_value()) - priv::change_window_position(m_set_window_offset, false); - else - m_set_window_offset = ImVec2(-1, -1); + m_set_window_offset = calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); + } else { + m_set_window_offset = (m_gui_cfg != nullptr) ? + ImGuiWrapper::change_window_position(on_get_name().c_str(), false) : ImVec2(-1, -1); } - - // when open by hyperlink it needs to show up - // or after key 'T' windows doesn't appear - m_parent.set_as_dirty(); } } @@ -968,7 +986,6 @@ void GLGizmoEmboss::on_stop_dragging() { m_rotate_gizmo.stop_dragging(); - // TODO: when start second rotatiton previous rotation rotate draggers // This is fast fix for second try to rotate // When fixing, move grabber above text (not on side) m_rotate_gizmo.set_angle(PI/2); @@ -981,7 +998,7 @@ void GLGizmoEmboss::on_stop_dragging() assert(m_style_manager.is_active_font()); assert(gl_volume != nullptr); if (m_style_manager.is_active_font() && gl_volume != nullptr) - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); + m_style_manager.get_style().angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); m_rotate_start_angle.reset(); @@ -989,107 +1006,6 @@ void GLGizmoEmboss::on_stop_dragging() } void GLGizmoEmboss::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } -GLGizmoEmboss::GuiCfg GLGizmoEmboss::create_gui_configuration() -{ - GuiCfg cfg; // initialize by default values; - - float line_height = ImGui::GetTextLineHeight(); - float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); - float space = line_height_with_spacing - line_height; - const ImGuiStyle &style = ImGui::GetStyle(); - - cfg.max_style_name_width = ImGui::CalcTextSize("Maximal font name, extended").x; - - cfg.icon_width = static_cast(std::ceil(line_height)); - // make size pair number - if (cfg.icon_width % 2 != 0) ++cfg.icon_width; - - cfg.delete_pos_x = cfg.max_style_name_width + space; - int count_line_of_text = 3; - cfg.text_size = ImVec2(-FLT_MIN, line_height_with_spacing * count_line_of_text); - ImVec2 letter_m_size = ImGui::CalcTextSize("M"); - int count_letter_M_in_input = 12; - cfg.input_width = letter_m_size.x * count_letter_M_in_input; - GuiCfg::Translations &tr = cfg.translations; - - tr.font = _u8L("Font"); - tr.height = _u8L("Height"); - tr.depth = _u8L("Depth"); - - float max_text_width = std::max({ - ImGui::CalcTextSize(tr.font.c_str()).x, - ImGui::CalcTextSize(tr.height.c_str()).x, - ImGui::CalcTextSize(tr.depth.c_str()).x}); - cfg.indent = static_cast(cfg.icon_width); - cfg.input_offset = style.WindowPadding.x + cfg.indent + max_text_width + space; - - tr.use_surface = _u8L("Use surface"); - tr.per_glyph = _u8L("Per glyph orientation"); - tr.alignment = _u8L("Alignment"); - tr.char_gap = _u8L("Char gap"); - tr.line_gap = _u8L("Line gap"); - tr.boldness = _u8L("Boldness"); - tr.skew_ration = _u8L("Skew ratio"); - tr.from_surface = _u8L("From surface"); - tr.rotation = _u8L("Rotation"); - tr.collection = _u8L("Collection"); - - float max_advanced_text_width = std::max({ - ImGui::CalcTextSize(tr.use_surface.c_str()).x, - ImGui::CalcTextSize(tr.per_glyph.c_str()).x, - ImGui::CalcTextSize(tr.alignment.c_str()).x, - ImGui::CalcTextSize(tr.char_gap.c_str()).x, - ImGui::CalcTextSize(tr.line_gap.c_str()).x, - ImGui::CalcTextSize(tr.boldness.c_str()).x, - ImGui::CalcTextSize(tr.skew_ration.c_str()).x, - ImGui::CalcTextSize(tr.from_surface.c_str()).x, - ImGui::CalcTextSize(tr.rotation.c_str()).x + cfg.icon_width + 2*space, - ImGui::CalcTextSize(tr.collection.c_str()).x }); - cfg.advanced_input_offset = max_advanced_text_width - + 3 * space + cfg.indent; - cfg.lock_offset = cfg.advanced_input_offset - (cfg.icon_width + space); - // calculate window size - float window_title = line_height + 2*style.FramePadding.y + 2 * style.WindowTitleAlign.y; - float input_height = line_height_with_spacing + 2*style.FramePadding.y; - float tree_header = line_height_with_spacing; - float separator_height = 1 + style.FramePadding.y; - - // "Text is to object" + radio buttons - cfg.height_of_volume_type_selector = separator_height + line_height_with_spacing + input_height; - - float window_height = - window_title + // window title - cfg.text_size.y + // text field - input_height * 4 + // font name + height + depth + style selector - tree_header + // advance tree - separator_height + // presets separator line - line_height_with_spacing + // "Presets" - 2 * style.WindowPadding.y; - float window_width = cfg.input_offset + cfg.input_width + 2*style.WindowPadding.x - + 2 * (cfg.icon_width + space); - cfg.minimal_window_size = ImVec2(window_width, window_height); - - // 8 = useSurface, per glyph, charGap, lineGap, bold, italic, surfDist, rotation, textFaceToCamera - // 4 = 1px for fix each edit image of drag float - float advance_height = input_height * 10 + 9; - cfg.minimal_window_size_with_advance = - ImVec2(cfg.minimal_window_size.x, - cfg.minimal_window_size.y + advance_height); - - cfg.minimal_window_size_with_collections = - ImVec2(cfg.minimal_window_size_with_advance.x, - cfg.minimal_window_size_with_advance.y + input_height); - - int max_style_image_width = cfg.max_style_name_width /2 - - 2 * style.FramePadding.x; - int max_style_image_height = 1.5 * input_height; - cfg.max_style_image_size = Vec2i(max_style_image_width, max_style_image_height); - cfg.face_name_size.y() = line_height_with_spacing; - cfg.face_name_size.x() = cfg.input_width; - cfg.face_name_texture_offset_x = cfg.input_width + space; - return cfg; -} - EmbossStyles GLGizmoEmboss::create_default_styles() { wxFontEnumerator::InvalidateCache(); @@ -1159,7 +1075,53 @@ EmbossStyles GLGizmoEmboss::create_default_styles() return styles; } -namespace{ +namespace { + +/// +/// Check installed fonts whether optional face name exist in installed fonts +/// +/// Name from style - style.prop.face_name +/// All installed good and bad fonts - not const must be possible to initialize it +/// When it could be installed it contain value(potentionaly empty string) +std::optional get_installed_face_name(const std::optional &face_name_opt, ::Facenames& face_names) +{ + // Could exist OS without getter on face_name, + // but it is able to restore font from descriptor + // Soo default value must be TRUE + if (!face_name_opt.has_value()) + return wxString(); + + wxString face_name(face_name_opt->c_str()); + + // search in enumerated fonts + // refresh list of installed font in the OS. + init_face_names(face_names); + face_names.is_init = false; + + auto cmp = [](const FaceName &fn, const wxString &wx_name) { return fn.wx_name < wx_name; }; + const std::vector &faces = face_names.faces; + // is font installed? + if (auto it = std::lower_bound(faces.begin(), faces.end(), face_name, cmp); + it != faces.end() && it->wx_name == face_name) + return face_name; + + const std::vector &bad = face_names.bad; + auto it_bad = std::lower_bound(bad.begin(), bad.end(), face_name); + if (it_bad == bad.end() || *it_bad != face_name) { + // check if wx allowed to set it up - another encoding of name + wxFontEnumerator::InvalidateCache(); + wxFont wx_font_; // temporary structure + if (wx_font_.SetFaceName(face_name) && WxFontUtils::create_font_file(wx_font_) != nullptr // can load TTF file? + ) { + return wxString(); + // QUESTION: add this name to allowed faces? + // Could create twin of font face name + // When not add it will be hard to select it again when change font + } + } + return {}; // not installed +} + 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(); @@ -1174,6 +1136,11 @@ void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* if (mv.is_the_only_one_part()) return; + const std::optional &es_opt = mv.emboss_shape; + if (!es_opt.has_value()) + return; + const EmbossShape &es = *es_opt; + const std::optional &tc_opt = mv.text_configuration; if (!tc_opt.has_value()) return; @@ -1191,26 +1158,10 @@ void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* // 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()); + if (es.fix_3mf_tr.has_value()) + mv_trafo = mv_trafo * (es.fix_3mf_tr->inverse()); text_lines.init(mv_trafo, volumes, style_manager, 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); - } - unsigned count_lines = 1; - text_lines.init(new_text_tr, volumes, style_manager, count_lines); -} - } void GLGizmoEmboss::reinit_text_lines(unsigned count_lines) { @@ -1220,7 +1171,7 @@ void GLGizmoEmboss::reinit_text_lines(unsigned count_lines) { void GLGizmoEmboss::set_volume_by_selection() { const Selection &selection = m_parent.get_selection(); - const GLVolume *gl_volume = get_selected_gl_volume(selection); + const GLVolume *gl_volume = get_selected_gl_volume(selection); if (gl_volume == nullptr) return reset_volume(); @@ -1239,60 +1190,28 @@ void GLGizmoEmboss::set_volume_by_selection() remove_notification_not_valid_font(); // Do not use focused input value when switch volume(it must swith value) - if (m_volume != nullptr && - m_volume != volume) // when update volume it changed id BUT not pointer + if (m_volume != nullptr && m_volume != volume) // when update volume it changed id BUT not pointer ImGuiWrapper::left_inputs(); // Is selected volume text volume? - const std::optional& tc_opt = volume->text_configuration; - if (!tc_opt.has_value()) + const std::optional &tc_opt = volume->text_configuration; + if (!tc_opt.has_value()) return reset_volume(); + // Emboss shape must be setted + assert(volume->emboss_shape.has_value()); + if (!volume->emboss_shape.has_value()) + return; + const TextConfiguration &tc = *tc_opt; const EmbossStyle &style = tc.style; - // Could exist OS without getter on face_name, - // but it is able to restore font from descriptor - // Soo default value must be TRUE - bool is_font_installed = true; - wxString face_name; - std::optional face_name_opt = style.prop.face_name; - if (face_name_opt.has_value()) { - face_name = wxString(face_name_opt->c_str()); - - // search in enumerated fonts - // refresh list of installed font in the OS. - init_face_names(m_face_names); - m_face_names.is_init = false; - - auto cmp = [](const FaceName &fn, const wxString& face_name)->bool { return fn.wx_name < face_name; }; - const std::vector &faces = m_face_names.faces; - auto it = std::lower_bound(faces.begin(), faces.end(), face_name, cmp); - is_font_installed = it != faces.end() && it->wx_name == face_name; - - if (!is_font_installed) { - const std::vector &bad = m_face_names.bad; - auto it_bad = std::lower_bound(bad.begin(), bad.end(), face_name); - if (it_bad == bad.end() || *it_bad != face_name){ - // check if wx allowed to set it up - another encoding of name - wxFontEnumerator::InvalidateCache(); - wxFont wx_font_; // temporary structure - if (wx_font_.SetFaceName(face_name) && - WxFontUtils::create_font_file(wx_font_) != nullptr // can load TTF file? - ) { - is_font_installed = true; - // QUESTION: add this name to allowed faces? - // Could create twin of font face name - // When not add it will be hard to select it again when change font - } - } - } - } + std::optional installed_name = get_installed_face_name(style.prop.face_name, *m_face_names); wxFont wx_font; // load wxFont from same OS when font name is installed - if (style.type == WxFontUtils::get_actual_type() && is_font_installed) - wx_font = WxFontUtils::load_wxFont(style.path); + if (style.type == WxFontUtils::get_current_type() && installed_name.has_value()) + wx_font = WxFontUtils::load_wxFont(style.path); // Flag that is selected same font bool is_exact_font = true; @@ -1301,8 +1220,8 @@ void GLGizmoEmboss::set_volume_by_selection() is_exact_font = false; // Try create similar wx font by FontFamily wx_font = WxFontUtils::create_wxFont(style); - if (is_font_installed) - is_exact_font = wx_font.SetFaceName(face_name); + if (installed_name.has_value() && !installed_name->empty()) + is_exact_font = wx_font.SetFaceName(*installed_name); // Have to use some wxFont if (!wx_font.IsOk()) @@ -1311,25 +1230,28 @@ void GLGizmoEmboss::set_volume_by_selection() assert(wx_font.IsOk()); // Load style to style manager - const auto& styles = m_style_manager.get_styles(); - auto has_same_name = [&style](const StyleManager::Item &style_item) -> bool { - const EmbossStyle &es = style_item.style; - return es.name == style.name; - }; - auto it = std::find_if(styles.begin(), styles.end(), has_same_name); - if (it == styles.end()) { + const auto &styles = m_style_manager.get_styles(); + auto has_same_name = [&name = style.name](const StyleManager::Style &style_item) { return style_item.name == name; }; + + StyleManager::Style style_{style}; // copy + style_.projection = volume->emboss_shape->projection; + style_.angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); + style_.distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + if (auto it = std::find_if(styles.begin(), styles.end(), has_same_name); + it == styles.end()) { // style was not found - m_style_manager.load_style(style, wx_font); + m_style_manager.load_style(style_, wx_font); } else { // style name is in styles list size_t style_index = it - styles.begin(); if (!m_style_manager.load_style(style_index)) { // can`t load stored style m_style_manager.erase(style_index); - m_style_manager.load_style(style, wx_font); + m_style_manager.load_style(style_, wx_font); } else { // stored style is loaded, now set modification of style - m_style_manager.get_style() = style; + m_style_manager.get_style() = style_; m_style_manager.set_wx_font(wx_font); } } @@ -1361,7 +1283,7 @@ void GLGizmoEmboss::set_volume_by_selection() // 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); + m_style_manager.get_style().angle = calc_up(gl_volume->world_matrix(), up_limit); // calculate scale for height and depth inside of scaled object instance calculate_scale(); @@ -1404,32 +1326,9 @@ void GLGizmoEmboss::calculate_scale() { m_style_manager.clear_imgui_font(); } -#ifdef EXECUTE_PROCESS_ON_MAIN_THREAD -namespace priv { -// Run Job on main thread (blocking) - ONLY DEBUG -static inline void execute_job(std::shared_ptr j) -{ - struct MyCtl : public Job::Ctl - { - void update_status(int st, const std::string &msg = "") override{}; - bool was_canceled() const override { return false; } - std::future call_on_main_thread(std::function fn) override - { - return std::future{}; - } - } ctl; - j->process(ctl); - wxGetApp().plater()->CallAfter([j]() { - std::exception_ptr e_ptr = nullptr; - j->finalize(false, e_ptr); - }); -} -} // namespace priv -#endif - -namespace priv { -static bool is_text_empty(const std::string &text) { return text.empty() || text.find_first_not_of(" \n\t\r") == std::string::npos; } -} // namespace priv +namespace { +bool is_text_empty(std::string_view text) { return text.empty() || text.find_first_not_of(" \n\t\r") == std::string::npos; } +} // namespace bool GLGizmoEmboss::process() { @@ -1438,61 +1337,20 @@ bool GLGizmoEmboss::process() if (m_volume == nullptr) return false; // without text there is nothing to emboss - if (priv::is_text_empty(m_text)) return false; + if (is_text_empty(m_text)) return false; // exist loaded font file? if (!m_style_manager.is_active_font()) return false; - - 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; - // 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()); + const Selection& selection = m_parent.get_selection(); + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, selection, m_volume->type(), m_job_cancel); + DataUpdate data{std::move(base), m_volume->id()}; - if (use_surface) { - // Model to cut surface from. - SurfaceVolumeData::ModelSources sources = create_volume_sources(m_volume); - if (sources.empty()) - return false; + // check valid count of text lines + assert(data.base->text_lines.empty() || data.base->text_lines.size() == get_count_lines(m_text)); - 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 + if (!start_update_volume(std::move(data), *m_volume, selection, m_raycast_manager)) + return false; // notification is removed befor object is changed by job remove_notification_not_valid_font(); @@ -1504,7 +1362,7 @@ void GLGizmoEmboss::close() // remove volume when text is empty if (m_volume != nullptr && m_volume->text_configuration.has_value() && - priv::is_text_empty(m_text)) { + is_text_empty(m_text)) { Plater &p = *wxGetApp().plater(); // is the text object? if (m_volume->is_the_only_one_part()) { @@ -1526,7 +1384,6 @@ void GLGizmoEmboss::draw_window() { #ifdef ALLOW_DEBUG_MODE if (ImGui::Button("re-process")) process(); - if (ImGui::Button("add svg")) choose_svg_file(); #endif // ALLOW_DEBUG_MODE // Setter of indent must be befor disable !!! @@ -1612,7 +1469,7 @@ void GLGizmoEmboss::draw_window() ImGui::SameLine(); if (ImGui::Checkbox("##ALLOW_OPEN_NEAR_VOLUME", &m_allow_open_near_volume)) { if (m_allow_open_near_volume) - m_set_window_offset = priv::calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); + m_set_window_offset = calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", ((m_allow_open_near_volume) ? "Fix settings position": @@ -1661,9 +1518,10 @@ void GLGizmoEmboss::draw_text_input() warning_tool_tip += "\n"; warning_tool_tip += t; }; - - if (priv::is_text_empty(m_text)) append_warning(_u8L("Embossed text cannot contain only white spaces.")); - if (m_text_contain_unknown_glyph) append_warning(_u8L("Text contains character glyph (represented by '?') unknown by font.")); + if (is_text_empty(m_text)) + append_warning(_u8L("Embossed text cannot contain only white spaces.")); + if (m_text_contain_unknown_glyph) + append_warning(_u8L("Text contains character glyph (represented by '?') unknown by font.")); const FontProp &prop = m_style_manager.get_font_prop(); if (prop.skew.has_value()) append_warning(_u8L("Text input doesn't show font skew.")); @@ -1704,14 +1562,12 @@ void GLGizmoEmboss::draw_text_input() // warning tooltip has to be with default font if (!warning_tool_tip.empty()) { // Multiline input has hidden window for scrolling - ImGuiWindow *input = ImGui::GetCurrentWindow()->DC.ChildWindows.front(); - + const ImGuiWindow *input = ImGui::GetCurrentWindow()->DC.ChildWindows.front(); const ImGuiStyle &style = ImGui::GetStyle(); float scrollbar_width = (input->ScrollbarY) ? style.ScrollbarSize : 0.f; float scrollbar_height = (input->ScrollbarX) ? style.ScrollbarSize : 0.f; - bool hovered = ImGui::IsItemHovered(); - if (hovered) + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", warning_tool_tip.c_str()); ImVec2 cursor = ImGui::GetCursorPos(); @@ -1730,216 +1586,15 @@ void GLGizmoEmboss::draw_text_input() // IMPROVE: only extend not clear // Extend font ranges if (!range_text.empty() && - !m_imgui->contain_all_glyphs(imgui_font, range_text) ) + !ImGuiWrapper::contain_all_glyphs(imgui_font, range_text) ) m_style_manager.clear_imgui_font(); } -#include -#include "wx/hashmap.h" -std::size_t hash_value(wxString const &s) -{ - boost::hash hasher; - return hasher(s.ToStdString()); -} - -static std::string concat(std::vector data) { - std::stringstream ss; - for (const auto &d : data) - ss << d.c_str() << ", "; - return ss.str(); -} - -#include -static boost::filesystem::path get_fontlist_cache_path() -{ - return boost::filesystem::path(data_dir()) / "cache" / "fonts.cereal"; -} - -// cache font list by cereal -#include -#include -#include -#include - -// increase number when change struct FacenamesSerializer -#define FACENAMES_VERSION 1 -struct FacenamesSerializer -{ - // hash number for unsorted vector of installed font into system - size_t hash = 0; - // assumption that is loadable - std::vector good; - // Can't load for some reason - std::vector bad; -}; - -template void save(Archive &archive, wxString const &d) -{ std::string s(d.ToUTF8().data()); archive(s);} -template void load(Archive &archive, wxString &d) -{ std::string s; archive(s); d = s;} - -template void serialize(Archive &ar, FacenamesSerializer &t, const std::uint32_t version) -{ - // When performing a load, the version associated with the class - // is whatever it was when that data was originally serialized - // When we save, we'll use the version that is defined in the macro - if (version != FACENAMES_VERSION) return; - ar(t.hash, t.good, t.bad); -} -CEREAL_CLASS_VERSION(FacenamesSerializer, FACENAMES_VERSION); // register class version - -#include -bool GLGizmoEmboss::store(const Facenames &facenames) { - std::string cache_path = get_fontlist_cache_path().string(); - boost::nowide::ofstream file(cache_path, std::ios::binary); - cereal::BinaryOutputArchive archive(file); - std::vector good; - good.reserve(facenames.faces.size()); - for (const FaceName &face : facenames.faces) good.push_back(face.wx_name); - FacenamesSerializer data = {facenames.hash, good, facenames.bad}; - - assert(std::is_sorted(data.bad.begin(), data.bad.end())); - assert(std::is_sorted(data.good.begin(), data.good.end())); - - try { - archive(data); - } catch (const std::exception &ex) { - BOOST_LOG_TRIVIAL(error) << "Failed to write fontlist cache - " << cache_path << ex.what(); - return false; - } - return true; -} - -bool GLGizmoEmboss::load(Facenames &facenames) { - boost::filesystem::path path = get_fontlist_cache_path(); - std::string path_str = path.string(); - if (!boost::filesystem::exists(path)) { - BOOST_LOG_TRIVIAL(warning) << "Fontlist cache - '" << path_str << "' does not exists."; - return false; - } - boost::nowide::ifstream file(path_str, std::ios::binary); - cereal::BinaryInputArchive archive(file); - - FacenamesSerializer data; - try { - archive(data); - } catch (const std::exception &ex) { - BOOST_LOG_TRIVIAL(error) << "Failed to load fontlist cache - '" << path_str << "'. Exception: " << ex.what(); - return false; - } - - assert(std::is_sorted(data.bad.begin(), data.bad.end())); - assert(std::is_sorted(data.good.begin(), data.good.end())); - - facenames.hash = data.hash; - facenames.faces.reserve(data.good.size()); - for (const wxString &face : data.good) - facenames.faces.push_back({face}); - facenames.bad = data.bad; - return true; -} - -void GLGizmoEmboss::init_truncated_names(Facenames &face_names, float max_width) -{ - for (FaceName &face : face_names.faces) { - std::string name_str(face.wx_name.ToUTF8().data()); - face.name_truncated = ImGuiWrapper::trunc(name_str, max_width); - } - face_names.has_truncated_names = true; -} - -void GLGizmoEmboss::init_face_names(Facenames &face_names) -{ - Timer t("enumerate_fonts"); - if (face_names.is_init) return; - face_names.is_init = true; - - // to reload fonts from system, when install new one - wxFontEnumerator::InvalidateCache(); - - // try load cache - // Only not OS enumerated face has hash value 0 - if (face_names.hash == 0) { - load(face_names); - face_names.has_truncated_names = false; - } - - using namespace std::chrono; - steady_clock::time_point enumerate_start = steady_clock::now(); - ScopeGuard sg([&enumerate_start, &face_names = face_names]() { - steady_clock::time_point enumerate_end = steady_clock::now(); - long long enumerate_duration = duration_cast(enumerate_end - enumerate_start).count(); - BOOST_LOG_TRIVIAL(info) << "OS enumerate " << face_names.faces.size() << " fonts " - << "(+ " << face_names.bad.size() << " can't load " - << "= " << face_names.faces.size() + face_names.bad.size() << " fonts) " - << "in " << enumerate_duration << " ms\n" << concat(face_names.bad); - }); - wxArrayString facenames = wxFontEnumerator::GetFacenames(face_names.encoding); - size_t hash = boost::hash_range(facenames.begin(), facenames.end()); - // Zero value is used as uninitialized hash - if (hash == 0) hash = 1; - // check if it is same as last time - if (face_names.hash == hash) { - // no new installed font - BOOST_LOG_TRIVIAL(info) << "Same FontNames hash, cache is used. " - << "For clear cache delete file: " << get_fontlist_cache_path().string(); - return; - } - - BOOST_LOG_TRIVIAL(info) << ((face_names.hash == 0) ? - "FontName list is generate from scratch." : - "Hash are different. Only previous bad fonts are used and set again as bad"); - face_names.hash = hash; - - // validation lambda - auto is_valid_font = [encoding = face_names.encoding, bad = face_names.bad /*copy*/](const wxString &name) { - if (name.empty()) return false; - - // vertical font start with @, we will filter it out - // Not sure if it is only in Windows so filtering is on all platforms - if (name[0] == '@') return false; - - // previously detected bad font - auto it = std::lower_bound(bad.begin(), bad.end(), name); - if (it != bad.end() && *it == name) return false; - - wxFont wx_font(wxFontInfo().FaceName(name).Encoding(encoding)); - //* - // Faster chech if wx_font is loadable but not 100% - // names could contain not loadable font - if (!WxFontUtils::can_load(wx_font)) return false; - - /*/ - // Slow copy of font files to try load font - // After this all files are loadable - auto font_file = WxFontUtils::create_font_file(wx_font); - if (font_file == nullptr) - return false; // can't create font file - // */ - return true; - }; - - face_names.faces.clear(); - face_names.bad.clear(); - face_names.faces.reserve(facenames.size()); - std::sort(facenames.begin(), facenames.end()); - for (const wxString &name : facenames) { - if (is_valid_font(name)) { - face_names.faces.push_back({name}); - }else{ - face_names.bad.push_back(name); - } - } - assert(std::is_sorted(face_names.bad.begin(), face_names.bad.end())); - face_names.has_truncated_names = false; - store(face_names); -} - // create texture for visualization font face void GLGizmoEmboss::init_font_name_texture() { Timer t("init_font_name_texture"); // check if already exists - GLuint &id = m_face_names.texture_id; + GLuint &id = m_face_names->texture_id; if (id != 0) return; // create texture for font GLenum target = GL_TEXTURE_2D; @@ -1948,7 +1603,7 @@ void GLGizmoEmboss::init_font_name_texture() { glsafe(::glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); glsafe(::glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); const Vec2i &size = m_gui_cfg->face_name_size; - GLint w = size.x(), h = m_face_names.count_cached_textures * size.y(); + GLint w = size.x(), h = m_face_names->count_cached_textures * size.y(); std::vector data(4*w * h, {0}); const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; const GLint level = 0, internal_format = GL_RGBA, border = 0; @@ -1959,107 +1614,20 @@ void GLGizmoEmboss::init_font_name_texture() { glsafe(::glBindTexture(target, no_texture_id)); // clear info about creation of texture - no one is initialized yet - for (FaceName &face : m_face_names.faces) { + for (FaceName &face : m_face_names->faces) { face.cancel = nullptr; face.is_created = nullptr; } // Prepare filtration cache - m_face_names.hide = std::vector(m_face_names.faces.size(), {false}); -} - -void GLGizmoEmboss::draw_font_preview(FaceName& face, bool is_visible) -{ - // Limit for opened font files at one moment - unsigned int &count_opened_fonts = m_face_names.count_opened_font_files; - // Size of texture - ImVec2 size(m_gui_cfg->face_name_size.x(), m_gui_cfg->face_name_size.y()); - float count_cached_textures_f = static_cast(m_face_names.count_cached_textures); - std::string state_text; - // uv0 and uv1 set to pixel 0,0 in texture - ImVec2 uv0(0.f, 0.f), uv1(1.f / size.x, 1.f / size.y / count_cached_textures_f); - if (face.is_created != nullptr) { - // not created preview - if (*face.is_created) { - // Already created preview - size_t texture_index = face.texture_index; - uv0 = ImVec2(0.f, texture_index / count_cached_textures_f); - uv1 = ImVec2(1.f, (texture_index + 1) / count_cached_textures_f); - } else { - // Not finished preview - if (is_visible) { - // when not canceled still loading - state_text = std::string(" ") + (face.cancel->load() ? - _u8L("No symbol") : - (dots.ToStdString() + _u8L("Loading"))); - } else { - // not finished and not visible cancel job - face.is_created = nullptr; - face.cancel->store(true); - } - } - } else if (is_visible && count_opened_fonts < m_gui_cfg->max_count_opened_font_files) { - ++count_opened_fonts; - face.cancel = std::make_shared(false); - face.is_created = std::make_shared(false); - - const unsigned char gray_level = 5; - // format type and level must match to texture data - const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; - const GLint level = 0; - // select next texture index - size_t texture_index = (m_face_names.texture_index + 1) % m_face_names.count_cached_textures; - - // set previous cach as deleted - for (FaceName &f : m_face_names.faces) - if (f.texture_index == texture_index) { - if (f.cancel != nullptr) f.cancel->store(true); - f.is_created = nullptr; - } - - m_face_names.texture_index = texture_index; - face.texture_index = texture_index; - - // render text to texture - FontImageData data{ - m_text, - face.wx_name, - m_face_names.encoding, - m_face_names.texture_id, - m_face_names.texture_index, - m_gui_cfg->face_name_size, - gray_level, - format, - type, - level, - &count_opened_fonts, - face.cancel, // copy - face.is_created // copy - }; - auto job = std::make_unique(std::move(data)); - auto &worker = wxGetApp().plater()->get_ui_job_worker(); - queue_job(worker, std::move(job)); - } else { - // cant start new thread at this moment so wait in queue - state_text = " " + dots.ToStdString() + " " + _u8L("Queue"); - } - - if (!state_text.empty()) { - ImGui::SameLine(m_gui_cfg->face_name_texture_offset_x); - m_imgui->text(state_text); - } - - ImGui::SameLine(m_gui_cfg->face_name_texture_offset_x); - ImTextureID tex_id = (void *) (intptr_t) m_face_names.texture_id; - ImGui::Image(tex_id, size, uv0, uv1); + m_face_names->hide = std::vector(m_face_names->faces.size(), {false}); } bool GLGizmoEmboss::select_facename(const wxString &facename) { if (!wxFontEnumerator::IsValidFacename(facename)) return false; // Select font - const wxFontEncoding &encoding = m_face_names.encoding; - wxFont wx_font(wxFontInfo().FaceName(facename).Encoding(encoding)); + wxFont wx_font(wxFontInfo().FaceName(facename).Encoding(Facenames::encoding)); if (!wx_font.IsOk()) return false; #ifdef USE_PIXEL_SIZE_IN_WX_FONT // wx font could change source file by size of font @@ -2155,19 +1723,19 @@ void GLGizmoEmboss::draw_font_list() // Fix clearance of search input, // Sometime happens that search text not disapear after font select - m_face_names.search.clear(); + m_face_names->search.clear(); } - if (ImGui::InputTextWithHint(input_id, selected, &m_face_names.search)) { + if (ImGui::InputTextWithHint(input_id, selected, &m_face_names->search)) { // update filtration result - m_face_names.hide = std::vector(m_face_names.faces.size(), {false}); + m_face_names->hide = std::vector(m_face_names->faces.size(), {false}); // search to uppercase - std::string search = m_face_names.search; // copy + std::string search = m_face_names->search; // copy std::transform(search.begin(), search.end(), search.begin(), ::toupper); - for (FaceName &face : m_face_names.faces) { - size_t index = &face - &m_face_names.faces.front(); + for (const FaceName &face : m_face_names->faces) { + size_t index = &face - &m_face_names->faces.front(); // font name to uppercase std::string name(face.wx_name.ToUTF8().data()); @@ -2175,7 +1743,7 @@ void GLGizmoEmboss::draw_font_list() // It should use C++ 20 feature https://en.cppreference.com/w/cpp/string/basic_string/starts_with bool start_with = boost::starts_with(name, search); - m_face_names.hide[index] = !start_with; + m_face_names->hide[index] = !start_with; } } if (!is_popup_open) @@ -2194,23 +1762,23 @@ void GLGizmoEmboss::draw_font_list() if (ImGui::BeginPopup(popup_id, popup_flags)) { bool set_selection_focus = false; - if (!m_face_names.is_init) { - init_face_names(m_face_names); + if (!m_face_names->is_init) { + init_face_names(*m_face_names); set_selection_focus = true; } - if (!m_face_names.has_truncated_names) - init_truncated_names(m_face_names, m_gui_cfg->input_width); + if (!m_face_names->has_truncated_names) + init_truncated_names(*m_face_names, m_gui_cfg->input_width); - if (m_face_names.texture_id == 0) + if (m_face_names->texture_id == 0) init_font_name_texture(); - for (FaceName &face : m_face_names.faces) { + for (FaceName &face : m_face_names->faces) { const wxString &wx_face_name = face.wx_name; - size_t index = &face - &m_face_names.faces.front(); + size_t index = &face - &m_face_names->faces.front(); // Filter for face names - if (m_face_names.hide[index]) + if (m_face_names->hide[index]) continue; ImGui::PushID(index); @@ -2231,7 +1799,7 @@ void GLGizmoEmboss::draw_font_list() // on first draw set focus on selected font if (set_selection_focus && is_selected) ImGui::SetScrollHereY(); - draw_font_preview(face, ImGui::IsItemVisible()); + ::draw_font_preview(face, m_text, *m_face_names, *m_gui_cfg, ImGui::IsItemVisible()); } if (!ImGui::IsWindowFocused() || @@ -2240,33 +1808,33 @@ void GLGizmoEmboss::draw_font_list() ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); - } else if (m_face_names.is_init) { + } else if (m_face_names->is_init) { // Just one after close combo box // free texture and set id to zero - m_face_names.is_init = false; - m_face_names.hide.clear(); + m_face_names->is_init = false; + m_face_names->hide.clear(); // cancel all process for generation of texture - for (FaceName &face : m_face_names.faces) + for (FaceName &face : m_face_names->faces) if (face.cancel != nullptr) face.cancel->store(true); - glsafe(::glDeleteTextures(1, &m_face_names.texture_id)); - m_face_names.texture_id = 0; + glsafe(::glDeleteTextures(1, &m_face_names->texture_id)); + m_face_names->texture_id = 0; // Remove value from search input ImGuiWrapper::left_inputs(); - m_face_names.search.clear(); + m_face_names->search.clear(); } // delete unloadable face name when try to use if (del_index.has_value()) { - auto face = m_face_names.faces.begin() + (*del_index); - std::vector& bad = m_face_names.bad; + auto face = m_face_names->faces.begin() + (*del_index); + std::vector& bad = m_face_names->bad; // sorted insert into bad fonts auto it = std::upper_bound(bad.begin(), bad.end(), face->wx_name); bad.insert(it, face->wx_name); - m_face_names.faces.erase(face); + m_face_names->faces.erase(face); // update cached file - store(m_face_names); + store(*m_face_names); } #ifdef ALLOW_ADD_FONT_BY_FILE @@ -2345,7 +1913,6 @@ void GLGizmoEmboss::draw_model_type() Plater::TakeSnapshot snapshot(plater, _L("Change Text Type"), UndoRedo::SnapshotType::GizmoAction); m_volume->set_type(*new_type); - // move inside 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) @@ -2374,19 +1941,15 @@ void GLGizmoEmboss::draw_style_rename_popup() { const std::string &old_name = m_style_manager.get_stored_style()->name; std::string text_in_popup = GUI::format(_L("Rename style(%1%) for embossing text"), old_name) + ": "; ImGui::Text("%s", text_in_popup.c_str()); - - bool is_unique = true; - for (const auto &item : m_style_manager.get_styles()) { - const EmbossStyle &style = item.style; - if (&style == &m_style_manager.get_style()) - continue; // could be same as original name - if (style.name == new_name) is_unique = false; - } + + bool is_unique = (new_name == old_name) || // could be same as before rename + m_style_manager.is_unique_style_name(new_name); + bool allow_change = false; if (new_name.empty()) { - m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); }else if (!is_unique) { - m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); } else { ImGui::NewLine(); allow_change = true; @@ -2404,7 +1967,7 @@ void GLGizmoEmboss::draw_style_rename_popup() { if (store) { // rename style in all objects and volumes - for (ModelObject *mo :wxGetApp().plater()->model().objects) { + for (const ModelObject *mo :wxGetApp().plater()->model().objects) { for (ModelVolume *mv : mo->volumes) { if (!mv->text_configuration.has_value()) continue; std::string& name = mv->text_configuration->style.name; @@ -2424,7 +1987,7 @@ void GLGizmoEmboss::draw_style_rename_button() bool can_rename = m_style_manager.exist_stored_style(); std::string title = _u8L("Rename style"); const char * popup_id = title.c_str(); - if (priv::draw_button(m_icons, IconType::rename, !can_rename)) { + if (draw_button(m_icons, IconType::rename, !can_rename)) { assert(m_style_manager.get_stored_style()); ImGui::OpenPopup(popup_id); } @@ -2462,16 +2025,12 @@ void GLGizmoEmboss::draw_style_save_as_popup() { // use name inside of volume configuration as temporary new name std::string &new_name = m_volume->text_configuration->style.name; - - bool is_unique = true; - for (const auto &item : m_style_manager.get_styles()) - if (item.style.name == new_name) is_unique = false; - + bool is_unique = m_style_manager.is_unique_style_name(new_name); bool allow_change = false; if (new_name.empty()) { - m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); }else if (!is_unique) { - m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); } else { ImGui::NewLine(); allow_change = true; @@ -2504,7 +2063,7 @@ void GLGizmoEmboss::draw_style_add_button() bool only_add_style = !m_style_manager.exist_stored_style(); bool can_add = true; if (only_add_style && - m_volume->text_configuration->style.type != WxFontUtils::get_actual_type()) + m_volume->text_configuration->style.type != WxFontUtils::get_current_type()) can_add = false; std::string title = _u8L("Save as new style"); @@ -2527,7 +2086,7 @@ void GLGizmoEmboss::draw_style_add_button() } } - if (ImGui::BeginPopupModal(popup_id, 0, ImGuiWindowFlags_AlwaysAutoResize)) { + if (ImGui::BeginPopupModal(popup_id, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { m_imgui->disable_background_fadeout_animation(); draw_style_save_as_popup(); ImGui::EndPopup(); @@ -2592,46 +2151,46 @@ void GLGizmoEmboss::draw_delete_style_button() { } } -// FIX IT: it should not change volume position before successfull change -void GLGizmoEmboss::fix_transformation(const FontProp &from, - const FontProp &to) -{ +namespace { +// FIX IT: It should not change volume position before successfull change volume by process +void fix_transformation(const StyleManager::Style &from, const StyleManager::Style &to, GLCanvas3D &canvas) { // fix Z rotation when exists difference in styles const std::optional &f_angle_opt = from.angle; const std::optional &t_angle_opt = to.angle; if (!is_approx(f_angle_opt, t_angle_opt)) { // fix rotation - float f_angle = f_angle_opt.has_value() ? *f_angle_opt : .0f; - float t_angle = t_angle_opt.has_value() ? *t_angle_opt : .0f; - do_rotate(t_angle - f_angle); + float f_angle = f_angle_opt.value_or(.0f); + float t_angle = t_angle_opt.value_or(.0f); + do_local_z_rotate(canvas, t_angle - f_angle); } // fix distance (Z move) when exists difference in styles const std::optional &f_move_opt = from.distance; const std::optional &t_move_opt = to.distance; if (!is_approx(f_move_opt, t_move_opt)) { - float f_move = f_move_opt.has_value() ? *f_move_opt : .0f; - float t_move = t_move_opt.has_value() ? *t_move_opt : .0f; - do_translate(Vec3d::UnitZ() * (t_move - f_move)); + float f_move = f_move_opt.value_or(.0f); + float t_move = t_move_opt.value_or(.0f); + do_local_z_move(canvas, t_move - f_move); } } +} // namesapce void GLGizmoEmboss::draw_style_list() { if (!m_style_manager.is_active_font()) return; - const EmbossStyle *stored_style = nullptr; + const StyleManager::Style *stored_style = nullptr; bool is_stored = m_style_manager.exist_stored_style(); if (is_stored) stored_style = m_style_manager.get_stored_style(); - const EmbossStyle &actual_style = m_style_manager.get_style(); - bool is_changed = (stored_style)? !(*stored_style == actual_style) : true; + const StyleManager::Style ¤t_style = m_style_manager.get_style(); + bool is_changed = (stored_style)? !(*stored_style == current_style) : true; bool is_modified = is_stored && is_changed; const float &max_style_name_width = m_gui_cfg->max_style_name_width; std::string &trunc_name = m_style_manager.get_truncated_name(); if (trunc_name.empty()) { // generate trunc name - std::string current_name = actual_style.name; + std::string current_name = current_style.name; ImGuiWrapper::escape_double_hash(current_name); trunc_name = ImGuiWrapper::trunc(current_name, max_style_name_width); } @@ -2652,19 +2211,19 @@ void GLGizmoEmboss::draw_style_list() { m_style_manager.init_style_images(m_gui_cfg->max_style_image_size, m_text); m_style_manager.init_trunc_names(max_style_name_width); std::optional> swap_indexes; - const std::vector &styles = m_style_manager.get_styles(); - for (const auto &item : styles) { - size_t index = &item - &styles.front(); - const EmbossStyle &style = item.style; + const StyleManager::Styles &styles = m_style_manager.get_styles(); + for (const StyleManager::Style &style : styles) { + size_t index = &style - &styles.front(); const std::string &actual_style_name = style.name; ImGui::PushID(actual_style_name.c_str()); bool is_selected = (index == m_style_manager.get_style_index()); - ImVec2 select_size(0,m_gui_cfg->max_style_image_size.y()); // 0,0 --> calculate in draw - const std::optional &img = item.image; + float select_height = static_cast(m_gui_cfg->max_style_image_size.y()); + ImVec2 select_size(0.f, select_height); // 0,0 --> calculate in draw + const std::optional &img = style.image; // allow click delete button ImGuiSelectableFlags_ flags = ImGuiSelectableFlags_AllowItemOverlap; - if (ImGui::Selectable(item.truncated_name.c_str(), is_selected, flags, select_size)) { + if (ImGui::Selectable(style.truncated_name.c_str(), is_selected, flags, select_size)) { selected_style_index = index; } else if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", actual_style_name.c_str()); @@ -2696,18 +2255,18 @@ void GLGizmoEmboss::draw_style_list() { // do not keep in memory style images when no combo box open m_style_manager.free_style_images(); if (ImGui::IsItemHovered()) { - std::string style_name = add_text_modify(actual_style.name); + std::string style_name = add_text_modify(current_style.name); std::string tooltip = is_modified? - GUI::format(_L("Modified style \"%1%\""), actual_style.name): - GUI::format(_L("Current style is \"%1%\""), actual_style.name); + GUI::format(_L("Modified style \"%1%\""), current_style.name): + GUI::format(_L("Current style is \"%1%\""), current_style.name); ImGui::SetTooltip(" %s", tooltip.c_str()); } } // Check whether user wants lose actual style modification if (selected_style_index.has_value() && is_modified) { - const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; - wxString message = GUI::format_wxstr(_L("Changing style to \"%1%\" will discard current style modification.\n\nWould you like to continue anyway?"), style.name); + const std::string & style_name = m_style_manager.get_styles()[*selected_style_index].name; + wxString message = GUI::format_wxstr(_L("Changing style to \"%1%\" will discard current style modification.\n\nWould you like to continue anyway?"), style_name); MessageDialog not_loaded_style_message(nullptr, message, _L("Warning"), wxICON_WARNING | wxYES | wxNO); if (not_loaded_style_message.ShowModal() != wxID_YES) selected_style_index.reset(); @@ -2715,12 +2274,12 @@ void GLGizmoEmboss::draw_style_list() { // selected style from combo box if (selected_style_index.has_value()) { - const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; + const StyleManager::Style &style = m_style_manager.get_styles()[*selected_style_index]; // create copy to be able do fix transformation only when successfully load style - FontProp act_prop = actual_style.prop; // copy - FontProp new_prop = style.prop; // copy + StyleManager::Style cur_s = current_style; // copy + StyleManager::Style new_s = style; // copy if (m_style_manager.load_style(*selected_style_index)) { - fix_transformation(act_prop, new_prop); + ::fix_transformation(cur_s, new_s, m_parent); process(); } else { wxString title = _L("Not valid style."); @@ -2860,7 +2419,7 @@ bool GLGizmoEmboss::revertible(const std::string &name, const T *default_value, const std::string &undo_tooltip, float undo_offset, - Draw draw) + Draw draw) const { bool changed = exist_change(value, default_value); if (changed || default_value == nullptr) @@ -2882,43 +2441,43 @@ bool GLGizmoEmboss::revertible(const std::string &name, } return draw(); } +// May be move to ImGuiWrapper +template bool imgui_input(const char *label, T *v, T step, T step_fast, const char *format, ImGuiInputTextFlags flags); +template<> bool imgui_input(const char *label, float *v, float step, float step_fast, const char *format, ImGuiInputTextFlags flags) +{ return ImGui::InputFloat(label, v, step, step_fast, format, flags); } +template<> bool imgui_input(const char *label, double *v, double step, double step_fast, const char *format, ImGuiInputTextFlags flags) +{ return ImGui::InputDouble(label, v, step, step_fast, format, flags); } - -bool GLGizmoEmboss::rev_input(const std::string &name, - float &value, - const float *default_value, - const std::string &undo_tooltip, - float step, - float step_fast, - const char *format, - ImGuiInputTextFlags flags) +template +bool GLGizmoEmboss::rev_input(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, ImGuiInputTextFlags flags) const { // draw offseted input - auto draw_offseted_input = [&]()->bool{ - float input_offset = m_gui_cfg->input_offset; - float input_width = m_gui_cfg->input_width; - ImGui::SameLine(input_offset); - ImGui::SetNextItemWidth(input_width); - return ImGui::InputFloat(("##" + name).c_str(), + auto draw_offseted_input = [&offset = m_gui_cfg->input_offset, &width = m_gui_cfg->input_width, + &name, &value, &step, &step_fast, format, flags](){ + ImGui::SameLine(offset); + ImGui::SetNextItemWidth(width); + return imgui_input(("##" + name).c_str(), &value, step, step_fast, format, flags); }; float undo_offset = ImGui::GetStyle().FramePadding.x; return revertible(name, value, default_value, undo_tooltip, undo_offset, draw_offseted_input); } -bool GLGizmoEmboss::rev_input_mm(const std::string &name, - float &value, - const float *default_value_ptr, - const std::string &undo_tooltip, - float step, - float step_fast, - const char *format, - bool use_inch, - const std::optional& scale) +template +bool GLGizmoEmboss::rev_input_mm(const std::string &name, + T &value, + const T *default_value_ptr, + const std::string &undo_tooltip, + T step, + T step_fast, + const char *format, + bool use_inch, + const std::optional &scale) const { // _variable which temporary keep value - float value_ = value; - float default_value_; + T value_ = value; + T default_value_; if (use_inch) { // calc value in inch value_ *= ObjectManipulation::mm_to_in; @@ -2946,11 +2505,11 @@ bool GLGizmoEmboss::rev_input_mm(const std::string &name, bool GLGizmoEmboss::rev_checkbox(const std::string &name, bool &value, const bool *default_value, - const std::string &undo_tooltip) + const std::string &undo_tooltip) const { // draw offseted input - auto draw_offseted_input = [&]() -> bool { - ImGui::SameLine(m_gui_cfg->advanced_input_offset); + auto draw_offseted_input = [&offset = m_gui_cfg->advanced_input_offset, &name, &value](){ + ImGui::SameLine(offset); return ImGui::Checkbox(("##" + name).c_str(), &value); }; float undo_offset = ImGui::GetStyle().FramePadding.x; @@ -2962,7 +2521,7 @@ bool GLGizmoEmboss::set_height() { float &value = m_style_manager.get_font_prop().size_in_mm; // size can't be zero or negative - priv::Limits::apply(value, priv::limits.size_in_mm); + apply(value, limits.size_in_mm); if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { assert(false); @@ -3001,28 +2560,22 @@ 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) { - float &value = m_style_manager.get_font_prop().emboss; - const EmbossStyle* stored_style = m_style_manager.get_stored_style(); - const float *stored = ((stored_style)? &stored_style->prop.emboss : nullptr); + double &value = m_style_manager.get_style().projection.depth; + const StyleManager::Style * stored_style = m_style_manager.get_stored_style(); + const double *stored = ((stored_style!=nullptr)? &stored_style->projection.depth : nullptr); const std::string revert_emboss_depth = _u8L("Revert embossed depth."); const char *size_format = ((use_inch) ? "%.3f in" : "%.2f mm"); const std::string name = m_gui_cfg->translations.depth; - if (rev_input_mm(name, value, stored, revert_emboss_depth, 0.1f, 1.f, size_format, use_inch, m_scale_depth)) - if (set_depth()) - process(); + if (rev_input_mm(name, value, stored, revert_emboss_depth, 0.1, 1., size_format, use_inch, m_scale_depth)){ + // size can't be zero or negative + apply(value, limits.emboss); + + // only different value need process + if(!is_approx(value, m_volume->emboss_shape->projection.depth)) + process(); + } } bool GLGizmoEmboss::rev_slider(const std::string &name, @@ -3032,7 +2585,7 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, int v_min, int v_max, const std::string& format, - const wxString &tooltip) + const wxString &tooltip) const { auto draw_slider_optional_int = [&]() -> bool { float slider_offset = m_gui_cfg->advanced_input_offset; @@ -3054,7 +2607,7 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, float v_min, float v_max, const std::string& format, - const wxString &tooltip) + const wxString &tooltip) const { auto draw_slider_optional_float = [&]() -> bool { float slider_offset = m_gui_cfg->advanced_input_offset; @@ -3076,7 +2629,7 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, float v_min, float v_max, const std::string &format, - const wxString &tooltip) + const wxString &tooltip) const { auto draw_slider_float = [&]() -> bool { float slider_offset = m_gui_cfg->advanced_input_offset; @@ -3091,38 +2644,6 @@ bool GLGizmoEmboss::rev_slider(const std::string &name, undo_tooltip, undo_offset, draw_slider_float); } -void GLGizmoEmboss::do_translate(const Vec3d &relative_move) -{ - assert(m_volume != nullptr); - assert(m_volume->text_configuration.has_value()); - Selection &selection = m_parent.get_selection(); - assert(!selection.is_empty()); - selection.setup_cache(); - selection.translate(relative_move, TransformationType::Local); - - std::string snapshot_name; // empty mean no store undo / redo - // NOTE: it use L instead of _L macro because prefix _ is appended inside - // function do_move - // snapshot_name = L("Set surface distance"); - m_parent.do_move(snapshot_name); -} - -void GLGizmoEmboss::do_rotate(float relative_z_angle) -{ - assert(m_volume != nullptr); - assert(m_volume->text_configuration.has_value()); - Selection &selection = m_parent.get_selection(); - assert(!selection.is_empty()); - selection.setup_cache(); - selection.rotate(Vec3d(0., 0., relative_z_angle), get_transformation_type(selection)); - - std::string snapshot_name; // empty meand no store undo / redo - // NOTE: it use L instead of _L macro because prefix _ is appended - // inside function do_move - // snapshot_name = L("Set text rotation"); - m_parent.do_rotate(snapshot_name); -} - void GLGizmoEmboss::draw_advanced() { const auto &ff = m_style_manager.get_font_file_with_cache(); @@ -3144,8 +2665,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 = font_prop.collection_number.has_value() ? - *font_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); @@ -3154,26 +2674,23 @@ void GLGizmoEmboss::draw_advanced() bool exist_change = false; auto &tr = m_gui_cfg->translations; - const EmbossStyle *stored_style = nullptr; + const StyleManager::Style *stored_style = nullptr; if (m_style_manager.exist_stored_style()) stored_style = m_style_manager.get_stored_style(); bool is_the_only_one_part = m_volume->is_the_only_one_part(); - bool can_use_surface = (font_prop.use_surface)? true : // already used surface must have option to uncheck + 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->prop.use_surface : nullptr; - if (rev_checkbox(tr.use_surface, font_prop.use_surface, def_use_surface, + &stored_style->projection.use_surface : nullptr; + StyleManager::Style ¤t_style = m_style_manager.get_style(); + bool &use_surface = current_style.projection.use_surface; + if (rev_checkbox(tr.use_surface, use_surface, def_use_surface, _u8L("Revert using of model surface."))) { - if (font_prop.use_surface) { + if (use_surface) // when using surface distance is not used - font_prop.distance.reset(); - - // there should be minimal embossing depth - if (font_prop.emboss < 0.1) - font_prop.emboss = 1; - } + current_style.distance.reset(); process(); } m_imgui->disabled_end(); // !can_use_surface @@ -3200,9 +2717,9 @@ void GLGizmoEmboss::draw_advanced() m_text_lines.reset(); m_imgui->disabled_end(); // !can_use_per_glyph - auto draw_align = [&align = font_prop.align, gui_cfg = m_gui_cfg, &icons = m_icons]() { + auto draw_align = [&align = font_prop.align, input_offset = m_gui_cfg->advanced_input_offset, &icons = m_icons]() { bool is_change = false; - ImGui::SameLine(gui_cfg->advanced_input_offset); + ImGui::SameLine(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", _CTX_utf8(L_CONTEXT("Left", "Alignment"), "Alignment").c_str()); @@ -3247,13 +2764,15 @@ void GLGizmoEmboss::draw_advanced() &stored_style->prop.char_gap : nullptr; int half_ascent = font_info.ascent / 2; - int min_char_gap = -half_ascent, max_char_gap = half_ascent; - if (rev_slider(tr.char_gap, font_prop.char_gap, def_char_gap, _u8L("Revert gap between characters"), + int min_char_gap = -half_ascent; + int max_char_gap = half_ascent; + FontProp ¤t_prop = current_style.prop; + if (rev_slider(tr.char_gap, current_prop.char_gap, def_char_gap, _u8L("Revert gap between characters"), min_char_gap, max_char_gap, units_fmt, _L("Distance between characters"))){ // Condition prevent recalculation when insertint out of limits value by imgui input - if (!priv::Limits::apply(font_prop.char_gap, priv::limits.char_gap) || - !m_volume->text_configuration->style.prop.char_gap.has_value() || - m_volume->text_configuration->style.prop.char_gap != font_prop.char_gap) { + const std::optional &volume_char_gap = m_volume->text_configuration->style.prop.char_gap; + if (!apply(current_prop.char_gap, limits.char_gap) || + !volume_char_gap.has_value() || volume_char_gap != current_prop.char_gap) { // char gap is stored inside of imgui font atlas m_style_manager.clear_imgui_font(); exist_change = true; @@ -3265,13 +2784,14 @@ void GLGizmoEmboss::draw_advanced() m_imgui->disabled_begin(!is_multiline); auto def_line_gap = stored_style ? &stored_style->prop.line_gap : nullptr; - int min_line_gap = -half_ascent, max_line_gap = half_ascent; - if (rev_slider(tr.line_gap, font_prop.line_gap, def_line_gap, _u8L("Revert gap between lines"), + int min_line_gap = -half_ascent; + int max_line_gap = half_ascent; + if (rev_slider(tr.line_gap, current_prop.line_gap, def_line_gap, _u8L("Revert gap between lines"), min_line_gap, max_line_gap, units_fmt, _L("Distance between lines"))){ // Condition prevent recalculation when insertint out of limits value by imgui input - if (!priv::Limits::apply(font_prop.line_gap, priv::limits.line_gap) || - !m_volume->text_configuration->style.prop.line_gap.has_value() || - m_volume->text_configuration->style.prop.line_gap != font_prop.line_gap) { + const std::optional &volume_line_gap = m_volume->text_configuration->style.prop.line_gap; + if (!apply(current_prop.line_gap, limits.line_gap) || + !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) @@ -3284,41 +2804,38 @@ void GLGizmoEmboss::draw_advanced() // input boldness auto def_boldness = stored_style ? &stored_style->prop.boldness : nullptr; - if (rev_slider(tr.boldness, font_prop.boldness, def_boldness, _u8L("Undo boldness"), - priv::limits.boldness.gui.min, priv::limits.boldness.gui.max, units_fmt, _L("Tiny / Wide glyphs"))){ - if (!priv::Limits::apply(font_prop.boldness, priv::limits.boldness.values) || - !m_volume->text_configuration->style.prop.boldness.has_value() || - m_volume->text_configuration->style.prop.boldness != font_prop.boldness) + if (rev_slider(tr.boldness, current_prop.boldness, def_boldness, _u8L("Undo boldness"), + limits.boldness.gui.min, limits.boldness.gui.max, units_fmt, _L("Tiny / Wide glyphs"))){ + const std::optional &volume_boldness = m_volume->text_configuration->style.prop.boldness; + if (!apply(current_prop.boldness, limits.boldness.values) || + !volume_boldness.has_value() || volume_boldness != current_prop.boldness) exist_change = true; } // input italic auto def_skew = stored_style ? &stored_style->prop.skew : nullptr; - if (rev_slider(tr.skew_ration, font_prop.skew, def_skew, _u8L("Undo letter's skew"), - priv::limits.skew.gui.min, priv::limits.skew.gui.max, "%.2f", _L("Italic strength ratio"))){ - if (!priv::Limits::apply(font_prop.skew, priv::limits.skew.values) || - !m_volume->text_configuration->style.prop.skew.has_value() || - m_volume->text_configuration->style.prop.skew != font_prop.skew) + if (rev_slider(tr.skew_ration, current_prop.skew, def_skew, _u8L("Undo letter's skew"), + limits.skew.gui.min, limits.skew.gui.max, "%.2f", _L("Italic strength ratio"))){ + const std::optional &volume_skew = m_volume->text_configuration->style.prop.skew; + if (!apply(current_prop.skew, limits.skew.values) || + !volume_skew.has_value() ||volume_skew != current_prop.skew) exist_change = true; } // input surface distance - bool allowe_surface_distance = - !m_volume->text_configuration->style.prop.use_surface && - !m_volume->is_the_only_one_part(); - std::optional &distance = font_prop.distance; - float prev_distance = distance.has_value() ? *distance : .0f, - min_distance = -2 * font_prop.emboss, - max_distance = 2 * font_prop.emboss; + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + std::optional &distance = current_style.distance; + float prev_distance = distance.value_or(.0f); + float min_distance = static_cast(-2 * current_style.projection.depth); + float max_distance = static_cast(2 * current_style.projection.depth); auto def_distance = stored_style ? - &stored_style->prop.distance : nullptr; - m_imgui->disabled_begin(!allowe_surface_distance); - + &stored_style->distance : nullptr; + m_imgui->disabled_begin(!allowe_surface_distance); + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); const std::string undo_move_tooltip = _u8L("Undo translation"); const wxString move_tooltip = _L("Distance of the center of the text to the model surface."); bool is_moved = false; - bool use_inch = wxGetApp().app_config->get_bool("use_inches"); if (use_inch) { std::optional distance_inch; if (distance.has_value()) distance_inch = (*distance * ObjectManipulation::mm_to_in); @@ -3331,9 +2848,9 @@ void GLGizmoEmboss::draw_advanced() max_distance *= ObjectManipulation::mm_to_in; if (rev_slider(tr.from_surface, distance_inch, def_distance, undo_move_tooltip, min_distance, max_distance, "%.3f in", move_tooltip)) { if (distance_inch.has_value()) { - font_prop.distance = *distance_inch * ObjectManipulation::in_to_mm; + distance = *distance_inch * ObjectManipulation::in_to_mm; } else { - font_prop.distance.reset(); + distance.reset(); } is_moved = true; } @@ -3346,83 +2863,76 @@ void GLGizmoEmboss::draw_advanced() if (font_prop.per_glyph){ process(); } else { - m_volume->text_configuration->style.prop.distance = font_prop.distance; - float act_distance = font_prop.distance.has_value() ? *font_prop.distance : .0f; - do_translate(Vec3d::UnitZ() * (act_distance - prev_distance)); + do_local_z_move(m_parent, distance.value_or(.0f) - prev_distance); } } - m_imgui->disabled_end(); + m_imgui->disabled_end(); // allowe_surface_distance // slider for Clock-wise angle in degress // stored angle is optional CCW and in radians // Convert stored value to degress // minus create clock-wise roation from CCW - const std::optional &angle_opt = m_style_manager.get_font_prop().angle; - float angle = angle_opt.has_value() ? *angle_opt: 0.f; + float angle = current_style.angle.value_or(0.f); float angle_deg = static_cast(-angle * 180 / M_PI); float def_angle_deg_val = - (!stored_style || !stored_style->prop.angle.has_value()) ? - 0.f : (*stored_style->prop.angle * -180 / M_PI); + (!stored_style || !stored_style->angle.has_value()) ? + 0.f : (*stored_style->angle * -180 / M_PI); float* def_angle_deg = stored_style ? &def_angle_deg_val : nullptr; if (rev_slider(tr.rotation, angle_deg, def_angle_deg, _u8L("Undo rotation"), - priv::limits.angle.min, priv::limits.angle.max, u8"%.2f °", + limits.angle.min, limits.angle.max, u8"%.2f °", _L("Rotate text Clock-wise."))) { // convert back to radians and CCW - float angle_rad = static_cast(-angle_deg * M_PI / 180.0); - priv::to_range_pi_pi(angle_rad); - + double angle_rad = -angle_deg * M_PI / 180.0; + Geometry::to_range_pi_pi(angle_rad); - float diff_angle = angle_rad - angle; - do_rotate(diff_angle); + double diff_angle = angle_rad - angle; + do_local_z_rotate(m_parent, diff_angle); // calc angle after rotation const GLVolume *gl_volume = get_selected_gl_volume(m_parent.get_selection()); assert(gl_volume != nullptr); assert(m_style_manager.is_active_font()); if (m_style_manager.is_active_font() && gl_volume != nullptr) - m_style_manager.get_font_prop().angle = calc_up(gl_volume->world_matrix(), priv::up_limit); + 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 (font_prop.use_surface || font_prop.per_glyph) + if (use_surface || font_prop.per_glyph) process(); } // Keep up - lock button icon - ImGui::SameLine(m_gui_cfg->lock_offset); - const IconManager::Icon &icon = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::activable); - const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_bold : IconType::unlock_bold, IconState::activable); - const IconManager::Icon &icon_disable = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::disabled); - if (button(icon, icon_hover, icon_disable)) { - m_keep_up = !m_keep_up; - if (m_keep_up) { - // copy angle to volume - m_volume->text_configuration->style.prop.angle = font_prop.angle; - } + if (!m_volume->is_the_only_one_part()) { + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::activable); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_bold : IconType::unlock_bold, IconState::activable); + const IconManager::Icon &icon_disable = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::disabled); + if (button(icon, icon_hover, icon_disable)) + m_keep_up = !m_keep_up; + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", (m_keep_up? + _u8L("Unlock the text's rotation when moving text along the object's surface."): + _u8L("Lock the text's rotation when moving text along the object's surface.") + ).c_str()); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", (m_keep_up? - _u8L("Unlock the text's rotation when moving text along the object's surface."): - _u8L("Lock the text's rotation when moving text along the object's surface.") - ).c_str()); // when more collection add selector if (ff.font_file->infos.size() > 1) { ImGui::Text("%s", tr.collection.c_str()); ImGui::SameLine(m_gui_cfg->advanced_input_offset); ImGui::SetNextItemWidth(m_gui_cfg->input_width); - unsigned int selected = font_prop.collection_number.has_value() ? - *font_prop.collection_number : 0; + unsigned int selected = current_prop.collection_number.value_or(0); if (ImGui::BeginCombo("## Font collection", std::to_string(selected).c_str())) { for (unsigned int i = 0; i < ff.font_file->infos.size(); ++i) { ImGui::PushID(1 << (10 + i)); bool is_selected = (i == selected); if (ImGui::Selectable(std::to_string(i).c_str(), is_selected)) { - if (i == 0) font_prop.collection_number.reset(); - else font_prop.collection_number = i; + if (i == 0) current_prop.collection_number.reset(); + else current_prop.collection_number = i; exist_change = true; } ImGui::PopID(); @@ -3442,32 +2952,36 @@ void GLGizmoEmboss::draw_advanced() 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(); - const FontProp &prop = m_style_manager.get_font_prop(); - if (priv::apply_camera_dir(cam, m_parent, m_keep_up) && - (prop.use_surface || prop.per_glyph)){ - if (prop.per_glyph) + const Camera &cam = wxGetApp().plater()->get_camera(); + FontProp& fp = m_style_manager.get_font_prop(); + if (face_selected_volume_to_camera(cam, m_parent) && + (use_surface || fp.per_glyph)) { + if (fp.per_glyph) reinit_text_lines(); process(); } } else if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", _u8L("Orient the text towards the camera.").c_str()); } + */ + + //ImGui::SameLine(); if (ImGui::Button("Re-emboss")) GLGizmoEmboss::re_emboss(*m_volume); #ifdef ALLOW_DEBUG_MODE - ImGui::Text("family = %s", (font_prop.family.has_value() ? - font_prop.family->c_str() : + ImGui::Text("family = %s", (current_prop.family.has_value() ? + current_prop.family->c_str() : " --- ")); - ImGui::Text("face name = %s", (font_prop.face_name.has_value() ? - font_prop.face_name->c_str() : + ImGui::Text("face name = %s", (current_prop.face_name.has_value() ? + current_prop.face_name->c_str() : " --- ")); ImGui::Text("style = %s", - (font_prop.style.has_value() ? font_prop.style->c_str() : + (current_prop.style.has_value() ? current_prop.style->c_str() : " --- ")); - ImGui::Text("weight = %s", (font_prop.weight.has_value() ? - font_prop.weight->c_str() : + ImGui::Text("weight = %s", (current_prop.weight.has_value() ? + current_prop.weight->c_str() : " --- ")); std::string descriptor = style.path; @@ -3484,9 +2998,9 @@ void GLGizmoEmboss::set_minimal_window_size(bool is_advance_edit_style) float diff_y = window_size.y - min_win_size_prev.y; m_is_advanced_edit_style = is_advance_edit_style; const ImVec2 &min_win_size = get_minimal_window_size(); - ImGui::SetWindowSize(ImVec2(0.f, min_win_size.y + diff_y), - ImGuiCond_Always); - priv::change_window_position(m_set_window_offset, true); + ImVec2 new_window_size(0.f, min_win_size.y + diff_y); + ImGui::SetWindowSize(new_window_size, ImGuiCond_Always); + m_set_window_offset = ImGuiWrapper::change_window_position(on_get_name().c_str(), true); } ImVec2 GLGizmoEmboss::get_minimal_window_size() const @@ -3499,9 +3013,10 @@ ImVec2 GLGizmoEmboss::get_minimal_window_size() const else res = m_gui_cfg->minimal_window_size_with_collections; - bool is_object = m_volume->get_object()->volumes.size() == 1; - if (!is_object) + // Can change type of volume + if (!m_volume->is_the_only_one_part()) res.y += m_gui_cfg->height_of_volume_type_selector; + return res; } @@ -3513,7 +3028,7 @@ bool GLGizmoEmboss::choose_font_by_wxdialog() data.RestrictSelection(wxFONTRESTRICT_SCALABLE); // set previous selected font EmbossStyle &selected_style = m_style_manager.get_style(); - if (selected_style.type == WxFontUtils::get_actual_type()) { + if (selected_style.type == WxFontUtils::get_current_type()) { std::optional selected_font = WxFontUtils::load_wxFont( selected_style.path); if (selected_font.has_value()) data.SetInitialFont(*selected_font); @@ -3585,7 +3100,7 @@ bool GLGizmoEmboss::choose_true_type_file() // use first valid font for (auto &input_file : input_files) { std::string path = std::string(input_file.c_str()); - std::string name = priv::get_file_name(path); + std::string name = get_file_name(path); //make_unique_name(name, m_font_list); const FontProp& prop = m_style_manager.get_font_prop(); EmbossStyle style{ name, path, EmbossStyle::Type::file_path, prop }; @@ -3598,42 +3113,6 @@ bool GLGizmoEmboss::choose_true_type_file() } #endif // ALLOW_ADD_FONT_BY_FILE -#ifdef ALLOW_DEBUG_MODE -bool GLGizmoEmboss::choose_svg_file() -{ - wxArrayString input_files; - wxString fontDir = wxEmptyString; - wxString selectedFile = wxEmptyString; - wxFileDialog dialog(nullptr, _L("Choose SVG file")+":", fontDir, - selectedFile, file_wildcards(FT_SVG), - wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() == wxID_OK) dialog.GetPaths(input_files); - if (input_files.IsEmpty()) return false; - if (input_files.size() != 1) return false; - auto & input_file = input_files.front(); - std::string path = std::string(input_file.c_str()); - std::string name = priv::get_file_name(path); - - NSVGimage *image = nsvgParseFromFile(path.c_str(), "mm", 96.0f); - ExPolygons polys = NSVGUtils::to_ExPolygons(image); - nsvgDelete(image); - - BoundingBox bb; - for (const auto &p : polys) bb.merge(p.contour.points); - const FontProp &fp = m_style_manager.get_font_prop(); - float scale = fp.size_in_mm / std::max(bb.max.x(), bb.max.y()); - auto project = std::make_unique( - std::make_unique(fp.emboss / scale), scale); - indexed_triangle_set its = polygons2model(polys, *project); - return false; - // test store: - // for (auto &poly : polys) poly.scale(1e5); - // SVG svg("converted.svg", BoundingBox(polys.front().contour.points)); - // svg.draw(polys); - //return add_volume(name, its); -} -#endif // ALLOW_DEBUG_MODE - void GLGizmoEmboss::create_notification_not_valid_font( const TextConfiguration &tc) { @@ -3716,22 +3195,89 @@ void GLGizmoEmboss::init_icons() m_icons = m_icon_manager.init(filenames, size, type); } -const IconManager::Icon &priv::get_icon(const IconManager::VIcons& icons, IconType type, IconState state) { return *icons[(unsigned) type][(unsigned) state]; } -bool priv::draw_button(const IconManager::VIcons &icons, IconType type, bool disable) +static std::size_t hash_value(wxString const &s){ + boost::hash hasher; + return hasher(s.ToStdString()); +} + +// increase number when change struct FacenamesSerializer +constexpr std::uint32_t FACENAMES_VERSION = 1; +struct FacenamesSerializer { + // hash number for unsorted vector of installed font into system + size_t hash = 0; + // assumption that is loadable + std::vector good; + // Can't load for some reason + std::vector bad; +}; + +template void save(Archive &archive, wxString const &d) +{ std::string s(d.ToUTF8().data()); archive(s);} +template void load(Archive &archive, wxString &d) +{ std::string s; archive(s); d = s;} +template void serialize(Archive &ar, FacenamesSerializer &t, const std::uint32_t version) +{ + // When performing a load, the version associated with the class + // is whatever it was when that data was originally serialized + // When we save, we'll use the version that is defined in the macro + if (version != FACENAMES_VERSION) return; + ar(t.hash, t.good, t.bad); +} +CEREAL_CLASS_VERSION(::FacenamesSerializer, FACENAMES_VERSION); // register class version + +///////////// +// private namespace implementation +/////////////// +namespace { + +const IconManager::Icon &get_icon(const IconManager::VIcons& icons, IconType type, IconState state) { + return *icons[(unsigned) type][(unsigned) state]; } + +bool draw_button(const IconManager::VIcons &icons, IconType type, bool disable){ return Slic3r::GUI::button( get_icon(icons, type, IconState::activable), get_icon(icons, type, IconState::hovered), get_icon(icons, type, IconState::disabled), - disable - ); + disable);} + +TextDataBase::TextDataBase(DataBase &&parent, + const FontFileWithCache &font_file, + TextConfiguration &&text_configuration, + const EmbossProjection &projection) + : DataBase(std::move(parent)), m_font_file(font_file) /* copy */, m_text_configuration(std::move(text_configuration)) +{ + assert(m_font_file.has_value()); + shape.projection = projection; // copy + + const FontProp &fp = m_text_configuration.style.prop; + const FontFile &ff = *m_font_file.font_file; + shape.scale = get_text_shape_scale(fp, ff); } -///////////// -// priv namespace implementation -/////////////// +EmbossShape &TextDataBase::create_shape() +{ + if (!shape.shapes_with_ids.empty()) + return shape; -DataBase priv::create_emboss_data_base(const std::string &text, + // create shape by configuration + const char *text = m_text_configuration.text.c_str(); + std::wstring text_w = boost::nowide::widen(text); + const FontProp &fp = m_text_configuration.style.prop; + auto was_canceled = [&c = cancel](){ return c->load(); }; + + shape.shapes_with_ids = text2vshapes(m_font_file, text_w, fp, was_canceled); + return shape; +} + +void TextDataBase::write(ModelVolume &volume) const +{ + DataBase::write(volume); + volume.text_configuration = m_text_configuration; // copy + assert(volume.emboss_shape.has_value()); +} + +std::unique_ptr create_emboss_data_base(const std::string &text, StyleManager &style_manager, TextLinesModel &text_lines, const Selection &selection, @@ -3752,14 +3298,13 @@ DataBase priv::create_emboss_data_base(const std::string &text, return {}; // no active font in style, should never happend !!! } - const EmbossStyle &es = style_manager.get_style(); + const StyleManager::Style &style = style_manager.get_style(); // actualize font path - during changes in gui it could be corrupted // volume must store valid path assert(style_manager.get_wx_font().IsOk()); - assert(es.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0); - TextConfiguration tc{es, text}; + assert(style.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0); - if (es.prop.per_glyph) { + if (style.prop.per_glyph) { if (!text_lines.is_init()) init_text_lines(text_lines, selection, style_manager); } else @@ -3774,146 +3319,27 @@ DataBase priv::create_emboss_data_base(const std::string &text, cancel->store(true); // create new shared ptr to cancel new job cancel = std::make_shared>(false); - return Slic3r::GUI::Emboss::DataBase{style_manager.get_font_file_with_cache(), tc, volume_name, is_outside, cancel, text_lines.get_lines()}; + + DataBase base(volume_name, cancel); + base.is_outside = is_outside; + base.text_lines = text_lines.get_lines(); + base.from_surface = style.distance; + + 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); } -void priv::start_create_object_job(DataBase &emboss_data, const Vec2d &coor) +CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager& raycaster, ModelVolumeType volume_type) { - // start creation of new object - Plater *plater = wxGetApp().plater(); - const Camera &camera = plater->get_camera(); - const Pointfs &bed_shape = plater->build_volume().bed_shape(); - - // can't create new object with distance from surface - FontProp &prop = emboss_data.text_configuration.style.prop; - if (prop.distance.has_value()) prop.distance.reset(); - - // can't create new object with using surface - if (prop.use_surface) - prop.use_surface = false; - - // Transform3d volume_tr = priv::create_transformation_on_bed(mouse_pos, camera, bed_shape, prop.emboss / 2); - DataCreateObject data{std::move(emboss_data), coor, camera, bed_shape}; - auto job = std::make_unique(std::move(data)); - Worker &worker = plater->get_ui_job_worker(); - queue_job(worker, std::move(job)); -} - -void priv::start_create_volume_job(const ModelObject *object, - const Transform3d volume_trmat, - DataBase &emboss_data, - ModelVolumeType volume_type) -{ - bool &use_surface = emboss_data.text_configuration.style.prop.use_surface; - std::unique_ptr job; - if (use_surface) { - // Model to cut surface from. - SurfaceVolumeData::ModelSources sources = create_sources(object->volumes); - if (sources.empty()) { - use_surface = false; - } else { - SurfaceVolumeData sfvd{volume_trmat, std::move(sources)}; - CreateSurfaceVolumeData surface_data{std::move(emboss_data), std::move(sfvd), volume_type, object->id()}; - job = std::make_unique(std::move(surface_data)); - } - } - if (!use_surface) { - // create volume - DataCreateVolume data{std::move(emboss_data), volume_type, object->id(), volume_trmat}; - job = std::make_unique(std::move(data)); - } - + auto gizmo = static_cast(GLGizmosManager::Emboss); + const GLVolume *gl_volume = get_first_hovered_gl_volume(canvas); Plater *plater = wxGetApp().plater(); - Worker &worker = plater->get_ui_job_worker(); - queue_job(worker, std::move(job)); + return CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), + plater->get_ui_job_worker(), volume_type, raycaster, gizmo, gl_volume, style.distance, style.angle}; } -bool priv::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, - StyleManager &style_manager, - GLCanvas3D &canvas) -{ - assert(gl_volume != nullptr); - if (gl_volume == nullptr) return false; - if (gl_volume->volume_idx() < 0) return false; - - Plater *plater = wxGetApp().plater(); - const ModelObjectPtrs &objects = plater->model().objects; - - int object_idx = gl_volume->object_idx(); - if (object_idx < 0 || static_cast(object_idx) >= objects.size()) return false; - const ModelObject *obj_ptr = objects[object_idx]; - if (obj_ptr == nullptr) return false; - const ModelObject &obj = *obj_ptr; - size_t vol_id = obj.volumes[gl_volume->volume_idx()]->id().id; - auto cond = RaycastManager::AllowVolumes({vol_id}); - - RaycastManager::Meshes meshes = create_meshes(canvas, cond); - raycaster.actualize(obj, &cond, &meshes); - - const Camera &camera = plater->get_camera(); - std::optional hit = ray_from_camera(raycaster, screen_coor, camera, &cond); - - // context menu for add text could be open only by right click on an - // object. After right click, object is selected and object_idx is set - // also hit must exist. But there is options to add text by object list - if (!hit.has_value()) - return false; - - // Create result volume transformation - Transform3d surface_trmat = create_transformation_onto_surface(hit->position, hit->normal, priv::up_limit); - const FontProp &font_prop = emboss_data.text_configuration.style.prop; - apply_transformation(font_prop, surface_trmat); - Transform3d instance = gl_volume->get_instance_transformation().get_matrix(); - Transform3d volume_trmat = instance.inverse() * surface_trmat; - - if (font_prop.per_glyph){ - init_new_text_line(text_lines, volume_trmat, obj, style_manager); - emboss_data.text_lines = text_lines.get_lines(); - } - start_create_volume_job(obj_ptr, volume_trmat, emboss_data, volume_type); - return true; -} - -void priv::find_closest_volume(const Selection &selection, - const Vec2d &screen_center, - const Camera &camera, - const ModelObjectPtrs &objects, - Vec2d *closest_center, - const GLVolume **closest_volume) -{ - assert(closest_center != nullptr); - assert(closest_volume != nullptr); - assert(*closest_volume == nullptr); - const Selection::IndicesList &indices = selection.get_volume_idxs(); - assert(!indices.empty()); // no selected volume - if (indices.empty()) return; - - double center_sq_distance = std::numeric_limits::max(); - for (unsigned int id : indices) { - const GLVolume *gl_volume = selection.get_volume(id); - const ModelVolume *volume = get_model_volume(*gl_volume, objects); - if (volume == nullptr || !volume->is_model_part()) continue; - Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *gl_volume); - Vec2d c = hull.centroid().cast(); - Vec2d d = c - screen_center; - bool is_bigger_x = std::fabs(d.x()) > std::fabs(d.y()); - if ((is_bigger_x && d.x() * d.x() > center_sq_distance) || - (!is_bigger_x && d.y() * d.y() > center_sq_distance)) continue; - - double distance = d.squaredNorm(); - if (center_sq_distance < distance) continue; - center_sq_distance = distance; - *closest_center = c; - *closest_volume = gl_volume; - } -} - -ImVec2 priv::calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size) +ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size) { const Selection::IndicesList indices = selection.get_volume_idxs(); // no selected volume @@ -3932,86 +3358,384 @@ ImVec2 priv::calc_fine_position(const Selection &selection, const ImVec2 &window return offset; } -// Need internals to get window -#include "imgui/imgui_internal.h" -void priv::change_window_position(std::optional& output_window_offset, bool try_to_fix) { - const char* name = "Emboss"; - ImGuiWindow *window = ImGui::FindWindowByName(name); - // is window just created - if (window == NULL) - return; - - // position of window on screen - ImVec2 position = window->Pos; - ImVec2 size = window->SizeFull; - - // screen size - ImVec2 screen = ImGui::GetMainViewport()->Size; - - if (position.x < 0) { - if (position.y < 0) - output_window_offset = ImVec2(0, 0); - else - output_window_offset = ImVec2(0, position.y); - } else if (position.y < 0) { - output_window_offset = ImVec2(position.x, 0); - } else if (screen.x < (position.x + size.x)) { - if (screen.y < (position.y + size.y)) - output_window_offset = ImVec2(screen.x - size.x, screen.y - size.y); - else - output_window_offset = ImVec2(screen.x - size.x, position.y); - } else if (screen.y < (position.y + size.y)) { - output_window_offset = ImVec2(position.x, screen.y - size.y); - } - - if (!try_to_fix && output_window_offset.has_value()) - output_window_offset = ImVec2(-1, -1); // Cannot +std::string concat(std::vector data) { + std::stringstream ss; + for (const auto &d : data) + ss << d.c_str() << ", "; + return ss.str(); } -bool priv::apply_camera_dir(const Camera &camera, GLCanvas3D &canvas, bool keep_up) { - const Vec3d &cam_dir = camera.get_dir_forward(); +boost::filesystem::path get_fontlist_cache_path(){ + return boost::filesystem::path(data_dir()) / "cache" / "fonts.cereal"; +} - 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(); +bool store(const Facenames &facenames) { + std::string cache_path = get_fontlist_cache_path().string(); + boost::nowide::ofstream file(cache_path, std::ios::binary); + ::cereal::BinaryOutputArchive archive(file); + std::vector good; + good.reserve(facenames.faces.size()); + for (const FaceName &face : facenames.faces) good.push_back(face.wx_name); + FacenamesSerializer data = {facenames.hash, good, facenames.bad}; - Vec3d emboss_dir(0., 0., -1.); + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); - // 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); + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to write fontlist cache - " << cache_path << ex.what(); + return false; } - - 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; } +bool load(Facenames &facenames) { + boost::filesystem::path path = get_fontlist_cache_path(); + std::string path_str = path.string(); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "Fontlist cache - '" << path_str << "' does not exists."; + return false; + } + boost::nowide::ifstream file(path_str, std::ios::binary); + cereal::BinaryInputArchive archive(file); + + FacenamesSerializer data; + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to load fontlist cache - '" << path_str << "'. Exception: " << ex.what(); + return false; + } + + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); + + facenames.hash = data.hash; + facenames.faces.reserve(data.good.size()); + for (const wxString &face : data.good) + facenames.faces.push_back({face}); + facenames.bad = data.bad; + return true; +} + +void init_truncated_names(Facenames &face_names, float max_width) +{ + for (FaceName &face : face_names.faces) { + std::string name_str(face.wx_name.ToUTF8().data()); + face.name_truncated = ImGuiWrapper::trunc(name_str, max_width); + } + face_names.has_truncated_names = true; +} + +void init_face_names(Facenames &face_names) +{ + Timer t("enumerate_fonts"); + if (face_names.is_init) return; + face_names.is_init = true; + + // to reload fonts from system, when install new one + wxFontEnumerator::InvalidateCache(); + + // try load cache + // Only not OS enumerated face has hash value 0 + if (face_names.hash == 0) { + load(face_names); + face_names.has_truncated_names = false; + } + + using namespace std::chrono; + steady_clock::time_point enumerate_start = steady_clock::now(); + ScopeGuard sg([&enumerate_start, &face_names = face_names]() { + steady_clock::time_point enumerate_end = steady_clock::now(); + long long enumerate_duration = duration_cast(enumerate_end - enumerate_start).count(); + BOOST_LOG_TRIVIAL(info) << "OS enumerate " << face_names.faces.size() << " fonts " + << "(+ " << face_names.bad.size() << " can't load " + << "= " << face_names.faces.size() + face_names.bad.size() << " fonts) " + << "in " << enumerate_duration << " ms\n" << concat(face_names.bad); + }); + wxArrayString facenames = wxFontEnumerator::GetFacenames(face_names.encoding); + size_t hash = boost::hash_range(facenames.begin(), facenames.end()); + // Zero value is used as uninitialized hash + if (hash == 0) hash = 1; + // check if it is same as last time + if (face_names.hash == hash) { + // no new installed font + BOOST_LOG_TRIVIAL(info) << "Same FontNames hash, cache is used. " + << "For clear cache delete file: " << get_fontlist_cache_path().string(); + return; + } + + BOOST_LOG_TRIVIAL(info) << ((face_names.hash == 0) ? + "FontName list is generate from scratch." : + "Hash are different. Only previous bad fonts are used and set again as bad"); + face_names.hash = hash; + + // validation lambda + auto is_valid_font = [encoding = face_names.encoding, bad = face_names.bad /*copy*/](const wxString &name) { + if (name.empty()) return false; + + // vertical font start with @, we will filter it out + // Not sure if it is only in Windows so filtering is on all platforms + if (name[0] == '@') return false; + + // previously detected bad font + auto it = std::lower_bound(bad.begin(), bad.end(), name); + if (it != bad.end() && *it == name) return false; + + wxFont wx_font(wxFontInfo().FaceName(name).Encoding(encoding)); + //* + // Faster chech if wx_font is loadable but not 100% + // names could contain not loadable font + if (!WxFontUtils::can_load(wx_font)) return false; + + /*/ + // Slow copy of font files to try load font + // After this all files are loadable + auto font_file = WxFontUtils::create_font_file(wx_font); + if (font_file == nullptr) + return false; // can't create font file + // */ + return true; + }; + + face_names.faces.clear(); + face_names.bad.clear(); + face_names.faces.reserve(facenames.size()); + std::sort(facenames.begin(), facenames.end()); + for (const wxString &name : facenames) { + if (is_valid_font(name)) { + face_names.faces.push_back({name}); + }else{ + face_names.bad.push_back(name); + } + } + assert(std::is_sorted(face_names.bad.begin(), face_names.bad.end())); + face_names.has_truncated_names = false; + store(face_names); +} + +void draw_font_preview(FaceName &face, const std::string& text, Facenames &faces, const GuiCfg &cfg, bool is_visible){ + // Limit for opened font files at one moment + unsigned int &count_opened_fonts = faces.count_opened_font_files; + // Size of texture + ImVec2 size(cfg.face_name_size.x(), cfg.face_name_size.y()); + float count_cached_textures_f = static_cast(faces.count_cached_textures); + std::string state_text; + // uv0 and uv1 set to pixel 0,0 in texture + ImVec2 uv0(0.f, 0.f), uv1(1.f / size.x, 1.f / size.y / count_cached_textures_f); + if (face.is_created != nullptr) { + // not created preview + if (*face.is_created) { + // Already created preview + size_t texture_index = face.texture_index; + uv0 = ImVec2(0.f, texture_index / count_cached_textures_f); + uv1 = ImVec2(1.f, (texture_index + 1) / count_cached_textures_f); + } else { + // Not finished preview + if (is_visible) { + // when not canceled still loading + state_text = (face.cancel->load()) ? + " " + _u8L("No symbol"): + " ... " + _u8L("Loading"); + } else { + // not finished and not visible cancel job + face.is_created = nullptr; + face.cancel->store(true); + } + } + } else if (is_visible && count_opened_fonts < cfg.max_count_opened_font_files) { + ++count_opened_fonts; + face.cancel = std::make_shared(false); + face.is_created = std::make_shared(false); + + const unsigned char gray_level = 5; + // format type and level must match to texture data + const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; + const GLint level = 0; + // select next texture index + size_t texture_index = (faces.texture_index + 1) % faces.count_cached_textures; + + // set previous cach as deleted + for (FaceName &f : faces.faces) + if (f.texture_index == texture_index) { + if (f.cancel != nullptr) f.cancel->store(true); + f.is_created = nullptr; + } + + faces.texture_index = texture_index; + face.texture_index = texture_index; + + // render text to texture + FontImageData data{ + text, + face.wx_name, + faces.encoding, + faces.texture_id, + faces.texture_index, + cfg.face_name_size, + gray_level, + format, + type, + level, + &count_opened_fonts, + face.cancel, // copy + face.is_created // copy + }; + auto job = std::make_unique(std::move(data)); + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::move(job)); + } else { + // cant start new thread at this moment so wait in queue + state_text = " ... " + _u8L("In queue"); + } + + if (!state_text.empty()) { + ImGui::SameLine(cfg.face_name_texture_offset_x); + ImGui::Text("%s", state_text.c_str()); + } + + ImGui::SameLine(cfg.face_name_texture_offset_x); + ImTextureID tex_id = (void *) (intptr_t) faces.texture_id; + ImGui::Image(tex_id, size, uv0, uv1); +} + +GuiCfg create_gui_configuration() +{ + GuiCfg cfg; // initialize by default values; + + float line_height = ImGui::GetTextLineHeight(); + float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); + float space = line_height_with_spacing - line_height; + const ImGuiStyle &style = ImGui::GetStyle(); + + cfg.max_style_name_width = ImGui::CalcTextSize("Maximal font name, extended").x; + + cfg.icon_width = static_cast(std::ceil(line_height)); + // make size pair number + if (cfg.icon_width % 2 != 0) ++cfg.icon_width; + + cfg.delete_pos_x = cfg.max_style_name_width + space; + const float count_line_of_text = 3.f; + cfg.text_size = ImVec2(-FLT_MIN, line_height_with_spacing * count_line_of_text); + ImVec2 letter_m_size = ImGui::CalcTextSize("M"); + const float count_letter_M_in_input = 12.f; + cfg.input_width = letter_m_size.x * count_letter_M_in_input; + GuiCfg::Translations &tr = cfg.translations; + + // TRN - Input label. Be short as possible + // Select look of letter shape + tr.font = _u8L("Font"); + // TRN - Input label. Be short as possible + // Height of one text line - Font Ascent + tr.height = _u8L("Height"); + // TRN - Input label. Be short as possible + // Size in emboss direction + tr.depth = _u8L("Depth"); + + float max_text_width = std::max({ + ImGui::CalcTextSize(tr.font.c_str()).x, + ImGui::CalcTextSize(tr.height.c_str()).x, + ImGui::CalcTextSize(tr.depth.c_str()).x}); + cfg.indent = static_cast(cfg.icon_width); + cfg.input_offset = style.WindowPadding.x + cfg.indent + max_text_width + space; + + // TRN - Input label. Be short as possible + // Copy surface of model on surface of the embossed text + tr.use_surface = _u8L("Use surface"); + // TRN - Input label. Be short as possible + // Option to change projection on curved surface + // for each character(glyph) in text separately + tr.per_glyph = _u8L("Per glyph"); + // TRN - Input label. Be short as possible + // Align Top|Middle|Bottom and Left|Center|Right + tr.alignment = _u8L("Alignment"); + // TRN - Input label. Be short as possible + tr.char_gap = _u8L("Char gap"); + // TRN - Input label. Be short as possible + tr.line_gap = _u8L("Line gap"); + // TRN - Input label. Be short as possible + tr.boldness = _u8L("Boldness"); + + // TRN - Input label. Be short as possible + // Like Font italic + tr.skew_ration = _u8L("Skew ratio"); + + // TRN - Input label. Be short as possible + // Distance from model surface to be able + // move text as part fully into not flat surface + // move text as modifier fully out of not flat surface + tr.from_surface = _u8L("From surface"); + + // TRN - Input label. Be short as possible + // Angle between Y axis and text line direction. + tr.rotation = _u8L("Rotation"); + + // TRN - Input label. Be short as possible + // Keep vector from bottom to top of text aligned with printer Y axis + tr.keep_up = _u8L("Keep up"); + + // TRN - Input label. Be short as possible. + // Some Font file contain multiple fonts inside and + // this is numerical selector of font inside font collections + tr.collection = _u8L("Collection"); + + float max_advanced_text_width = std::max({ + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.per_glyph.c_str()).x, + ImGui::CalcTextSize(tr.alignment.c_str()).x, + ImGui::CalcTextSize(tr.char_gap.c_str()).x, + ImGui::CalcTextSize(tr.line_gap.c_str()).x, + ImGui::CalcTextSize(tr.boldness.c_str()).x, + ImGui::CalcTextSize(tr.skew_ration.c_str()).x, + ImGui::CalcTextSize(tr.from_surface.c_str()).x, + ImGui::CalcTextSize(tr.rotation.c_str()).x + cfg.icon_width + 2*space, + ImGui::CalcTextSize(tr.keep_up.c_str()).x, + ImGui::CalcTextSize(tr.collection.c_str()).x }); + cfg.advanced_input_offset = max_advanced_text_width + + 3 * space + cfg.indent; + + cfg.lock_offset = cfg.advanced_input_offset - (cfg.icon_width + space); + // calculate window size + float window_title = line_height + 2*style.FramePadding.y + 2 * style.WindowTitleAlign.y; + float input_height = line_height_with_spacing + 2*style.FramePadding.y; + float tree_header = line_height_with_spacing; + float separator_height = 2 + style.FramePadding.y; + + // "Text is to object" + radio buttons + cfg.height_of_volume_type_selector = separator_height + line_height_with_spacing + input_height; + + float window_height = + window_title + // window title + cfg.text_size.y + // text field + input_height * 4 + // font name + height + depth + style selector + tree_header + // advance tree + separator_height + // presets separator line + line_height_with_spacing + // "Presets" + 2 * style.WindowPadding.y; + float window_width = cfg.input_offset + cfg.input_width + 2*style.WindowPadding.x + + 2 * (cfg.icon_width + space); + cfg.minimal_window_size = ImVec2(window_width, window_height); + + // 9 = useSurface, charGap, lineGap, bold, italic, surfDist, rotation, keepUp + // 4 = 1px for fix each edit image of drag float + float advance_height = input_height * 9 + 9; + cfg.minimal_window_size_with_advance = + ImVec2(cfg.minimal_window_size.x, + cfg.minimal_window_size.y + advance_height); + + cfg.minimal_window_size_with_collections = + ImVec2(cfg.minimal_window_size_with_advance.x, + cfg.minimal_window_size_with_advance.y + input_height); + + int max_style_image_width = static_cast(std::round(cfg.max_style_name_width/2 - 2 * style.FramePadding.x)); + int max_style_image_height = static_cast(std::round(1.5 * input_height)); + cfg.max_style_image_size = Vec2i(max_style_image_width, max_style_image_height); + cfg.face_name_size = Vec2i(cfg.input_width, line_height_with_spacing); + cfg.face_name_texture_offset_x = cfg.face_name_size.x() + space; + return cfg; +} +} // namespace + // any existing icon filename to not influence GUI const std::string GLGizmoEmboss::M_ICON_FILENAME = "cut.svg"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp index c3cacde6a8..aac28f1b89 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp @@ -9,7 +9,7 @@ #include "GLGizmoRotate.hpp" #include "slic3r/GUI/IconManager.hpp" #include "slic3r/GUI/SurfaceDrag.hpp" -#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/I18N.hpp" // TODO: not needed #include "slic3r/GUI/TextLines.hpp" #include "slic3r/Utils/RaycastManager.hpp" #include "slic3r/Utils/EmbossStyleManager.hpp" @@ -20,7 +20,6 @@ #include "libslic3r/Emboss.hpp" #include "libslic3r/Point.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/TextConfiguration.hpp" #include @@ -37,20 +36,20 @@ namespace Slic3r::GUI { class GLGizmoEmboss : public GLGizmoBase { public: - GLGizmoEmboss(GLCanvas3D& parent); + explicit GLGizmoEmboss(GLCanvas3D& parent); /// /// Create new embossed text volume by type on position of mouse /// /// Object part / Negative volume / Modifier /// Define position of new volume - void create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); + bool create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); /// /// Create new text without given position /// /// Object part / Negative volume / Modifier - void create_volume(ModelVolumeType volume_type); + bool create_volume(ModelVolumeType volume_type); /// /// Handle pressing of shortcut @@ -65,6 +64,14 @@ public: /// True on success start job otherwise False bool do_mirror(size_t axis); + + /// + /// Call on change inside of object conatining projected volume + /// + /// Way to stop re_emboss job + /// True on success otherwise False + static bool re_emboss(const ModelVolume &text, std::shared_ptr> job_cancel = nullptr); + protected: bool on_init() override; std::string on_get_name() const override; @@ -90,10 +97,11 @@ protected: /// Propagete normaly return false. bool on_mouse(const wxMouseEvent &mouse_event) override; - bool wants_enter_leave_snapshots() const override { return true; } - std::string get_gizmo_entering_text() const override { return _u8L("Enter emboss gizmo"); } - std::string get_gizmo_leaving_text() const override { return _u8L("Leave emboss gizmo"); } - std::string get_action_snapshot_name() const override { return _u8L("Embossing actions"); } + bool wants_enter_leave_snapshots() const override; + std::string get_gizmo_entering_text() const override; + std::string get_gizmo_leaving_text() const override; + std::string get_action_snapshot_name() const override; + private: void volume_transformation_changing(); void volume_transformation_changed(); @@ -111,7 +119,6 @@ private: void draw_window(); void draw_text_input(); void draw_model_type(); - void fix_transformation(const FontProp &from, const FontProp &to); void draw_style_list(); void draw_delete_style_button(); void draw_style_rename_popup(); @@ -120,8 +127,6 @@ private: void draw_style_save_as_popup(); void draw_style_add_button(); void init_font_name_texture(); - struct FaceName; - void draw_font_preview(FaceName &face, bool is_visible); void draw_font_list_line(); void draw_font_list(); void draw_height(bool use_inch); @@ -129,8 +134,6 @@ private: // call after set m_style_manager.get_style().prop.size_in_mm bool set_height(); - // call after set m_style_manager.get_style().prop.emboss - bool set_depth(); bool draw_italic_button(); bool draw_bold_button(); @@ -138,30 +141,25 @@ private: bool select_facename(const wxString& facename); - void do_translate(const Vec3d& relative_move); - void do_rotate(float relative_z_angle); - - bool rev_input_mm(const std::string &name, float &value, const float *default_value, - const std::string &undo_tooltip, float step, float step_fast, const char *format, - bool use_inch, const std::optional& scale); + template bool rev_input_mm(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, bool use_inch, const std::optional& scale) const; /// /// Reversible input float with option to restor default value /// TODO: make more general, static and move to ImGuiWrapper /// /// True when value changed otherwise FALSE. - bool rev_input(const std::string &name, float &value, const float *default_value, - const std::string &undo_tooltip, float step, float step_fast, const char *format, - ImGuiInputTextFlags flags = 0); - bool rev_checkbox(const std::string &name, bool &value, const bool* default_value, const std::string &undo_tooltip); + template bool rev_input(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, ImGuiInputTextFlags flags = 0) const; + bool rev_checkbox(const std::string &name, bool &value, const bool* default_value, const std::string &undo_tooltip) const; bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, - const std::string &undo_tooltip, int v_min, int v_max, const std::string &format, const wxString &tooltip); + const std::string &undo_tooltip, int v_min, int v_max, const std::string &format, const wxString &tooltip) const; bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, - const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip); + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip) const; bool rev_slider(const std::string &name, float &value, const float *default_value, - const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip); - template - bool revertible(const std::string &name, T &value, const T *default_value, const std::string &undo_tooltip, float undo_offset, Draw draw); + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip) const; + template bool revertible(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, float undo_offset, Draw draw) const; bool m_should_set_minimal_windows_size = false; void set_minimal_window_size(bool is_advance_edit_style); @@ -173,71 +171,13 @@ private: void on_mouse_change_selection(const wxMouseEvent &mouse_event); // When open text loaded from .3mf it could be written with unknown font - bool m_is_unknown_font; + bool m_is_unknown_font = false; void create_notification_not_valid_font(const TextConfiguration& tc); void create_notification_not_valid_font(const std::string& text); void remove_notification_not_valid_font(); - - // This configs holds GUI layout size given by translated texts. - // etc. When language changes, GUI is recreated and this class constructed again, - // so the change takes effect. (info by GLGizmoFdmSupports.hpp) - struct GuiCfg - { - // Detect invalid config values when change monitor DPI - double screen_scale; - float main_toolbar_height; - // Zero means it is calculated in init function - ImVec2 minimal_window_size = ImVec2(0, 0); - ImVec2 minimal_window_size_with_advance = ImVec2(0, 0); - ImVec2 minimal_window_size_with_collections = ImVec2(0, 0); - float height_of_volume_type_selector = 0.f; - float input_width = 0.f; - float delete_pos_x = 0.f; - float max_style_name_width = 0.f; - unsigned int icon_width = 0; - - // maximal width and height of style image - Vec2i max_style_image_size = Vec2i(0, 0); - - float indent = 0.f; - float input_offset = 0.f; - float advanced_input_offset = 0.f; - - float lock_offset = 0.f; - - ImVec2 text_size; - - // maximal size of face name image - Vec2i face_name_size = Vec2i(100, 0); - float face_name_texture_offset_x = 0.f; - - // maximal texture generate jobs running at once - unsigned int max_count_opened_font_files = 10; - - // Only translations needed for calc GUI size - struct Translations - { - std::string font; - std::string height; - std::string depth; - - // advanced - std::string use_surface; - std::string per_glyph; - std::string alignment; - std::string char_gap; - std::string line_gap; - std::string boldness; - std::string skew_ration; - std::string from_surface; - std::string rotation; - std::string collection; - }; - Translations translations; - }; - std::optional m_gui_cfg; - static GuiCfg create_gui_configuration(); + struct GuiCfg; + std::unique_ptr m_gui_cfg; // Is open tree with advanced options bool m_is_advanced_edit_style = false; @@ -251,62 +191,9 @@ private: // Keep information about stored styles and loaded actual style to compare with Emboss::StyleManager m_style_manager; - struct FaceName{ - wxString wx_name; - std::string name_truncated = ""; - size_t texture_index = 0; - // State for generation of texture - // when start generate create share pointers - std::shared_ptr> cancel = nullptr; - // R/W only on main thread - finalize of job - std::shared_ptr is_created = nullptr; - }; - - // Keep sorted list of loadable face names - struct Facenames - { - // flag to keep need of enumeration fonts from OS - // false .. wants new enumeration check by Hash - // true .. already enumerated(During opened combo box) - bool is_init = false; - - bool has_truncated_names = false; - - // data of can_load() faces - std::vector faces = {}; - // Sorter set of Non valid face names in OS - std::vector bad = {}; - - // Configuration of font encoding - static constexpr wxFontEncoding encoding = wxFontEncoding::wxFONTENCODING_SYSTEM; - - // Identify if preview texture exists - GLuint texture_id = 0; - - // protection for open too much font files together - // Gtk:ERROR:../../../../gtk/gtkiconhelper.c:494:ensure_surface_for_gicon: assertion failed (error == NULL): Failed to load /usr/share/icons/Yaru/48x48/status/image-missing.png: Error opening file /usr/share/icons/Yaru/48x48/status/image-missing.png: Too many open files (g-io-error-quark, 31) - // This variable must exist until no CreateFontImageJob is running - unsigned int count_opened_font_files = 0; - - // Configuration for texture height - const int count_cached_textures = 32; - - // index for new generated texture index(must be lower than count_cached_textures) - size_t texture_index = 0; - - // hash created from enumerated font from OS - // check when new font was installed - size_t hash = 0; - - // filtration pattern - std::string search = ""; - std::vector hide; // result of filtration - } m_face_names; - static bool store(const Facenames &facenames); - static bool load(Facenames &facenames); - - static void init_face_names(Facenames &facenames); - static void init_truncated_names(Facenames &face_names, float max_width); + // pImpl to hide implementation of FaceNames to .cpp file + struct Facenames; // forward declaration + std::unique_ptr m_face_names; // Text to emboss std::string m_text; // Sequence of Unicode UTF8 symbols @@ -316,7 +203,7 @@ private: // current selected volume // NOTE: Be carefull could be uninitialized (removed from Model) - ModelVolume *m_volume; + ModelVolume *m_volume = nullptr; // When work with undo redo stack there could be situation that // m_volume point to unexisting volume so One need also objectID @@ -326,7 +213,7 @@ private: bool m_text_contain_unknown_glyph = false; // cancel for previous update of volume to cancel finalize part - std::shared_ptr> m_job_cancel; + std::shared_ptr> m_job_cancel = nullptr; // Keep information about curvature of text line around surface TextLinesModel m_text_lines; @@ -340,8 +227,8 @@ private: // Keep data about dragging only during drag&drop std::optional m_surface_drag; - // TODO: it should be accessible by other gizmo too. - // May be move to plater? + // Keep old scene triangle data in AABB trees, + // all the time it need actualize before use. RaycastManager m_raycast_manager; // For text on scaled objects diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp new file mode 100644 index 0000000000..bac1923ad3 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -0,0 +1,2261 @@ +#include "GLGizmoSVG.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#include "slic3r/GUI/MainFrame.hpp" // to update title when add text +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/Jobs/EmbossJob.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +#include "libslic3r/Point.hpp" +#include "libslic3r/SVG.hpp" // debug store +#include "libslic3r/Geometry.hpp" // covex hull 2d +#include "libslic3r/Timer.hpp" // covex hull 2d +#include "libslic3r/Emboss.hpp" // heal_shape + +#include "libslic3r/NSVGUtils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/ClipperUtils.hpp" // union_ex + +#include "imgui/imgui_stdlib.h" // using std::string for inputs +#include "nanosvg/nanosvg.h" // load SVG file + +#include // detection of change DPI +#include + +#include +#include // measure enumeration of fonts +#include // save for svg +#include + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +GLGizmoSVG::GLGizmoSVG(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -3) + , m_gui_cfg(nullptr) + , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) +{ + m_rotate_gizmo.set_group_id(0); + m_rotate_gizmo.set_force_local_coordinate(true); +} + +// Private functions to create emboss volume +namespace{ + +// Variable keep limits for variables +const struct Limits +{ + MinMax depth{0.01, 1e4}; // in mm + MinMax size{0.01f, 1e4f}; // in mm (width + height) + MinMax ui_size{5.f, 100.f}; // in mm (width + height) - only slider values + MinMax ui_size_in{.1f, 4.f};// in inches (width + height) - only slider values + MinMax relative_scale_ratio{1e-5, 1e4}; // change size + // distance text object from surface + MinMax angle{-180.f, 180.f}; // in degrees +} limits; + +// Store path to directory with svg for import and export svg's +wxString last_used_directory = wxEmptyString; + +/// +/// Open file dialog with svg files +/// +/// File path to svg +std::string choose_svg_file(); + +constexpr double get_tesselation_tolerance(double scale){ + constexpr double tesselation_tolerance_in_mm = .1; //8e-2; + constexpr double tesselation_tolerance_scaled = (tesselation_tolerance_in_mm*tesselation_tolerance_in_mm) / SCALING_FACTOR / SCALING_FACTOR; + return tesselation_tolerance_scaled / scale / scale; +} + +/// +/// Let user to choose file with (S)calable (V)ector (G)raphics - SVG. +/// Than let select contour +/// +/// SVG file path, when empty promt user to select one +/// EmbossShape to create +EmbossShape select_shape(std::string_view filepath = "", double tesselation_tolerance_in_mm = get_tesselation_tolerance(1.)); + +/// +/// Create new embos data +/// +/// Cancel for previous job +/// To distiquish whether it is outside of model +/// SVG file path +/// Base data for emboss SVG +DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath = ""); + +/// +/// Separate file name from file path. +/// String after last delimiter and before last point +/// +/// path return by file dialog +/// File name without directory path +std::string get_file_name(const std::string &file_path); + +/// +/// Create volume name from shape information +/// +/// File path +/// Name for volume +std::string volume_name(const EmbossShape& shape); + +/// +/// Create input for volume creation +/// +/// parent of gizmo +/// Keep scene +/// Type of volume to be created +/// Params +CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager &raycaster, ModelVolumeType volume_type); + +enum class IconType : unsigned { + reset_value, + reset_value_hover, + refresh, + refresh_hover, + change_file, + bake, + bake_inactive, + save, + exclamation, + lock, + lock_hover, + unlock, + unlock_hover, + reflection_x, + reflection_x_hover, + reflection_y, + reflection_y_hover, + // automatic calc of icon's count + _count +}; +// Do not forgot add loading of file in funtion: +// IconManager::Icons init_icons( + +const IconManager::Icon &get_icon(const IconManager::Icons &icons, IconType type) { + return *icons[static_cast(type)]; } + +// This configs holds GUI layout size given by translated texts. +// etc. When language changes, GUI is recreated and this class constructed again, +// so the change takes effect. (info by GLGizmoFdmSupports.hpp) +struct GuiCfg +{ + // Detect invalid config values when change monitor DPI + double screen_scale = -1.; + float main_toolbar_height = -1.f; + + // Define bigger size(width or height) + unsigned texture_max_size_px = 256; + + // Zero means it is calculated in init function + ImVec2 window_size = ImVec2(0, 0); + ImVec2 window_size_for_object = ImVec2(0, 0); // without change type + + float input_width = 0.f; + float input_offset = 0.f; + + float icon_width = 0.f; + + // offset for checbox for lock up vector + float lock_offset = 0.f; + // Only translations needed for calc GUI size + struct Translations + { + std::string depth; + std::string size; + std::string use_surface; + std::string rotation; + std::string distance; // from surface + std::string mirror; + }; + Translations translations; +}; +GuiCfg create_gui_configuration(); + +} // namespace + +// use private definition +struct GLGizmoSVG::GuiCfg: public ::GuiCfg{}; + +bool GLGizmoSVG::create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type); + if (!base) return false; // Uninterpretable svg + return start_create_volume(input, std::move(base), mouse_pos); +} + +bool GLGizmoSVG::create_volume(ModelVolumeType volume_type) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel,volume_type); + if (!base) return false; // Uninterpretable svg + return start_create_volume_without_position(input, std::move(base)); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, ModelVolumeType volume_type){ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); + if (!base) return false; // Uninterpretable svg + return start_create_volume_without_position(input, std::move(base)); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); + if (!base) return false; // Uninterpretable svg + return start_create_volume(input, std::move(base), mouse_pos); +} + +bool GLGizmoSVG::is_svg(const ModelVolume &volume) { + return volume.emboss_shape.has_value(); +} + +bool GLGizmoSVG::is_svg_object(const ModelVolume &volume) { + if (!volume.emboss_shape.has_value()) return false; + if (volume.type() != ModelVolumeType::MODEL_PART) return false; + for (const ModelVolume *v : volume.get_object()->volumes) { + if (v->id() == volume.id()) continue; + if (v->type() == ModelVolumeType::MODEL_PART) return false; + } + return true; +} + +namespace { +TransformationType get_transformation_type(const Selection &selection) +{ + assert(selection.is_single_full_object() || selection.is_single_volume()); + return selection.is_single_volume() ? + TransformationType::Local_Relative_Independent : + TransformationType::Instance_Relative_Independent; // object +} +} // namespace + +bool GLGizmoSVG::on_mouse_for_rotation(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Moving()) return false; + + bool used = use_grabbers(mouse_event); + if (!m_dragging) return used; + + if (mouse_event.Dragging()) { + if (!m_rotate_start_angle.has_value()) + m_rotate_start_angle = m_angle.value_or(0.f); + double angle = m_rotate_gizmo.get_angle(); + angle -= PI / 2; // Grabber is upward + + double new_angle = angle + *m_rotate_start_angle; + // move to range <-M_PI, M_PI> + Geometry::to_range_pi_pi(new_angle); + + double z_rotation = m_volume->emboss_shape->fix_3mf_tr.has_value()? + (new_angle - m_angle.value_or(0.f)) : // relative angle + angle; // relativity is keep by selection cache + + Selection &selection = m_parent.get_selection(); + auto selection_rotate_fnc = [z_rotation, &selection]() { + selection.rotate(Vec3d(0., 0., z_rotation), get_transformation_type(selection)); + }; + selection_transform(selection, selection_rotate_fnc, m_volume); + + // propagate angle into property + m_angle = static_cast(new_angle); + + // do not store zero + if (is_approx(*m_angle, 0.f)) + m_angle.reset(); + } + return used; +} + +namespace{ +std::optional calculate_angle(const Selection& selection) +{ + const GLVolume *gl_volume = selection.get_first_volume(); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return {}; + + Transform3d to_world = gl_volume->world_matrix(); + const ModelVolume* volume = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume != nullptr); + assert(volume->emboss_shape.has_value()); + if (volume == nullptr || + !volume->emboss_shape.has_value() || + !volume->emboss_shape->fix_3mf_tr) + return calc_up(to_world, Slic3r::GUI::up_limit); + + // exist fix matrix and must be applied before calculation + to_world = to_world * volume->emboss_shape->fix_3mf_tr->inverse(); + return calc_up(to_world, Slic3r::GUI::up_limit); +} +} + +bool GLGizmoSVG::on_mouse_for_translate(const wxMouseEvent &mouse_event) +{ + // exist selected volume? + if (m_volume == nullptr) + return false; + + std::optional up_limit; + if (m_keep_up) + up_limit = Slic3r::GUI::up_limit; + const Camera &camera = wxGetApp().plater()->get_camera(); + + bool was_dragging = m_surface_drag.has_value(); + bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, up_limit); + 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(); + + // TODO: Remove it when it will be stable + // Distance should not change during dragging + const GLVolume *gl_volume = m_parent.get_selection().get_first_volume(); + m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + // Show correct value of height & depth inside of inputs + calculate_scale(); + } + + // Start with dragging + else if (!was_dragging && is_dragging) { + // Cancel job to prevent interuption of dragging (duplicit result) + if (m_job_cancel != nullptr) + m_job_cancel->store(true); + } + + // during drag + else if (was_dragging && is_dragging) { + // update scale of selected volume --> should be approx the same + calculate_scale(); + + // Recalculate angle for GUI + if (!m_keep_up) + m_angle = calculate_angle(m_parent.get_selection()); + } + return res; +} + +bool GLGizmoSVG::on_mouse(const wxMouseEvent &mouse_event) +{ + // not selected volume + if (m_volume == nullptr || + get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr || + !m_volume->emboss_shape.has_value()) return false; + + if (on_mouse_for_rotation(mouse_event)) return true; + if (on_mouse_for_translate(mouse_event)) return true; + + return false; +} + +bool GLGizmoSVG::wants_enter_leave_snapshots() const { return true; } +std::string GLGizmoSVG::get_gizmo_entering_text() const { return _u8L("Enter SVG gizmo"); } +std::string GLGizmoSVG::get_gizmo_leaving_text() const { return _u8L("Leave SVG gizmo"); } +std::string GLGizmoSVG::get_action_snapshot_name() const { return _u8L("SVG actions"); } + +bool GLGizmoSVG::on_init() +{ + m_rotate_gizmo.init(); + ColorRGBA gray_color(.6f, .6f, .6f, .3f); + m_rotate_gizmo.set_highlight_color(gray_color); + // Set rotation gizmo upwardrotate + m_rotate_gizmo.set_angle(PI / 2); + return true; +} + +std::string GLGizmoSVG::on_get_name() const { return _u8L("SVG"); } + +void GLGizmoSVG::on_render() { + // no volume selected + if (m_volume == nullptr || + get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr) + return; + + Selection &selection = m_parent.get_selection(); + if (selection.is_empty()) return; + + // prevent get local coordinate system on multi volumes + if (!selection.is_single_volume_or_modifier() && + !selection.is_single_volume_instance()) return; + bool is_surface_dragging = m_surface_drag.has_value(); + bool is_parent_dragging = m_parent.is_mouse_dragging(); + // Do NOT render rotation grabbers when dragging object + bool is_rotate_by_grabbers = m_dragging; + if (is_rotate_by_grabbers || + (!is_surface_dragging && !is_parent_dragging)) { + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + m_rotate_gizmo.render(); + } +} + +void GLGizmoSVG::on_register_raycasters_for_picking(){ + m_rotate_gizmo.register_raycasters_for_picking(); +} +void GLGizmoSVG::on_unregister_raycasters_for_picking(){ + m_rotate_gizmo.unregister_raycasters_for_picking(); +} + +namespace{ +IconManager::Icons init_icons(IconManager &mng, const GuiCfg &cfg) +{ + mng.release(); + + ImVec2 size(cfg.icon_width, cfg.icon_width); + // icon order has to match the enum IconType + IconManager::InitTypes init_types{ + {"undo.svg", size, IconManager::RasterType::white_only_data}, // reset_value + {"undo.svg", size, IconManager::RasterType::color}, // reset_value_hover + {"refresh.svg", size, IconManager::RasterType::white_only_data}, // refresh + {"refresh.svg", size, IconManager::RasterType::color}, // refresh_hovered + {"open.svg", size, IconManager::RasterType::color}, // changhe_file + {"burn.svg", size, IconManager::RasterType::color}, // bake + {"burn.svg", size, IconManager::RasterType::gray_only_data}, // bake_inactive + {"save.svg", size, IconManager::RasterType::color}, // save + {"exclamation.svg", size, IconManager::RasterType::color}, // exclamation + {"lock_closed.svg", size, IconManager::RasterType::white_only_data}, // lock + {"lock_open_f.svg", size, IconManager::RasterType::white_only_data}, // lock_hovered + {"lock_open.svg", size, IconManager::RasterType::white_only_data}, // unlock + {"lock_closed_f.svg",size, IconManager::RasterType::white_only_data}, // unlock_hovered + {"reflection_x.svg", size, IconManager::RasterType::white_only_data}, // reflection_x + {"reflection_x.svg", size, IconManager::RasterType::color}, // reflection_x_hovered + {"reflection_y.svg", size, IconManager::RasterType::white_only_data}, // reflection_y + {"reflection_y.svg", size, IconManager::RasterType::color}, // reflection_y_hovered + }; + + assert(init_types.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/icons/"; + for (IconManager::InitType &init_type : init_types) + init_type.filepath = path + init_type.filepath; + + return mng.init(init_types); + + //IconManager::VIcons vicons = mng.init(init_types); + // + //// flatten icons + //IconManager::Icons icons; + //icons.reserve(vicons.size()); + //for (IconManager::Icons &i : vicons) + // icons.push_back(i.front()); + //return icons; +} + +bool reset_button(const IconManager::Icons &icons) +{ + float reset_offset = ImGui::GetStyle().FramePadding.x; + ImGui::SameLine(reset_offset); + + // from GLGizmoCut + //std::string label_id = "neco"; + //std::string btn_label; + //btn_label += ImGui::RevertButton; + //return ImGui::Button((btn_label + "##" + label_id).c_str()); + + return clickable(get_icon(icons, IconType::reset_value), get_icon(icons, IconType::reset_value_hover)); +} + +} // namespace + +void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) +{ + set_volume_by_selection(); + + // Configuration creation + double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); + float main_toolbar_height = m_parent.get_main_toolbar_height(); + if (m_gui_cfg == nullptr || // Exist configuration - first run + m_gui_cfg->screen_scale != screen_scale || // change of DPI + m_gui_cfg->main_toolbar_height != main_toolbar_height // change size of view port + ) { + // Create cache for gui offsets + ::GuiCfg cfg = create_gui_configuration(); + cfg.screen_scale = screen_scale; + cfg.main_toolbar_height = main_toolbar_height; + + GuiCfg gui_cfg{std::move(cfg)}; + m_gui_cfg = std::make_unique(std::move(gui_cfg)); + + // set position near toolbar + m_set_window_offset = ImVec2(-1.f, -1.f); + + m_icons = init_icons(m_icon_manager, *m_gui_cfg); // need regeneration when change resolution(move between monitors) + } + + const ImVec2 &window_size = m_volume->is_the_only_one_part() ? + m_gui_cfg->window_size_for_object : m_gui_cfg->window_size; + ImGui::SetNextWindowSize(window_size); + // Draw origin position of text during dragging + if (m_surface_drag.has_value()) { + ImVec2 mouse_pos = ImGui::GetMousePos(); + ImVec2 center( + mouse_pos.x + m_surface_drag->mouse_offset.x(), + mouse_pos.y + m_surface_drag->mouse_offset.y()); + ImU32 color = ImGui::GetColorU32( + m_surface_drag->exist_hit ? + ImVec4(1.f, 1.f, 1.f, .75f) : // transparent white + ImVec4(1.f, .3f, .3f, .75f) + ); // Warning color + const float radius = 16.f; + ImGuiWrapper::draw_cross_hair(center, radius, color); + } + + // check if is set window offset + if (m_set_window_offset.has_value()) { + if (m_set_window_offset->y < 0) + // position near toolbar + m_set_window_offset = ImVec2(x, std::min(y, bottom_limit - window_size.y)); + + ImGui::SetNextWindowPos(*m_set_window_offset, ImGuiCond_Always); + m_set_window_offset.reset(); + } + + bool is_opened = true; + constexpr ImGuiWindowFlags flag = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + if (ImGui::Begin(on_get_name().c_str(), &is_opened, flag)) + draw_window(); + + ImGui::End(); + if (!is_opened) + close(); +} + +void GLGizmoSVG::on_set_state() +{ + // enable / disable bed from picking + // Rotation gizmo must work through bed + m_parent.set_raycaster_gizmos_on_top(GLGizmoBase::m_state == GLGizmoBase::On); + + m_rotate_gizmo.set_state(GLGizmoBase::m_state); + + // Closing gizmo. e.g. selecting another one + if (GLGizmoBase::m_state == GLGizmoBase::Off) { + reset_volume(); + } else if (GLGizmoBase::m_state == GLGizmoBase::On) { + // Try(when exist) set text configuration by volume + set_volume_by_selection(); + + m_set_window_offset = (m_gui_cfg != nullptr) ? + ImGuiWrapper::change_window_position(on_get_name().c_str(), false) : ImVec2(-1, -1); + } +} + +void GLGizmoSVG::data_changed(bool is_serializing) { + set_volume_by_selection(); + if (!is_serializing && m_volume == nullptr) + close(); +} + +void GLGizmoSVG::on_start_dragging() { m_rotate_gizmo.start_dragging(); } +void GLGizmoSVG::on_stop_dragging() +{ + m_rotate_gizmo.stop_dragging(); + + // TODO: when start second rotatiton previous rotation rotate draggers + // This is fast fix for second try to rotate + // When fixing, move grabber above text (not on side) + m_rotate_gizmo.set_angle(PI/2); + + // apply rotation + m_parent.do_rotate(L("Text-Rotate")); + + m_rotate_start_angle.reset(); + + // recalculate for surface cut + if (m_volume != nullptr && + m_volume->emboss_shape.has_value() && + m_volume->emboss_shape->projection.use_surface) + process(); +} +void GLGizmoSVG::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } + +#include "slic3r/GUI/BitmapCache.hpp" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/AABBTreeLines.hpp" // aabb lines for draw filled expolygon + +namespace{ +NSVGimage* init_image(EmbossShape::SvgFile &svg_file) { + // is already initialized? + if (svg_file.image.get() != nullptr) + return svg_file.image.get(); + + + if (svg_file.file_data == nullptr){ + // chech if path is known + if (svg_file.path.empty()) + return nullptr; + svg_file.file_data = read_from_disk(svg_file.path); + if (svg_file.file_data == nullptr) + return nullptr; + } + + // init svg image + svg_file.image = nsvgParse(*svg_file.file_data); + if (svg_file.image.get() == NULL) + return nullptr; + + // Disable stroke + //for (NSVGshape *shape = svg_file.image->shapes; shape != NULL; shape = shape->next) + // shape->stroke.type = 0; + + return svg_file.image.get(); +} + +// inspired by Xiaolin Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm +// Draw inner part of polygon CCW line as full brightness(edge of expolygon) +void wu_draw_line_side(Linef line, + const std::function& plot) { + auto ipart = [](float x) -> int {return static_cast(std::floor(x));}; + auto round = [](float x) -> float {return std::round(x);}; + auto fpart = [](float x) -> float {return x - std::floor(x);}; + auto rfpart = [=](float x) -> float {return 1 - fpart(x);}; + + Vec2d d = line.b - line.a; + const bool steep = abs(d.y()) > abs(d.x()); + bool is_full; // identify full brightness pixel + if (steep) { + is_full = d.y() >= 0; + std::swap(line.a.x(), line.a.y()); + std::swap(line.b.x(), line.b.y()); + std::swap(d.x(), d.y()); + }else + is_full = d.x() < 0; // opposit direction of y + + if (line.a.x() > line.b.x()) { + std::swap(line.a.x(), line.b.x()); + std::swap(line.a.y(), line.b.y()); + d *= -1; + } + const float gradient = (d.x() == 0) ? 1. : d.y() / d.x(); + + int xpx11; + float intery; + { + const float xend = round(line.a.x()); + const float yend = line.a.y() + gradient * (xend - line.a.x()); + const float xgap = rfpart(line.a.x() + 0.5f); + xpx11 = int(xend); + const int ypx11 = ipart(yend); + if (steep) { + plot(ypx11, xpx11, is_full? 1.f : (rfpart(yend) * xgap)); + plot(ypx11 + 1, xpx11, !is_full? 1.f : ( fpart(yend) * xgap)); + } else { + plot(xpx11, ypx11, is_full? 1.f : (rfpart(yend) * xgap)); + plot(xpx11, ypx11 + 1,!is_full? 1.f : ( fpart(yend) * xgap)); + } + intery = yend + gradient; + } + + int xpx12; + { + const float xend = round(line.b.x()); + const float yend = line.b.y() + gradient * (xend - line.b.x()); + const float xgap = rfpart(line.b.x() + 0.5f); + xpx12 = int(xend); + const int ypx12 = ipart(yend); + if (steep) { + plot(ypx12, xpx12, is_full? 1.f : (rfpart(yend) * xgap)); + plot(ypx12 + 1, xpx12, !is_full? 1.f : ( fpart(yend) * xgap)); + } else { + plot(xpx12, ypx12, is_full? 1.f : (rfpart(yend) * xgap)); + plot(xpx12, ypx12 + 1, !is_full? 1.f : ( fpart(yend) * xgap)); + } + } + + if (steep) { + if (is_full){ + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, 1.f); + plot(ipart(intery) + 1, x, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, rfpart(intery)); + plot(ipart(intery) + 1, x, 1.f ); + intery += gradient; + } + } + } else { + if (is_full){ + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), 1.f); + plot(x, ipart(intery) + 1, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), rfpart(intery)); + plot(x, ipart(intery) + 1, 1.f); + intery += gradient; + } + } + } +} + +// Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm +void wu_draw_line(Linef line, + const std::function& plot) { + auto ipart = [](float x) -> int {return int(std::floor(x));}; + auto round = [](float x) -> float {return std::round(x);}; + auto fpart = [](float x) -> float {return x - std::floor(x);}; + auto rfpart = [=](float x) -> float {return 1 - fpart(x);}; + + Vec2d d = line.b - line.a; + const bool steep = abs(d.y()) > abs(d.x()); + if (steep) { + std::swap(line.a.x(), line.a.y()); + std::swap(line.b.x(), line.b.y()); + } + if (line.a.x() > line.b.x()) { + std::swap(line.a.x(), line.b.x()); + std::swap(line.a.y(), line.b.y()); + } + d = line.b - line.a; + const float gradient = (d.x() == 0) ? 1 : d.y() / d.x(); + + int xpx11; + float intery; + { + const float xend = round(line.a.x()); + const float yend = line.a.y() + gradient * (xend - line.a.x()); + const float xgap = rfpart(line.a.x() + 0.5); + xpx11 = int(xend); + const int ypx11 = ipart(yend); + if (steep) { + plot(ypx11, xpx11, rfpart(yend) * xgap); + plot(ypx11 + 1, xpx11, fpart(yend) * xgap); + } else { + plot(xpx11, ypx11, rfpart(yend) * xgap); + plot(xpx11, ypx11 + 1, fpart(yend) * xgap); + } + intery = yend + gradient; + } + + int xpx12; + { + const float xend = round(line.b.x()); + const float yend = line.b.y() + gradient * (xend - line.b.x()); + const float xgap = rfpart(line.b.x() + 0.5); + xpx12 = int(xend); + const int ypx12 = ipart(yend); + if (steep) { + plot(ypx12, xpx12, rfpart(yend) * xgap); + plot(ypx12 + 1, xpx12, fpart(yend) * xgap); + } else { + plot(xpx12, ypx12, rfpart(yend) * xgap); + plot(xpx12, ypx12 + 1, fpart(yend) * xgap); + } + } + + if (steep) { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, rfpart(intery)); + plot(ipart(intery) + 1, x, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), rfpart(intery)); + plot(x, ipart(intery) + 1, fpart(intery)); + intery += gradient; + } + } +} + +template // N .. count of channels per pixel +void draw_side_outline(const ExPolygons &shape, const std::array &color, std::vector &data, size_t data_width, double scale) +{ + int count_lines = data.size() / (N * data_width); + size_t data_line = N * data_width; + auto get_offset = [count_lines, data_line](int x, int y) { + // NOTE: y has opposit direction in texture + return (count_lines - y - 1) * data_line + x * N; + }; + + // overlap color + auto draw = [&data, data_width, count_lines, get_offset, &color](int x, int y, float brightess) { + if (x < 0 || y < 0 || x >= data_width || y >= count_lines) + return; // out of image + size_t offset = get_offset(x, y); + bool change_color = false; + for (size_t i = 0; i < N - 1; ++i) { + if(data[offset + i] != color[i]){ + data[offset + i] = color[i]; + change_color = true; + } + } + + unsigned char &alpha = data[offset + N - 1]; + if (alpha == 0 || change_color){ + alpha = static_cast(std::round(brightess * 255)); + } else if (alpha != 255){ + alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); + } + }; + + BoundingBox bb_unscaled = get_extents(shape); + Linesf lines = to_linesf(shape); + BoundingBoxf bb(bb_unscaled.min.cast(), bb_unscaled.max.cast()); + + // scale lines to pixels + if (!is_approx(scale, 1.)) { + for (Linef &line : lines) { + line.a *= scale; + line.b *= scale; + } + bb.min *= scale; + bb.max *= scale; + } + + for (const Linef &line : lines) + wu_draw_line_side(line, draw); +} + +/// +/// Draw filled ExPolygon into data +/// line by line inspired by: http://alienryderflex.com/polygon_fill/ +/// +/// Count channels for one pixel(RGBA = 4) +/// Shape to draw +/// Color of shape contain count of channels(N) +/// Image(2d) stored in 1d array +/// Count of pixel on one line(size in data = N x data_width) +/// Shape scale for conversion to pixels +template // N .. count of channels per pixel +void draw_filled(const ExPolygons &shape, const std::array& color, std::vector &data, size_t data_width, double scale){ + assert(data.size() % N == 0); + assert(data.size() % data_width == 0); + assert((data.size() % (N*data_width)) == 0); + + BoundingBox bb_unscaled = get_extents(shape); + + Linesf lines = to_linesf(shape); + BoundingBoxf bb( + bb_unscaled.min.cast(), + bb_unscaled.max.cast()); + + // scale lines to pixels + if (!is_approx(scale, 1.)) { + for (Linef &line : lines) { + line.a *= scale; + line.b *= scale; + } + bb.min *= scale; + bb.max *= scale; + } + + int count_lines = data.size() / (N * data_width); + size_t data_line = N * data_width; + auto get_offset = [count_lines, data_line](int x, int y) { + // NOTE: y has opposit direction in texture + return (count_lines - y - 1) * data_line + x * N; + }; + auto set_color = [&data, &color, get_offset](int x, int y) { + size_t offset = get_offset(x, y); + if (data[offset + N - 1] != 0) + return; // already setted by line + for (size_t i = 0; i < N; ++i) + data[offset + i] = color[i]; + }; + + // anti aliased drawing of lines + auto draw = [&data, data_width, count_lines, get_offset, &color](int x, int y, float brightess) { + if (x < 0 || y < 0 || x >= data_width || y >= count_lines) + return; // out of image + size_t offset = get_offset(x, y); + unsigned char &alpha = data[offset + N - 1]; + if (alpha == 0){ + alpha = static_cast(std::round(brightess * 255)); + for (size_t i = 0; i < N-1; ++i) + data[offset + i] = color[i]; + } else if (alpha != 255){ + alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); + } + }; + + for (const Linef& line: lines) + wu_draw_line_side(line, draw); + + auto tree = Slic3r::AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + // range for intersection line + double x1 = bb.min.x() - 1.f; + double x2 = bb.max.x() + 1.f; + + int max_y = std::min(count_lines, static_cast(std::round(bb.max.y()))); + for (int y = std::max(0, static_cast(std::round(bb.min.y()))); y < max_y; ++y){ + double y_f = y + .5; // 0.5 ... intersection in center of pixel of pixel + Linef line(Vec2d(x1, y_f), Vec2d(x2, y_f)); + using Intersection = std::pair; + using Intersections = std::vector; + // sorted .. false + // + Intersections intersections = Slic3r::AABBTreeLines::get_intersections_with_line(lines, tree, line); + if (intersections.empty()) + continue; + + assert((intersections.size() % 2) == 0); + + // sort intersections by x + std::sort(intersections.begin(), intersections.end(), + [](const Intersection &i1, const Intersection &i2) { return i1.first.x() < i2.first.x(); }); + + // draw lines + for (size_t i = 0; i < intersections.size(); i+=2) { + const Vec2d& p2 = intersections[i+1].first; + if (p2.x() < 0) + continue; // out of data + + const Vec2d& p1 = intersections[i].first; + if (p1.x() > data_width) + break; // out of data + + // clamp to data + int max_x = std::min(static_cast(data_width-1), static_cast(std::round(p2.x()))); + for (int x = std::max(0, static_cast(std::round(p1.x()))); x <= max_x; ++x) + set_color(x, y); + } + } +} + +// init texture by draw expolygons into texture +bool init_texture(Texture &texture, const ExPolygonsWithIds& shapes_with_ids, unsigned max_size_px, const std::vector& shape_warnings){ + BoundingBox bb = get_extents(shapes_with_ids); + Point bb_size = bb.size(); + double bb_width = bb_size.x(); // [in mm] + double bb_height = bb_size.y(); // [in mm] + + bool is_widder = bb_size.x() > bb_size.y(); + double scale = 0.f; + if (is_widder) { + scale = max_size_px / bb_width; + texture.width = max_size_px; + texture.height = static_cast(std::ceil(bb_height * scale)); + } else { + scale = max_size_px / bb_height; + texture.width = static_cast(std::ceil(bb_width * scale)); + texture.height = max_size_px; + } + const int n_pixels = texture.width * texture.height; + if (n_pixels <= 0) + return false; + + constexpr int channels_count = 4; + std::vector data(n_pixels * channels_count, {0}); + + // Union All shapes + ExPolygons shape; + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) + expolygons_append(shape, shapes_with_id.expoly); + shape = union_ex(shape); + + // align to texture + translate(shape, -bb.min); + size_t texture_width = static_cast(texture.width); + unsigned char alpha = 255; // without transparency + std::array color_shape{201, 201, 201, alpha}; // from degin by @JosefZachar + std::array color_error{237, 28, 36, alpha}; // from icon: resources/icons/flag_red.svg + std::array color_warning{237, 107, 33, alpha}; // icons orange + // draw unhealedable shape + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) + if (!shapes_with_id.is_healed) { + ExPolygons bad_shape = shapes_with_id.expoly; // copy + translate(bad_shape, -bb.min); // align to texture + draw_side_outline<4>(bad_shape, color_error, data, texture_width, scale); + } + // Draw shape with warning + if (!shape_warnings.empty()) { + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids){ + assert(shapes_with_id.id < shape_warnings.size()); + if (shapes_with_id.id >= shape_warnings.size()) + continue; + if (shape_warnings[shapes_with_id.id].empty()) + continue; // no warnings for shape + ExPolygons warn_shape = shapes_with_id.expoly; // copy + translate(warn_shape, -bb.min); // align to texture + draw_side_outline<4>(warn_shape, color_warning, data, texture_width, scale); + } + } + + // Draw rest of shape + draw_filled<4>(shape, color_shape, data, texture_width, scale); + + // sends data to gpu + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + if (texture.id != 0) + glsafe(::glDeleteTextures(1, &texture.id)); + glsafe(::glGenTextures(1, &texture.id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, texture.id)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) texture.width, (GLsizei) texture.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, + (const void *) data.data())); + + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + + GLuint NO_TEXTURE_ID = 0; + glsafe(::glBindTexture(GL_TEXTURE_2D, NO_TEXTURE_ID)); + return true; +} + +bool is_closed(NSVGpath *path){ + for (; path != NULL; path = path->next) + if (path->next == NULL && path->closed) + return true; + return false; +} + +void add_comma_separated(std::string &result, const std::string &add){ + if (!result.empty()) + result += ", "; + result += add; +} + +const float warning_preccission = 1e-4f; +std::string create_fill_warning(const NSVGshape &shape) { + if (!(shape.flags & NSVG_FLAGS_VISIBLE) || + shape.fill.type == NSVG_PAINT_NONE) + return {}; // not visible + + std::string warning; + if ((shape.opacity - 1.f + warning_preccission) <= 0.f) + add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); + + // if(shape->flags != NSVG_FLAGS_VISIBLE) add_warning(_u8L("Visibility flag")); + bool is_fill_gradient = shape.fillGradient[0] != '\0'; + if (is_fill_gradient) + add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.fillGradient)); + + switch (shape.fill.type) { + case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined fill type")); break; + case NSVG_PAINT_LINEAR_GRADIENT: + if (!is_fill_gradient) + add_comma_separated(warning, _u8L("Linear gradient")); + break; + case NSVG_PAINT_RADIAL_GRADIENT: + if (!is_fill_gradient) + add_comma_separated(warning, _u8L("Radial gradient")); + break; + // case NSVG_PAINT_NONE: + // case NSVG_PAINT_COLOR: + // default: break; + } + + // Unfilled is only line which could be opened + if (shape.fill.type != NSVG_PAINT_NONE && !is_closed(shape.paths)) + add_comma_separated(warning, _u8L("Open filled path")); + return warning; +} + +std::string create_stroke_warning(const NSVGshape &shape) { + + std::string warning; + if (!(shape.flags & NSVG_FLAGS_VISIBLE) || + shape.stroke.type == NSVG_PAINT_NONE || + shape.strokeWidth <= 1e-5f) + return {}; // not visible + + if ((shape.opacity - 1.f + warning_preccission) <= 0.f) + add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); + + bool is_stroke_gradient = shape.strokeGradient[0] != '\0'; + if (is_stroke_gradient) + add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.strokeGradient)); + + switch (shape.stroke.type) { + case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined stroke type")); break; + case NSVG_PAINT_LINEAR_GRADIENT: + if (!is_stroke_gradient) + add_comma_separated(warning, _u8L("Linear gradient")); + break; + case NSVG_PAINT_RADIAL_GRADIENT: + if (!is_stroke_gradient) + add_comma_separated(warning, _u8L("Radial gradient")); + break; + // case NSVG_PAINT_COLOR: + // case NSVG_PAINT_NONE: + // default: break; + } + + return warning; +} + +/// +/// Create warnings about shape +/// +/// Input svg loaded to shapes +/// Vector of warnings with same size as EmbossShape::shapes_with_ids +/// or Empty when no warnings -> for fast checking that every thing is all right(more common case) +std::vector create_shape_warnings(const EmbossShape &shape, float scale){ + assert(shape.svg_file.image != nullptr); + if (shape.svg_file.image == nullptr) + return {std::string{"Uninitialized SVG image"}}; + + const NSVGimage &image = *shape.svg_file.image; + std::vector result; + auto add_warning = [&result, &image](size_t index, const std::string &message) { + if (result.empty()) + result = std::vector(get_shapes_count(image) * 2); + std::string &res = result[index]; + if (res.empty()) + res = message; + else + res += '\n' + message; + }; + + if (!shape.is_healed) { + for (const ExPolygonsWithId &i : shape.shapes_with_ids) + if (!i.is_healed) + add_warning(i.id, _u8L("Path can't be healed from selfintersection and multiple points.")); + + // This waning is not connected to NSVGshape. It is about union of paths, but Zero index is shown first + size_t index = 0; + add_warning(index, _u8L("Final shape constains selfintersection or multiple points with same coordinate")); + } + + size_t shape_index = 0; + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next, ++shape_index) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)){ + add_warning(shape_index * 2, GUI::format(_L("Shape is marked as invisible (%1%)"), shape->id)); + continue; + } + + std::string fill_warning = create_fill_warning(*shape); + if (!fill_warning.empty()) + add_warning(shape_index * 2, GUI::format(_L("Fill of shape (%1%) contains unsupported: %2% "), shape->id, fill_warning)); + + float minimal_width_in_mm = 1e-3f; + if (shape->strokeWidth <= minimal_width_in_mm * scale) { + add_warning(shape_index * 2, GUI::format(_L("Stroke of shape (%1%) is too thin (minimal width is %2% mm)"), shape->id, minimal_width_in_mm)); + continue; + } + std::string stroke_warning = create_stroke_warning(*shape); + if (!stroke_warning.empty()) + add_warning(shape_index * 2 + 1, GUI::format(_L("Stroke of shape (%1%) contains unsupported: %2% "), shape->id, stroke_warning)); + } + return result; +} + + +} // namespace + +void GLGizmoSVG::set_volume_by_selection() +{ + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = get_selected_gl_volume(selection); + if (gl_volume == nullptr) + return reset_volume(); + + const ModelObjectPtrs &objects = selection.get_model()->objects; + ModelVolume *volume =get_model_volume(*gl_volume, objects); + if (volume == nullptr) + return reset_volume(); + + // is same volume as actual selected? + if (volume->id() == m_volume_id) + return; + + // Do not use focused input value when switch volume(it must swith value) + if (m_volume != nullptr && + m_volume != volume) // when update volume it changed id BUT not pointer + ImGuiWrapper::left_inputs(); + + // is valid svg volume? + if (!is_svg(*volume)) + return reset_volume(); + + // cancel previous job + if (m_job_cancel != nullptr) { + m_job_cancel->store(true); + m_job_cancel = nullptr; + } + + // calculate scale for height and depth inside of scaled object instance + calculate_scale(); // must be before calculation of tesselation + + EmbossShape &es = *volume->emboss_shape; + EmbossShape::SvgFile &svg_file = es.svg_file; + if (svg_file.image == nullptr) { + if (init_image(svg_file) == nullptr) + return reset_volume(); + } + assert(svg_file.image != nullptr); + assert(svg_file.image.get() != nullptr); + const NSVGimage &image = *svg_file.image; + ExPolygonsWithIds &shape_ids = es.shapes_with_ids; + if (shape_ids.empty()) { + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; + shape_ids = create_shape_with_ids(image, params); + } + + reset_volume(); // clear cached data + + m_volume = volume; + m_volume_id = volume->id(); + m_volume_shape = es; // copy + m_shape_warnings = create_shape_warnings(es, get_scale_for_tolerance()); + + // Calculate current angle of up vector + m_angle = calculate_angle(selection); + m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + m_shape_bb = get_extents(m_volume_shape.shapes_with_ids); +} +namespace { +void delete_texture(Texture& texture){ + if (texture.id != 0) { + glsafe(::glDeleteTextures(1, &texture.id)); + texture.id = 0; + } +} +} +void GLGizmoSVG::reset_volume() +{ + if (m_volume == nullptr) + return; // already reseted + + m_volume = nullptr; + m_volume_id.id = 0; + m_volume_shape.shapes_with_ids.clear(); + m_filename_preview.clear(); + m_shape_warnings.clear(); + // delete texture after finish imgui draw + wxGetApp().plater()->CallAfter([&texture = m_texture]() { delete_texture(texture); }); +} + +void GLGizmoSVG::calculate_scale() { + // be carefull m_volume is not set yet + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) + return; + + Transform3d to_world = gl_volume->world_matrix(); + + const ModelVolume *volume_ptr = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume_ptr != nullptr); + assert(volume_ptr->emboss_shape.has_value()); + // Fix for volume loaded from 3mf + if (volume_ptr != nullptr && + volume_ptr->emboss_shape.has_value()) { + const std::optional &fix_tr = volume_ptr->emboss_shape->fix_3mf_tr; + if (fix_tr.has_value()) + to_world = to_world * (fix_tr->inverse()); + } + + auto to_world_linear = to_world.linear(); + auto calc = [&to_world_linear](const Vec3d &axe, std::optional& scale) { + Vec3d axe_world = to_world_linear * axe; + double norm_sq = axe_world.squaredNorm(); + if (is_approx(norm_sq, 1.)) { + if (!scale.has_value()) + return; + scale.reset(); + } else { + scale = sqrt(norm_sq); + } + }; + + calc(Vec3d::UnitX(), m_scale_width); + calc(Vec3d::UnitY(), m_scale_height); + calc(Vec3d::UnitZ(), m_scale_depth); +} + +float GLGizmoSVG::get_scale_for_tolerance(){ + return std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)); } + +bool GLGizmoSVG::process() +{ + // no volume is selected -> selection from right panel + assert(m_volume != nullptr); + if (m_volume == nullptr) + return false; + + assert(m_volume->emboss_shape.has_value()); + if (!m_volume->emboss_shape.has_value()) + return false; + + // 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 + if (m_job_cancel != nullptr) + m_job_cancel->store(true); + // create new shared ptr to cancel new job + m_job_cancel = std::make_shared>(false); + + EmbossShape shape = m_volume_shape; // copy + auto base = std::make_unique(m_volume->name, m_job_cancel, std::move(shape)); + base->is_outside = m_volume->type() == ModelVolumeType::MODEL_PART; + DataUpdate data{std::move(base), m_volume_id}; + return start_update_volume(std::move(data), *m_volume, m_parent.get_selection(), m_raycast_manager); +} + +void GLGizmoSVG::close() +{ + // close gizmo == open it again + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); + reset_volume(); +} + +void GLGizmoSVG::draw_window() +{ + assert(m_volume != nullptr); + assert(m_volume_id.valid()); + if (m_volume == nullptr || + m_volume_id.invalid()) { + ImGui::Text("Not valid state please report reproduction steps on github"); + return; + } + + assert(m_volume->emboss_shape.has_value()); + if (!m_volume->emboss_shape.has_value()) { + ImGui::Text("No embossed file"); + return; + } + + draw_preview(); + draw_filename(); + + // Is SVG baked? + if (m_volume == nullptr) return; + + ImGui::Separator(); + + ImGui::Indent(m_gui_cfg->icon_width); + draw_depth(); + draw_size(); + draw_use_surface(); + + draw_distance(); + draw_rotation(); + draw_mirroring(); + + /* + if (ImGui::Button(_u8L("Face the camera").c_str())) { + const Camera &cam = wxGetApp().plater()->get_camera(); + if (face_selected_volume_to_camera(cam, m_parent) && + m_volume->emboss_shape->projection.use_surface) + process(); + } + */ + + ImGui::Unindent(m_gui_cfg->icon_width); + + if (!m_volume->is_the_only_one_part()) { + ImGui::Separator(); + draw_model_type(); + } +} +namespace { +void draw(const ExPolygonsWithIds& shapes_with_ids, unsigned max_size) +{ + ImVec2 actual_pos = ImGui::GetCursorPos(); + // draw shapes + BoundingBox bb; + for (const ExPolygonsWithId &shape : shapes_with_ids) + bb.merge(get_extents(shape.expoly)); + + Point bb_size = bb.size(); + double scale = max_size / (double) std::max(bb_size.x(), bb_size.y()); + ImVec2 win_offset = ImGui::GetWindowPos(); + Point offset(win_offset.x + actual_pos.x, win_offset.y + actual_pos.y); + offset += bb_size / 2 * scale; + auto draw_polygon = [&scale, offset](Slic3r::Polygon p) { + p.scale(scale, -scale); // Y mirror + p.translate(offset); + ImGuiWrapper::draw(p); + }; + + for (const ExPolygonsWithId &shape : shapes_with_ids) { + for (const ExPolygon &expoly : shape.expoly) { + draw_polygon(expoly.contour); + for (const Slic3r::Polygon &hole : expoly.holes) + draw_polygon(hole); + } + } +} +} + +void GLGizmoSVG::draw_preview(){ + // init texture when not initialized yet. + // drag&drop is out of rendering scope so texture must be created on this place + if (m_texture.id == 0) { + const ExPolygonsWithIds &shapes = m_volume->emboss_shape->shapes_with_ids; + init_texture(m_texture, shapes, m_gui_cfg->texture_max_size_px, m_shape_warnings); + } + + //::draw(m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px); + + if (m_texture.id != 0) { + ImTextureID id = (void *) static_cast(m_texture.id); + ImVec2 s(m_texture.width, m_texture.height); + + std::optional spacing; + // is texture over full height? + if (m_texture.height != m_gui_cfg->texture_max_size_px) { + spacing = (m_gui_cfg->texture_max_size_px - m_texture.height) / 2.f; + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); + } + // is texture over full width? + unsigned window_width = static_cast( + ImGui::GetWindowSize().x - 2*ImGui::GetStyle().WindowPadding.x); + if (window_width > m_texture.width){ + float space = (window_width - m_texture.width) / 2.f; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + space); + } + + ImGui::Image(id, s); + //if(ImGui::IsItemHovered()){ + // const EmbossShape &es = *m_volume->emboss_shape; + // size_t count_of_shapes = get_shapes_count(*es.svg_file.image); + // size_t count_of_expolygons = 0; + // size_t count_of_points = 0; + // for (const auto &shape : es.shapes_with_ids) { + // for (const ExPolygon &expoly : shape.expoly){ + // ++count_of_expolygons; + // count_of_points += count_points(expoly); + // } + // } + // // Do not translate it is only for debug + // std::string tooltip = GUI::format("%1% shapes, which create %2% polygons with %3% line segments", + // count_of_shapes, count_of_expolygons, count_of_points); + // ImGui::SetTooltip("%s", tooltip.c_str()); + //} + + if (spacing.has_value()) + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); + } +} + +void GLGizmoSVG::draw_filename(){ + const EmbossShape &es = *m_volume->emboss_shape; + if (m_filename_preview.empty()){ + // create filename preview + if (!es.svg_file.path.empty()) { + m_filename_preview = get_file_name(es.svg_file.path); + } else if (!es.svg_file.path_in_3mf.empty()) { + m_filename_preview = get_file_name(es.svg_file.path_in_3mf); + } + + if (m_filename_preview.empty()){ + assert(false); + m_filename_preview = "unknown"; + } else { + m_filename_preview = ImGuiWrapper::trunc(m_filename_preview, m_gui_cfg->input_width); + } + } + + if (!m_shape_warnings.empty()){ + draw(get_icon(m_icons, IconType::exclamation)); + if (ImGui::IsItemHovered()) { + std::string tooltip; + for (const std::string &w: m_shape_warnings){ + if (w.empty()) + continue; + if (!tooltip.empty()) + tooltip += "\n"; + tooltip += w; + } + ImGui::SetTooltip("%s", tooltip.c_str()); + } + ImGui::SameLine(); + } + + // Remove space between filename and gray suffix ".svg" + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::Text("%s", m_filename_preview.c_str()); + bool is_hovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_LIGHT, ".svg"); + ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + + is_hovered |= ImGui::IsItemHovered(); + if (is_hovered) { + std::string tooltip = GUI::format(_L("SVG file path is \"%1%\""), es.svg_file.path); + ImGui::SetTooltip("%s", tooltip.c_str()); + } + + bool file_changed = false; + + // Re-Load button + bool can_reload = !m_volume_shape.svg_file.path.empty(); + if (can_reload) { + ImGui::SameLine(); + if (clickable(get_icon(m_icons, IconType::refresh), get_icon(m_icons, IconType::refresh_hover))) { + if (!boost::filesystem::exists(m_volume_shape.svg_file.path)) { + m_volume_shape.svg_file.path.clear(); + } else { + file_changed = true; + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reload SVG file from disk.").c_str()); + } + + ImGuiComboFlags flags = ImGuiComboFlags_PopupAlignLeft | ImGuiComboFlags_NoPreview; + ImGui::SameLine(); + if (ImGui::BeginCombo("##file_options", nullptr, flags)) { + ScopeGuard combo_sg([]() { ImGui::EndCombo(); }); + + draw(get_icon(m_icons, IconType::change_file)); + ImGui::SameLine(); + if (ImGui::Selectable((_L("Change file") + dots).ToUTF8().data())) { + std::string new_path = choose_svg_file(); + if (!new_path.empty()) { + file_changed = true; + m_volume_shape.svg_file = {}; // clear data + m_volume_shape.svg_file.path = new_path; + } + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Change to another .svg file").c_str()); + } + + std::string forget_path = _u8L("Forget the file path"); + if (m_volume->emboss_shape->svg_file.path.empty()){ + draw(get_icon(m_icons, IconType::bake_inactive)); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, forget_path.c_str()); + } else { + draw(get_icon(m_icons, IconType::bake)); + ImGui::SameLine(); + if (ImGui::Selectable(forget_path.c_str())) { + // set .svg_file.path_in_3mf to remember file name + m_volume->emboss_shape->svg_file.path.clear(); + m_volume_shape.svg_file.path.clear(); + m_filename_preview.clear(); + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Do NOT save local path to 3MF file.\n Also disables 'reload from disk' option.").c_str()); + } + } + + //draw(get_icon(m_icons, IconType::bake)); + //ImGui::SameLine(); + //if (ImGui::Selectable(_u8L("Bake 2 ©").c_str())) { + // EmbossShape::SvgFile &svg = m_volume_shape.svg_file; + // std::stringstream ss; + // Slic3r::save(*svg.image, ss); + // svg.file_data = std::make_unique(ss.str()); + // svg.image = nsvgParse(*svg.file_data); + // assert(svg.image.get() != NULL); + // if (svg.image.get() != NULL) { + // m_volume->emboss_shape->svg_file = svg; // copy - write changes into volume + // } else { + // svg = m_volume->emboss_shape->svg_file; // revert changes + // } + //} else if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("%s", _u8L("Use only paths from svg - recreate svg").c_str()); + //} + + draw(get_icon(m_icons, IconType::bake)); + ImGui::SameLine(); + // TRN: An menu option to convert the SVG into an unmodifiable model part. + if (ImGui::Selectable(_u8L("Bake").c_str())) { + m_volume->emboss_shape.reset(); + close(); + } else if (ImGui::IsItemHovered()) { + // TRN: Tooltip for the menu item. + ImGui::SetTooltip("%s", _u8L("Bake into model as uneditable part").c_str()); + } + + draw(get_icon(m_icons, IconType::save)); + ImGui::SameLine(); + if (ImGui::Selectable((_L("Save as") + dots).ToUTF8().data())) { + wxWindow *parent = nullptr; + GUI::FileType file_type = FT_SVG; + wxString wildcard = file_wildcards(file_type); + wxString dlg_title = _L("Save SVG file"); + const EmbossShape::SvgFile& svg = m_volume_shape.svg_file; + wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; + wxFileDialog dlg(parent, dlg_title, last_used_directory, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (dlg.ShowModal() == wxID_OK ){ + last_used_directory = dlg.GetDirectory(); + wxString out_path = dlg.GetPath(); + std::string path{out_path.c_str()}; + //Slic3r::save(*m_volume_shape.svg_file.image, path); + + std::ofstream stream(path); + if (stream.is_open()){ + stream << *svg.file_data; + + // change source file + m_filename_preview.clear(); + m_volume_shape.svg_file.path = path; + m_volume_shape.svg_file.path_in_3mf.clear(); // possible change name + m_volume->emboss_shape->svg_file = m_volume_shape.svg_file; // copy - write changes into volume + } else { + BOOST_LOG_TRIVIAL(error) << "Opening file: \"" << path << "\" Failed"; + } + + } + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Save as '.svg' file").c_str()); + } + + //draw(get_icon(m_icons, IconType::save)); + //ImGui::SameLine(); + //if (ImGui::Selectable((_L("Save used as") + dots).ToUTF8().data())) { + // GUI::FileType file_type = FT_SVG; + // wxString wildcard = file_wildcards(file_type); + // wxString dlg_title = _L("Export SVG file:"); + // wxString dlg_dir = from_u8(wxGetApp().app_config->get_last_dir()); + // const EmbossShape::SvgFile& svg = m_volume_shape.svg_file; + // wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; + // wxFileDialog dlg(nullptr, dlg_title, dlg_dir, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + // if (dlg.ShowModal() == wxID_OK ){ + // wxString out_path = dlg.GetPath(); + // std::string path{out_path.c_str()}; + // Slic3r::save(*m_volume_shape.svg_file.image, path); + // } + //} else if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("%s", _u8L("Save only used path as '.svg' file").c_str()); + //} + } + + if (file_changed) { + float scale = get_scale_for_tolerance(); + double tes_tol = get_tesselation_tolerance(scale); + EmbossShape es_ = select_shape(m_volume_shape.svg_file.path, tes_tol); + m_volume_shape.svg_file = std::move(es_.svg_file); + m_volume_shape.shapes_with_ids = std::move(es_.shapes_with_ids); + m_shape_warnings = create_shape_warnings(m_volume_shape, scale); + init_texture(m_texture, m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px, m_shape_warnings); + process(); + } +} + +void GLGizmoSVG::draw_depth() +{ + ImGuiWrapper::text(m_gui_cfg->translations.depth); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + double &value = m_volume_shape.projection.depth; + constexpr double step = 1.; + constexpr double step_fast = 10.; + std::optional result_scale; + const char *size_format = "%.1f mm"; + double input = value; + if (use_inch) { + size_format = "%.2f in"; + // input in inches + input *= ObjectManipulation::mm_to_in * m_scale_depth.value_or(1.f); + result_scale = ObjectManipulation::in_to_mm / m_scale_depth.value_or(1.f); + } else if (m_scale_depth.has_value()) { + // scale input + input *= (*m_scale_depth); + result_scale = 1. / (*m_scale_depth); + } + + if (ImGui::InputDouble("##depth", &input, step, step_fast, size_format)) { + if (result_scale.has_value()) + input *= (*result_scale); + apply(input, limits.depth); + if (!is_approx(input, value, 1e-4)){ + value = input; + process(); + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Size in emboss direction.").c_str()); +} + +void GLGizmoSVG::draw_size() +{ + ImGuiWrapper::text(m_gui_cfg->translations.size); + if (ImGui::IsItemHovered()){ + size_t count_points = 0; + for (const auto &s : m_volume_shape.shapes_with_ids) + count_points += Slic3r::count_points(s.expoly); + // TRN: The placeholder contains a number. + ImGui::SetTooltip("%s", GUI::format(_L("Scale also changes amount of curve samples (%1%)"), count_points).c_str()); + } + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + + Point size = m_shape_bb.size(); + double width = size.x() * m_volume_shape.scale * m_scale_width.value_or(1.f); + if (use_inch) width *= ObjectManipulation::mm_to_in; + double height = size.y() * m_volume_shape.scale * m_scale_height.value_or(1.f); + if (use_inch) height *= ObjectManipulation::mm_to_in; + + const auto is_valid_scale_ratio = [limit = &limits.relative_scale_ratio](double ratio) { + if (std::fabs(ratio - 1.) < limit->min) + return false; // too small ratio --> without effect + + if (ratio > limit->max) + return false; + + if (ratio < 1e-4) + return false; // negative scale is not allowed + + return true; + }; + + std::optional new_relative_scale; + if (m_keep_ratio) { + std::stringstream ss; + ss << std::setprecision(2) << std::fixed << width << " x " << height << " " << (use_inch ? "in" : "mm"); + + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + const MinMax &minmax = use_inch ? limits.ui_size_in : limits.ui_size; + // convert to float for slider + float width_f = static_cast(width); + if (m_imgui->slider_float("##width_size_slider", &width_f, minmax.min, minmax.max, ss.str().c_str(), 1.f, false)) { + double width_ratio = width_f / width; + if (is_valid_scale_ratio(width_ratio)) { + m_scale_width = m_scale_width.value_or(1.f) * width_ratio; + m_scale_height = m_scale_height.value_or(1.f) * width_ratio; + new_relative_scale = Vec3d(width_ratio, width_ratio, 1.); + } + } + } else { + ImGuiInputTextFlags flags = 0; + + float space = m_gui_cfg->icon_width / 2; + float input_width = m_gui_cfg->input_width / 2 - space / 2; + float second_offset = m_gui_cfg->input_offset + input_width + space; + + const char *size_format = (use_inch) ? "%.2f in" : "%.1f mm"; + double step = -1.0; + double fast_step = -1.0; + + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(input_width); + double prev_width = width; + if (ImGui::InputDouble("##width", &width, step, fast_step, size_format, flags)) { + double width_ratio = width / prev_width; + if (is_valid_scale_ratio(width_ratio)) { + m_scale_width = m_scale_width.value_or(1.f) * width_ratio; + new_relative_scale = Vec3d(width_ratio, 1., 1.); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Width of SVG.").c_str()); + + ImGui::SameLine(second_offset); + ImGui::SetNextItemWidth(input_width); + double prev_height = height; + if (ImGui::InputDouble("##height", &height, step, fast_step, size_format, flags)) { + double height_ratio = height / prev_height; + if (is_valid_scale_ratio(height_ratio)) { + m_scale_height = m_scale_height.value_or(1.f) * height_ratio; + new_relative_scale = Vec3d(1., height_ratio, 1.); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Height of SVG.").c_str()); + } + + // Lock on ratio m_keep_ratio + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons, m_keep_ratio ? IconType::lock : IconType::unlock); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_ratio ? IconType::lock_hover : IconType::unlock_hover); + if (button(icon, icon_hover, icon)) + m_keep_ratio = !m_keep_ratio; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Lock/unlock the aspect ratio of the SVG.").c_str()); + + + // reset button + bool can_reset = m_scale_width.has_value() || m_scale_height.has_value() || m_scale_depth.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + new_relative_scale = Vec3d(1./m_scale_width.value_or(1.f), 1./m_scale_height.value_or(1.f), 1.); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset scale").c_str()); + } + + if (new_relative_scale.has_value()){ + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_scale_fnc = [&selection, rel_scale = *new_relative_scale]() { + selection.scale(rel_scale, get_transformation_type(selection)); + }; + selection_transform(selection, selection_scale_fnc, m_volume); + + m_parent.do_scale(L("Resize")); + wxGetApp().obj_manipul()->set_dirty(); + // should be the almost same + calculate_scale(); + + NSVGimage *img = m_volume_shape.svg_file.image.get(); + assert(img != NULL); + if (img != NULL){ + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; + m_volume_shape.shapes_with_ids = create_shape_with_ids(*img, params); + process(); + } + } +} + +void GLGizmoSVG::draw_use_surface() +{ + bool can_use_surface = (m_volume->emboss_shape->projection.use_surface)? true : // already used surface must have option to uncheck + !m_volume->is_the_only_one_part(); + m_imgui->disabled_begin(!can_use_surface); + ScopeGuard sc([imgui = m_imgui]() { imgui->disabled_end(); }); + + ImGuiWrapper::text(m_gui_cfg->translations.use_surface); + ImGui::SameLine(m_gui_cfg->input_offset); + + if (ImGui::Checkbox("##useSurface", &m_volume_shape.projection.use_surface)) + process(); +} + +void GLGizmoSVG::draw_distance() +{ + const EmbossProjection& projection = m_volume->emboss_shape->projection; + bool use_surface = projection.use_surface; + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + + float prev_distance = m_distance.value_or(.0f); + float min_distance = static_cast(-2 * projection.depth); + float max_distance = static_cast(2 * projection.depth); + + m_imgui->disabled_begin(!allowe_surface_distance); + ScopeGuard sg([imgui = m_imgui]() { imgui->disabled_end(); }); + + ImGuiWrapper::text(m_gui_cfg->translations.distance); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + const wxString move_tooltip = _L("Distance of the center of the SVG to the model surface."); + bool is_moved = false; + if (use_inch) { + std::optional distance_inch; + if (m_distance.has_value()) distance_inch = (*m_distance * ObjectManipulation::mm_to_in); + min_distance = static_cast(min_distance * ObjectManipulation::mm_to_in); + max_distance = static_cast(max_distance * ObjectManipulation::mm_to_in); + if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.3f in", 1.f, false, move_tooltip)) { + if (distance_inch.has_value()) { + m_distance = *distance_inch * ObjectManipulation::in_to_mm; + } else { + m_distance.reset(); + } + is_moved = true; + } + } else { + if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.2f mm", 1.f, false, move_tooltip)) + is_moved = true; + } + + bool can_reset = m_distance.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + m_distance.reset(); + is_moved = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset distance").c_str()); + } + + if (is_moved) + do_local_z_move(m_parent, m_distance.value_or(.0f) - prev_distance); +} + +void GLGizmoSVG::draw_rotation() +{ + ImGuiWrapper::text(m_gui_cfg->translations.rotation); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + // slider for Clock-wise angle in degress + // stored angle is optional CCW and in radians + // Convert stored value to degress + // minus create clock-wise roation from CCW + float angle = m_angle.value_or(0.f); + float angle_deg = static_cast(-angle * 180 / M_PI); + if (m_imgui->slider_float("##angle", &angle_deg, limits.angle.min, limits.angle.max, u8"%.2f °", 1.f, false, _L("Rotate text Clock-wise."))){ + // convert back to radians and CCW + double angle_rad = -angle_deg * M_PI / 180.0; + Geometry::to_range_pi_pi(angle_rad); + + double diff_angle = angle_rad - angle; + + do_local_z_rotate(m_parent, diff_angle); + + // calc angle after rotation + m_angle = calculate_angle(m_parent.get_selection()); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process(); + } + + // Reset button + if (m_angle.has_value()) { + if (reset_button(m_icons)) { + do_local_z_rotate(m_parent, -(*m_angle)); + m_angle.reset(); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Reset rotation").c_str()); + } + + // Keep up - lock button icon + if (!m_volume->is_the_only_one_part()) { + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons,m_keep_up ? IconType::lock : IconType::unlock); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock_hover : IconType::unlock_hover); + if (button(icon, icon_hover, icon)) + m_keep_up = !m_keep_up; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Lock/unlock rotation angle when dragging above the surface.").c_str()); + } +} + +void GLGizmoSVG::draw_mirroring() +{ + ImGui::Text("%s", m_gui_cfg->translations.mirror.c_str()); + ImGui::SameLine(m_gui_cfg->input_offset); + Axis axis = Axis::UNKNOWN_AXIS; + if(clickable(get_icon(m_icons, IconType::reflection_x), get_icon(m_icons, IconType::reflection_x_hover))){ + axis = Axis::X; + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Mirror vertically").c_str()); + } + + ImGui::SameLine(); + if (clickable(get_icon(m_icons, IconType::reflection_y), get_icon(m_icons, IconType::reflection_y_hover))) { + axis = Axis::Y; + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Mirror horizontally").c_str()); + } + + if (axis != Axis::UNKNOWN_AXIS){ + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_mirror_fnc = [&selection, &axis](){ + selection.mirror(axis, get_transformation_type(selection)); + }; + selection_transform(selection, selection_mirror_fnc, m_volume); + + m_parent.do_mirror(L("Set Mirror")); + wxGetApp().obj_manipul()->UpdateAndShow(true); + + if (m_volume_shape.projection.use_surface) + process(); + } +} + +void GLGizmoSVG::draw_model_type() +{ + bool is_last_solid_part = m_volume->is_the_only_one_part(); + std::string title = _u8L("Operation"); + if (is_last_solid_part) { + ImVec4 color{.5f, .5f, .5f, 1.f}; + m_imgui->text_colored(color, title.c_str()); + } else { + ImGui::Text("%s", title.c_str()); + } + + std::optional new_type; + ModelVolumeType modifier = ModelVolumeType::PARAMETER_MODIFIER; + ModelVolumeType negative = ModelVolumeType::NEGATIVE_VOLUME; + ModelVolumeType part = ModelVolumeType::MODEL_PART; + ModelVolumeType type = m_volume->type(); + + //TRN EmbossOperation + if (ImGui::RadioButton(_u8L("Join").c_str(), type == part)) + new_type = part; + else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to change text into object part.").c_str()); + ImGui::SameLine(); + + std::string last_solid_part_hint = _u8L("You can't change a type of the last solid part of the object."); + if (ImGui::RadioButton(_CTX_utf8(L_CONTEXT("Cut", "EmbossOperation"), "EmbossOperation").c_str(), type == negative)) + new_type = negative; + else if (ImGui::IsItemHovered()) { + if (is_last_solid_part) + ImGui::SetTooltip("%s", last_solid_part_hint.c_str()); + else if (type != negative) + ImGui::SetTooltip("%s", _u8L("Click to change part type into negative volume.").c_str()); + } + + // In simple mode are not modifiers + if (wxGetApp().plater()->printer_technology() != ptSLA && wxGetApp().get_mode() != ConfigOptionMode::comSimple) { + ImGui::SameLine(); + if (ImGui::RadioButton(_u8L("Modifier").c_str(), type == modifier)) + new_type = modifier; + else if (ImGui::IsItemHovered()) { + if (is_last_solid_part) + ImGui::SetTooltip("%s", last_solid_part_hint.c_str()); + else if (type != modifier) + ImGui::SetTooltip("%s", _u8L("Click to change part type into modifier.").c_str()); + } + } + + if (m_volume != nullptr && new_type.has_value() && !is_last_solid_part) { + GUI_App &app = wxGetApp(); + Plater * plater = app.plater(); + // TRN: This is the name of the action that shows in undo/redo stack (changing part type from SVG to something else). + Plater::TakeSnapshot snapshot(plater, _L("Change SVG Type"), UndoRedo::SnapshotType::GizmoAction); + m_volume->set_type(*new_type); + + 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 + ObjectList *obj_list = app.obj_list(); + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection( + obj_list->get_selected_obj_idx(), + [volume = m_volume](const ModelVolume *vol) { return vol == volume; }); + if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + + // NOTE: on linux, function reorder_volumes_and_get_selection call GLCanvas3D::reload_scene(refresh_immediately = false) + // which discard m_volume pointer and set it to nullptr also selection is cleared so gizmo is automaticaly closed + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() != GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); + // TODO: select volume back - Ask @Sasa + } +} + + +///////////// +// private namespace implementation +/////////////// +namespace { + +std::string get_file_name(const std::string &file_path) +{ + if (file_path.empty()) + return file_path; + + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + if (pos_last_delimiter == std::string::npos) { + // should not happend that in path is not delimiter + assert(false); + pos_last_delimiter = 0; + } + + size_t pos_point = file_path.find_last_of('.'); + if (pos_point == std::string::npos || pos_point < pos_last_delimiter // last point is inside of directory path + ) { + // there is no extension + assert(false); + pos_point = file_path.size(); + } + + size_t offset = pos_last_delimiter + 1; // result should not contain last delimiter ( +1 ) + size_t count = pos_point - pos_last_delimiter - 1; // result should not contain extension point ( -1 ) + return file_path.substr(offset, count); +} + +std::string volume_name(const EmbossShape &shape) +{ + std::string file_name = get_file_name(shape.svg_file.path); + if (!file_name.empty()) + return file_name; + return "SVG shape"; +} + +CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager& raycaster, ModelVolumeType volume_type) +{ + auto gizmo = static_cast(GLGizmosManager::Svg); + const GLVolume *gl_volume = get_first_hovered_gl_volume(canvas); + Plater *plater = wxGetApp().plater(); + return CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), + plater->get_ui_job_worker(), volume_type, raycaster, gizmo, gl_volume}; +} + +GuiCfg create_gui_configuration() { + GuiCfg cfg; // initialize by default values; + + float line_height = ImGui::GetTextLineHeight(); + float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); + + float space = line_height_with_spacing - line_height; + + cfg.icon_width = std::max(std::round(line_height/8)*8, 8.f); + + GuiCfg::Translations &tr = cfg.translations; + + float lock_width = cfg.icon_width + 3 * space; + // TRN - Input label. Be short as possible + tr.depth = _u8L("Depth"); + // TRN - Input label. Be short as possible + tr.size = _u8L("Size"); + // TRN - Input label. Be short as possible + tr.use_surface = _u8L("Use surface"); + // TRN - Input label. Be short as possible + tr.distance = _u8L("From surface"); + // TRN - Input label. Be short as possible + tr.rotation = _u8L("Rotation"); + // TRN - Input label. Be short as possible + tr.mirror = _u8L("Mirror"); + float max_tr_width = std::max({ + ImGui::CalcTextSize(tr.depth.c_str()).x, + ImGui::CalcTextSize(tr.size.c_str()).x + lock_width, + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.distance.c_str()).x, + ImGui::CalcTextSize(tr.rotation.c_str()).x + lock_width, + ImGui::CalcTextSize(tr.mirror.c_str()).x, + }); + + const ImGuiStyle &style = ImGui::GetStyle(); + cfg.input_offset = style.WindowPadding.x + max_tr_width + space + cfg.icon_width; + cfg.lock_offset = cfg.input_offset - (cfg.icon_width + 2 * space); + + ImVec2 letter_m_size = ImGui::CalcTextSize("M"); + const float count_letter_M_in_input = 12.f; + cfg.input_width = letter_m_size.x * count_letter_M_in_input; + cfg.texture_max_size_px = std::round((cfg.input_width + cfg.input_offset + cfg.icon_width + space)/8) * 8; + + // calculate window size + float window_input_width = cfg.input_offset + cfg.input_width + style.WindowPadding.x + space; + float window_image_width = cfg.texture_max_size_px + 2*style.WindowPadding.x; + + float window_title = line_height + 2 * style.FramePadding.y + 2 * style.WindowTitleAlign.y; + float input_height = line_height_with_spacing + 2 * style.FramePadding.y; + float separator_height = 2 + style.FramePadding.y; + float window_height = + window_title + // window title + cfg.texture_max_size_px + 2 * style.FramePadding.y + // preview (-- not sure with padding -> fix retina height) + line_height_with_spacing + // filename + separator_height + // separator - orange line + input_height * 6 + // depth + size + use_surface + FromSurface + Rotation + Mirror + 2 * style.WindowPadding.y; + cfg.window_size_for_object = ImVec2(std::max(window_input_width, window_image_width), window_height); + float change_type_height = separator_height + line_height_with_spacing + input_height; + cfg.window_size = ImVec2(cfg.window_size_for_object.x, cfg.window_size_for_object.y + change_type_height); + return cfg; +} + +std::string choose_svg_file() +{ + wxWindow *parent = nullptr; + wxString message = _L("Choose SVG file for emboss:"); + wxString selected_file = wxEmptyString; + wxString wildcard = file_wildcards(FT_SVG); + long style = wxFD_OPEN | wxFD_FILE_MUST_EXIST; + wxFileDialog dialog(parent, message, last_used_directory, selected_file, wildcard, style); + if (dialog.ShowModal() != wxID_OK) { + BOOST_LOG_TRIVIAL(warning) << "SVG file for emboss was NOT selected."; + return {}; + } + + wxArrayString input_files; + dialog.GetPaths(input_files); + if (input_files.IsEmpty()) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result is empty."; + return {}; + } + + if (input_files.size() != 1) + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result contain multiple files but only first is used."; + + auto &input_file = input_files.front(); + std::string path = std::string(input_file.c_str()); + + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return invalid path."; + return {}; + } + + if (!boost::algorithm::iends_with(path, ".svg")) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return path without '.svg' tail"; + return {}; + } + + last_used_directory = dialog.GetDirectory(); + return path; +} + +EmbossShape select_shape(std::string_view filepath, double tesselation_tolerance) +{ + EmbossShape shape; + shape.projection.depth = 10.; + shape.projection.use_surface = false; + + if (filepath.empty()) { + // When empty open file dialog + shape.svg_file.path = choose_svg_file(); + if (shape.svg_file.path.empty()) + return {}; // file was not selected + } else { + shape.svg_file.path = filepath; // copy + } + + + boost::filesystem::path path(shape.svg_file.path); + if (!boost::filesystem::exists(path)) { + show_error(nullptr, GUI::format(_u8L("File does NOT exist (%1%)."), shape.svg_file.path)); + return {}; + } + + if (!boost::algorithm::iends_with(shape.svg_file.path, ".svg")) { + show_error(nullptr, GUI::format(_u8L("Filename has to end with \".svg\" but you selected %1%"), shape.svg_file.path)); + return {}; + } + + if(init_image(shape.svg_file) == nullptr) { + show_error(nullptr, GUI::format(_u8L("Nano SVG parser can't load from file (%1%)."), shape.svg_file.path)); + return {}; + } + + // Set default and unchanging scale + NSVGLineParams params{tesselation_tolerance}; + shape.shapes_with_ids = create_shape_with_ids(*shape.svg_file.image, params); + + // Must contain some shapes !!! + if (shape.shapes_with_ids.empty()) { + show_error(nullptr, GUI::format(_u8L("SVG file does NOT contain a single path to be embossed (%1%)."), shape.svg_file.path)); + return {}; + } + return shape; +} + +DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath) +{ + EmbossShape shape = select_shape(filepath); + + if (shape.shapes_with_ids.empty()) + // canceled selection of SVG file + return nullptr; + + // 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 + if (cancel != nullptr) + cancel->store(true); + // create new shared ptr to cancel new job + cancel = std::make_shared>(false); + + std::string name = volume_name(shape); + + auto result = std::make_unique(name, cancel /*copy*/, std::move(shape)); + result->is_outside = volume_type == ModelVolumeType::MODEL_PART; + return result; +} +} // namespace + +// any existing icon filename to not influence GUI +const std::string GLGizmoSVG::M_ICON_FILENAME = "cut.svg"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp new file mode 100644 index 0000000000..42785759f7 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp @@ -0,0 +1,200 @@ +#ifndef slic3r_GLGizmoSVG_hpp_ +#define slic3r_GLGizmoSVG_hpp_ + +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, +// which overrides our localization "L" macro. +#include "GLGizmoBase.hpp" +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/GLTexture.hpp" +#include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/GUI/IconManager.hpp" + +#include +#include +#include + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Model.hpp" + +#include +#include + +namespace Slic3r{ +class ModelVolume; +enum class ModelVolumeType : int; +} + +namespace Slic3r::GUI { + +struct Texture{ + unsigned id{0}; + unsigned width{0}; + unsigned height{0}; +}; + +class GLGizmoSVG : public GLGizmoBase +{ +public: + explicit GLGizmoSVG(GLCanvas3D &parent); + + /// + /// Create new embossed text volume by type on position of mouse + /// + /// Object part / Negative volume / Modifier + /// Define position of new volume + /// True on succesfull start creation otherwise False + bool create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); // first open file dialog + + /// + /// Create new text without given position + /// + /// Object part / Negative volume / Modifier + /// True on succesfull start creation otherwise False + bool create_volume(ModelVolumeType volume_type); // first open file dialog + + /// + /// Create volume from already selected svg file + /// + /// File path + /// Position on screen where to create volume + /// Object part / Negative volume / Modifier + /// True on succesfull start creation otherwise False + bool create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type = ModelVolumeType::MODEL_PART); + bool create_volume(std::string_view svg_file, ModelVolumeType volume_type = ModelVolumeType::MODEL_PART); + + /// + /// Check whether volume is object containing only emboss volume + /// + /// Pointer to volume + /// True when object otherwise False + static bool is_svg_object(const ModelVolume &volume); + + /// + /// Check whether volume has emboss data + /// + /// Pointer to volume + /// True when constain emboss data otherwise False + static bool is_svg(const ModelVolume &volume); + +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_render() override; + void on_register_raycasters_for_picking() override; + void on_unregister_raycasters_for_picking() override; + void on_render_input_window(float x, float y, float bottom_limit) override; + bool on_is_activable() const override { return true; } + bool on_is_selectable() const override { return false; } + void on_set_state() override; + void data_changed(bool is_serializing) override; // selection changed + void on_set_hover_id() override{ m_rotate_gizmo.set_hover_id(m_hover_id); } + void on_enable_grabber(unsigned int id) override { m_rotate_gizmo.enable_grabber(); } + void on_disable_grabber(unsigned int id) override { m_rotate_gizmo.disable_grabber(); } + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + + /// + /// Rotate by text on dragging rotate grabers + /// + /// Information about mouse + /// Propagete normaly return false. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + bool wants_enter_leave_snapshots() const override; + std::string get_gizmo_entering_text() const override; + std::string get_gizmo_leaving_text() const override; + std::string get_action_snapshot_name() const override; +private: + void set_volume_by_selection(); + void reset_volume(); + + // create volume from text - main functionality + bool process(); + void close(); + void draw_window(); + void draw_preview(); + void draw_filename(); + void draw_depth(); + void draw_size(); + void draw_use_surface(); + void draw_distance(); + void draw_rotation(); + void draw_mirroring(); + void draw_model_type(); + + // process mouse event + bool on_mouse_for_rotation(const wxMouseEvent &mouse_event); + bool on_mouse_for_translate(const wxMouseEvent &mouse_event); + + struct GuiCfg; + std::unique_ptr m_gui_cfg; + + // actual selected only one volume - with emboss data + ModelVolume *m_volume = nullptr; + + // Is used to edit eboss and send changes to job + // Inside volume is current state of shape WRT Volume + EmbossShape m_volume_shape; // copy from m_volume for edit + + // same index as volumes in + std::vector m_shape_warnings; + + // When work with undo redo stack there could be situation that + // m_volume point to unexisting volume so One need also objectID + ObjectID m_volume_id; + + // cancel for previous update of volume to cancel finalize part + std::shared_ptr> m_job_cancel = nullptr; + + // Rotation gizmo + GLGizmoRotate m_rotate_gizmo; + std::optional m_angle; + std::optional m_distance; + + // Value is set only when dragging rotation to calculate actual angle + std::optional m_rotate_start_angle; + + // TODO: it should be accessible by other gizmo too. + // May be move to plater? + RaycastManager m_raycast_manager; + + // When true keep up vector otherwise relative rotation + bool m_keep_up = true; + + // Keep size aspect ratio when True. + bool m_keep_ratio = true; + + // setted only when wanted to use - not all the time + std::optional m_set_window_offset; + + // Keep data about dragging only during drag&drop + std::optional m_surface_drag; + + // For volume on scaled objects + std::optional m_scale_width; + std::optional m_scale_height; + std::optional m_scale_depth; + void calculate_scale(); + float get_scale_for_tolerance(); + + // keep SVG data rendered on GPU + Texture m_texture; + + // bounding box of shape + // Note: Scaled mm to int value by m_volume_shape.scale + BoundingBox m_shape_bb; + + std::string m_filename_preview; + + IconManager m_icon_manager; + IconManager::Icons m_icons; + + // only temporary solution + static const std::string M_ICON_FILENAME; +}; +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoSVG_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index 49920f76a2..428705ff88 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -41,7 +41,7 @@ static void call_after_if_active(std::function fn, GUI_App* app = &wxGet }); } -static std::set get_volume_ids(const Selection &selection) +static std::set get_selected_volume_ids(const Selection &selection) { const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); const ModelObjectPtrs &model_objects = selection.get_model()->objects; @@ -68,20 +68,6 @@ static std::set get_volume_ids(const Selection &selection) return result; } -// return ModelVolume from selection by object id -static ModelVolume *get_volume(const ObjectID &id, const Selection &selection) { - const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); - const ModelObjectPtrs &model_objects = selection.get_model()->objects; - for (auto volume_id : volume_ids) { - const GLVolume *selected_volume = selection.get_volume(volume_id); - const GLVolume::CompositeID &cid = selected_volume->composite_id; - ModelObject *obj = model_objects[cid.object_id]; - ModelVolume *volume = obj->volumes[cid.volume_id]; - if (id == volume->id()) return volume; - } - return nullptr; -} - static std::string create_volumes_name(const std::set& ids, const Selection &selection){ assert(!ids.empty()); std::string name; @@ -92,7 +78,7 @@ static std::string create_volumes_name(const std::set& ids, const Sele else name += " + "; - const ModelVolume *volume = get_volume(id, selection); + const ModelVolume *volume = get_selected_volume(id, selection); assert(volume != nullptr); name += volume->name; } @@ -185,7 +171,7 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi { create_gui_cfg(); const Selection &selection = m_parent.get_selection(); - auto act_volume_ids = get_volume_ids(selection); + auto act_volume_ids = get_selected_volume_ids(selection); if (act_volume_ids.empty()) { stop_worker_thread_request(); close(); @@ -474,7 +460,7 @@ void GLGizmoSimplify::process() const Selection& selection = m_parent.get_selection(); State::Data its; for (const auto &id : m_volume_ids) { - const ModelVolume *volume = get_volume(id, selection); + const ModelVolume *volume = get_selected_volume(id, selection); its[id] = std::make_unique(volume->mesh().its); // copy } @@ -552,7 +538,7 @@ void GLGizmoSimplify::apply_simplify() { for (const auto &item: m_state.result) { const ObjectID &id = item.first; const indexed_triangle_set &its = *item.second; - ModelVolume *volume = get_volume(id, selection); + ModelVolume *volume = get_selected_volume(id, selection); assert(volume != nullptr); ModelObject *obj = volume->get_object(); @@ -728,7 +714,7 @@ void GLGizmoSimplify::on_render() const Selection & selection = m_parent.get_selection(); // Check that the GLVolume still belongs to the ModelObject we work on. - if (m_volume_ids != get_volume_ids(selection)) return; + if (m_volume_ids != get_selected_volume_ids(selection)) return; const ModelObjectPtrs &model_objects = selection.get_model()->objects; const Selection::IndicesList &volume_idxs = selection.get_volume_idxs(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index fb54dad429..99caa946c8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -27,6 +27,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp" #include "libslic3r/format.hpp" @@ -114,6 +115,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 9)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 10)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent)); + m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoSimplify(m_parent)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 6ddb4a5039..ce0e39884c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -83,6 +83,7 @@ public: MmuSegmentation, Measure, Emboss, + Svg, Simplify, Undefined }; diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp index 45c76887c3..a0637189f1 100644 --- a/src/slic3r/GUI/IconManager.cpp +++ b/src/slic3r/GUI/IconManager.cpp @@ -1,6 +1,15 @@ #include "IconManager.hpp" #include #include +#include "nanosvg/nanosvg.h" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/Utils.hpp" // ScopeGuard + +#include "3DScene.hpp" // glsafe +#include "GL/glew.h" + +#define STB_RECT_PACK_IMPLEMENTATION +#include "imgui/imstb_rectpack.h" // distribute rectangles using namespace Slic3r::GUI; @@ -14,16 +23,198 @@ static void draw_transparent_icon(const IconManager::Icon &icon); // only help f IconManager::~IconManager() { priv::clear(m_icons); // release opengl texture is made in ~GLTexture() + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); } -std::vector IconManager::init(const InitTypes &input) +namespace { +NSVGimage *parse_file(const char * filepath) { + FILE *fp = boost::nowide::fopen(filepath, "rb"); + assert(fp != nullptr); + if (fp == nullptr) + return nullptr; + + Slic3r::ScopeGuard sg([fp]() { fclose(fp); }); + + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + // Note: +1 is for null termination + auto data_ptr = std::make_unique(size+1); + data_ptr[size] = '\0'; // Must be null terminated. + + size_t readed_size = fread(data_ptr.get(), 1, size, fp); + assert(readed_size == size); + if (readed_size != size) + return nullptr; + + return nsvgParse(data_ptr.get(), "px", 96.0f); +} + +void subdata(unsigned char *data, size_t data_stride, const std::vector &data2, size_t data2_row) { + assert(data_stride >= data2_row); + for (size_t data2_offset = 0, data_offset = 0; + data2_offset < data2.size(); + data2_offset += data2_row, data_offset += data_stride) + ::memcpy((void *)(data + data_offset), (const void *)(data2.data() + data2_offset), data2_row); +} +} + +IconManager::Icons IconManager::init(const InitTypes &input) { - BOOST_LOG_TRIVIAL(error) << "Not implemented yet"; - return {}; + assert(!input.empty()); + if (input.empty()) + return {}; + + // TODO: remove in future + if (m_id != 0) { + glsafe(::glDeleteTextures(1, &m_id)); + m_id = 0; + } + + int total_surface = 0; + for (const InitType &i : input) + total_surface += i.size.x * i.size.y; + const int surface_sqrt = (int)sqrt((float)total_surface) + 1; + + // Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + int width = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + int num_nodes = width; + std::vector nodes(num_nodes); + stbrp_context context; + stbrp_init_target(&context, width, TEX_HEIGHT_MAX, nodes.data(), num_nodes); + + ImVector pack_rects; + pack_rects.resize(input.size()); + memset(pack_rects.Data, 0, (size_t) pack_rects.size_in_bytes()); + for (size_t i = 0; i < input.size(); i++) { + const ImVec2 &size = input[i].size; + assert(size.x > 1); + assert(size.y > 1); + pack_rects[i].w = size.x; + pack_rects[i].h = size.y; + } + int pack_rects_res = stbrp_pack_rects(&context, &pack_rects[0], pack_rects.Size); + assert(pack_rects_res == 1); + if (pack_rects_res != 1) + return {}; + + ImVec2 tex_size(width, width); + for (const stbrp_rect &rect : pack_rects) { + float x = rect.x + rect.w; + float y = rect.y + rect.h; + if(x > tex_size.x) tex_size.x = x; + if(y > tex_size.y) tex_size.y = y; + } + + Icons result(input.size()); + for (int i = 0; i < pack_rects.Size; i++) { + const stbrp_rect &rect = pack_rects[i]; + assert(rect.was_packed); + if (!rect.was_packed) + return {}; + + ImVec2 tl(rect.x / tex_size.x, rect.y / tex_size.y); + ImVec2 br((rect.x + rect.w) / tex_size.x, (rect.y + rect.h) / tex_size.y); + + assert(input[i].size.x == rect.w); + assert(input[i].size.y == rect.h); + Icon icon = {input[i].size, tl, br}; + result[i] = std::make_shared(std::move(icon)); + } + + NSVGrasterizer *rast = nsvgCreateRasterizer(); + assert(rast != nullptr); + if (rast == nullptr) + return {}; + ScopeGuard sg_rast([rast]() { ::nsvgDeleteRasterizer(rast); }); + + int channels = 4; + int n_pixels = tex_size.x * tex_size.y; + // store data for whole texture + std::vector data(n_pixels * channels, {0}); + + // initialize original index locations + std::vector idx(input.size()); + std::iota(idx.begin(), idx.end(), 0); + + // Group same filename by sort inputs + // sort indexes based on comparing values in input + std::sort(idx.begin(), idx.end(), [&input](size_t i1, size_t i2) { return input[i1].filepath < input[i2].filepath; }); + for (size_t j: idx) { + const InitType &i = input[j]; + if (i.filepath.empty()) + continue; // no file path only reservation of space for texture + assert(boost::filesystem::exists(i.filepath)); + if (!boost::filesystem::exists(i.filepath)) + continue; + assert(boost::algorithm::iends_with(i.filepath, ".svg")); + if (!boost::algorithm::iends_with(i.filepath, ".svg")) + continue; + + NSVGimage *image = parse_file(i.filepath.c_str()); + assert(image != nullptr); + if (image == nullptr) + return {}; + + ScopeGuard sg_image([image]() { ::nsvgDelete(image); }); + + float svg_scale = i.size.y / image->height; + // scale should be same in both directions + assert(is_approx(svg_scale, i.size.y / image->width)); + + const stbrp_rect &rect = pack_rects[j]; + int n_pixels = rect.w * rect.h; + std::vector icon_data(n_pixels * channels, {0}); + ::nsvgRasterize(rast, image, 0, 0, svg_scale, icon_data.data(), i.size.x, i.size.y, i.size.x * channels); + + // makes white or gray only data in icon + if (i.type == RasterType::white_only_data || + i.type == RasterType::gray_only_data) { + unsigned char value = (i.type == RasterType::white_only_data) ? 255 : 127; + for (size_t k = 0; k < icon_data.size(); k += channels) + if (icon_data[k] != 0 || icon_data[k + 1] != 0 || icon_data[k + 2] != 0) { + icon_data[k] = value; + icon_data[k + 1] = value; + icon_data[k + 2] = value; + } + } + + int start_offset = (rect.y*tex_size.x + rect.x) * channels; + int data_stride = tex_size.x * channels; + subdata(data.data() + start_offset, data_stride, icon_data, rect.w * channels); + } + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); + + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glGenTextures(1, &m_id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) m_id)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) tex_size.x, (GLsizei) tex_size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*) data.data())); + + // bind no texture + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + + for (const auto &i : result) + i->tex_id = m_id; + return result; } std::vector IconManager::init(const std::vector &file_paths, const ImVec2 &size, RasterType type) { + assert(!file_paths.empty()); + assert(size.x >= 1); + assert(size.x < 256*16); + // TODO: remove in future if (!m_icons.empty()) { // not first initialization @@ -159,6 +350,7 @@ void priv::draw_transparent_icon(const IconManager::Icon &icon) draw(icon_px); } +#include "imgui/imgui_internal.h" //ImGuiWindow namespace Slic3r::GUI { void draw(const IconManager::Icon &icon, const ImVec2 &size, const ImVec4 &tint_col, const ImVec4 &border_col) @@ -179,7 +371,10 @@ void draw(const IconManager::Icon &icon, const ImVec2 &size, const ImVec4 &tint_ bool clickable(const IconManager::Icon &icon, const IconManager::Icon &icon_hover) { // check of hover - float cursor_x = ImGui::GetCursorPosX(); + ImGuiWindow *window = ImGui::GetCurrentWindow(); + float cursor_x = ImGui::GetCursorPosX() + - window->DC.GroupOffset.x + - window->DC.ColumnsOffset.x; priv::draw_transparent_icon(icon); ImGui::SameLine(cursor_x); if (ImGui::IsItemHovered()) { diff --git a/src/slic3r/GUI/IconManager.hpp b/src/slic3r/GUI/IconManager.hpp index aa7afda800..24ec3f7a91 100644 --- a/src/slic3r/GUI/IconManager.hpp +++ b/src/slic3r/GUI/IconManager.hpp @@ -70,10 +70,10 @@ public: /// Initialize raster texture on GPU with given images /// NOTE: Have to be called after OpenGL initialization /// - /// Define files and its + /// Define files and its size with rasterization /// Rasterized icons stored on GPU, /// Same size and order as input, each item of vector is set of texture in order by RasterType - VIcons init(const InitTypes &input); + Icons init(const InitTypes &input); /// /// Initialize multiple icons with same settings for size and type @@ -96,6 +96,8 @@ public: private: // keep data stored on GPU GLTexture m_icons_texture; + + unsigned int m_id{ 0 }; Icons m_icons; }; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 9faddad696..169887740d 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -673,8 +673,9 @@ bool ImGuiWrapper::slider_float(const char* label, float* v, float v_min, float ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.4f, 0.4f, 0.4f, 1.0f }); ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.4f, 0.4f, 0.4f, 1.0f }); + int frame_padding = style.ItemSpacing.y / 2; // keep same line height for input and slider const ImTextureID tex_id = io.Fonts->TexID; - if (image_button(tex_id, size, uv0, uv1, -1, ImVec4(0.0, 0.0, 0.0, 0.0), ImVec4(1.0, 1.0, 1.0, 1.0), ImGuiButtonFlags_PressedOnClick)) { + if (image_button(tex_id, size, uv0, uv1, frame_padding, ImVec4(0.0, 0.0, 0.0, 0.0), ImVec4(1.0, 1.0, 1.0, 1.0), ImGuiButtonFlags_PressedOnClick)) { if (!slider_editing) ImGui::SetKeyboardFocusHere(-1); else @@ -1398,6 +1399,48 @@ bool ImGuiWrapper::slider_optional_int(const char *label, } else return false; } +std::optional ImGuiWrapper::change_window_position(const char *window_name, bool try_to_fix) { + ImGuiWindow *window = ImGui::FindWindowByName(window_name); + // is window just created + if (window == NULL) + return {}; + + // position of window on screen + ImVec2 position = window->Pos; + ImVec2 size = window->SizeFull; + + // screen size + ImVec2 screen = ImGui::GetMainViewport()->Size; + + std::optional output_window_offset; + if (position.x < 0) { + if (position.y < 0) + // top left + output_window_offset = ImVec2(0, 0); + else + // only left + output_window_offset = ImVec2(0, position.y); + } else if (position.y < 0) { + // only top + output_window_offset = ImVec2(position.x, 0); + } else if (screen.x < (position.x + size.x)) { + if (screen.y < (position.y + size.y)) + // right bottom + output_window_offset = ImVec2(screen.x - size.x, screen.y - size.y); + else + // only right + output_window_offset = ImVec2(screen.x - size.x, position.y); + } else if (screen.y < (position.y + size.y)) { + // only bottom + output_window_offset = ImVec2(position.x, screen.y - size.y); + } + + if (!try_to_fix && output_window_offset.has_value()) + output_window_offset = ImVec2(-1, -1); // Put on default position + + return output_window_offset; +} + void ImGuiWrapper::left_inputs() { ImGui::ClearActiveID(); } diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index 1ec72ed1ca..b3f8b7cf95 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -158,6 +158,15 @@ public: // Extended function ImGuiWrapper::slider_float to work with std::optional, when value == def_val than optional release its value bool slider_optional_int(const char* label, std::optional &v, int v_min, int v_max, const char* format = "%.3f", float power = 1.0f, bool clamp = true, const wxString& tooltip = {}, bool show_edit_btn = true, int def_val = 0); + /// + /// Change position of imgui window + /// + /// ImGui identifier of window + /// [output] optional + /// When True Only move to be full visible otherwise reset position + /// New offset of window for function ImGui::SetNextWindowPos + static std::optional change_window_position(const char *window_name, bool try_to_fix); + /// /// Use ImGui internals to unactivate (lose focus) in input. /// When input is activ it can't change value by application. diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp index 7111eb7ba4..f0ebf7baf4 100644 --- a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp @@ -176,10 +176,11 @@ bool BoostThreadWorker::wait_for_idle(unsigned timeout_ms) bool BoostThreadWorker::push(std::unique_ptr job) { - if (job) - m_input_queue.push(JobEntry{std::move(job)}); + if (!job) + return false; - return bool{job}; + m_input_queue.push(JobEntry{std::move(job)}); + return true; } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp index cd0207cde8..3c080148f1 100644 --- a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp @@ -78,7 +78,7 @@ void CreateFontImageJob::process(Ctl &ctl) // normalize height of font BoundingBox bounding_box; - for (ExPolygon &shape : shapes) + for (const ExPolygon &shape : shapes) bounding_box.merge(BoundingBox(shape.contour.points)); if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1) { m_input.cancel->store(true); diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp index 24c1f6c32e..0b51be2480 100644 --- a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -51,8 +51,7 @@ void CreateFontStyleImagesJob::process(Ctl &ctl) for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); // calculate conversion from FontPoint to screen pixels by size of font - const FontFile::Info &info = get_font_info(*item.font.font_file, item.prop); - double scale = item.prop.size_in_mm / info.unit_per_em * SHAPE_SCALE * m_input.ppm; + double scale = get_text_shape_scale(item.prop, *item.font.font_file); scales[index] = scale; //double scale = font_prop.size_in_mm * SCALING_FACTOR; diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index c44320a932..3fc4616cb9 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -10,6 +10,7 @@ #include #include // load_obj for default mesh #include // use surface cuts +#include // create object #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/NotificationManager.hpp" @@ -19,29 +20,143 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Selection.hpp" #include "slic3r/GUI/CameraUtils.hpp" #include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/Jobs/Worker.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "slic3r/Utils/RaycastManager.hpp" + +// #define EXECUTE_UPDATE_ON_MAIN_THREAD // debug execution on main thread using namespace Slic3r; using namespace Slic3r::Emboss; using namespace Slic3r::GUI; using namespace Slic3r::GUI::Emboss; -// private namespace -namespace priv{ -// create sure that emboss object is bigger than source object [in mm] -constexpr float safe_extension = 1.0f; +// Private implementation for create volume and objects jobs +namespace { +/// +/// 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 +{ + // Hold data about shape + DataBasePtr base; + + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // new created volume transformation + std::optional trmat; + + // Define which gizmo open on the success + 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 +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +class CreateVolumeJob : public Job +{ + DataCreateVolume m_input; + TriangleMesh m_result; + +public: + explicit CreateVolumeJob(DataCreateVolume &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to create ModelObject in job +/// Object is placed on bed under screen coor +/// OR to center of scene when it is out of bed shape +/// +struct DataCreateObject +{ + // Hold data about shape + DataBasePtr base; + + // define position on screen where to create object + Vec2d screen_coor; + + // projection property + Camera camera; + + // shape of bed in case of create volume on bed + std::vector bed_shape; + + // Define which gizmo open on the success + GLGizmosManager::EType gizmo; +}; + +/// +/// Create new TextObject on the platter +/// Should not be stopped +/// +class CreateObjectJob : public Job +{ + DataCreateObject m_input; + TriangleMesh m_result; + Transform3d m_transformation; + +public: + explicit CreateObjectJob(DataCreateObject &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to create(cut) volume from surface object in job +/// +struct CreateSurfaceVolumeData : public SurfaceVolumeData +{ + // Hold data about shape + DataBasePtr base; + + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // Define which gizmo open on the success + GLGizmosManager::EType gizmo; +}; + +/// +/// Cut surface from object and create cutted volume +/// Should not be stopped +/// +class CreateSurfaceVolumeJob : public Job +{ + CreateSurfaceVolumeData m_input; + TriangleMesh m_result; + +public: + explicit CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + /// /// Assert check of inputs data /// -/// -/// bool check(const DataBase &input, bool check_fontfile = true, bool use_surface = false); +bool check(GLGizmosManager::EType gizmo); +bool check(const CreateVolumeParams& input); bool check(const DataCreateVolume &input, bool is_main_thread = false); bool check(const DataCreateObject &input); bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surface = false); @@ -49,7 +164,9 @@ 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; // /// Try to create mesh from text @@ -60,14 +177,14 @@ template static std::vector create_shapes(DataBase &in /// NOTE: Cache glyphs is changed /// To check if process was canceled /// Triangle mesh model -template static TriangleMesh try_create_mesh(DataBase &input, Fnc was_canceled); -template static TriangleMesh create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl &ctl); +template TriangleMesh try_create_mesh(DataBase &input, const Fnc& was_canceled); +template TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl &ctl); /// /// Create default mesh for embossed text /// /// Not empty model(index trinagle set - its) -static TriangleMesh create_default_mesh(); +TriangleMesh create_default_mesh(); /// /// Must be called on main thread @@ -75,7 +192,14 @@ static TriangleMesh create_default_mesh(); /// New mesh data /// Text configuration, ... /// Transformation of volume -static void update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3d *tr = nullptr); +void update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr = nullptr); + +/// +/// Update name in right panel +/// +/// Right panel data +/// Volume with just changed name +void update_name_in_list(const ObjectList &object_list, const ModelVolume &volume); /// /// Add new volume to object @@ -85,16 +209,9 @@ static void update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform /// Type of new volume /// Transformation of volume inside of object /// Text configuration and New VolumeName -static void create_volume(TriangleMesh &&mesh, const ObjectID& object_id, - const ModelVolumeType type, const Transform3d trmat, const DataBase &data); - -/// -/// Select Volume from objects -/// -/// All objects in scene -/// Identifier of volume in object -/// Pointer to volume when exist otherwise nullptr -static ModelVolume *get_volume(ModelObjectPtrs &objects, const ObjectID &volume_id); +/// Gizmo to open +void create_volume(TriangleMesh &&mesh, const ObjectID& object_id, const ModelVolumeType type, + const std::optional& trmat, const DataBase &data, GLGizmosManager::EType gizmo); /// /// Create projection for cut surface from mesh @@ -103,7 +220,7 @@ static ModelVolume *get_volume(ModelObjectPtrs &objects, const ObjectID &volume_ /// Convert shape to milimeters /// Bounding box 3d of model volume for projection ranges /// Orthogonal cut_projection -static OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range); +OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range); /// /// Create tranformation for emboss Cutted surface @@ -113,65 +230,81 @@ static OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale /// Text voliume transformation inside object /// Cutted surface from model /// Projection -static OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut); +OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut); /// /// Cut surface into triangle mesh /// -/// (can't be const - cache of font) +/// (can't be const - cache of font) /// SurfaceVolume data /// Check to interupt execution /// Extruded object from cuted surace -static TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, std::function was_canceled); +template +TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, const Fnc& was_canceled); -static void create_message(const std::string &message); // only in finalize -static bool process(std::exception_ptr &eptr); -static bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input); +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Source object volumes for cut surface from +/// Source volume id +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id = {}); + +void create_message(const std::string &message); // only in finalize +bool process(std::exception_ptr &eptr); +bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input); class JobException : public std::runtime_error { -public: JobException(const char* message):runtime_error(message){}}; +public: using std::runtime_error::runtime_error;}; +auto was_canceled(const Job::Ctl &ctl, const DataBase &base){ + return [&ctl, &cancel = base.cancel]() { + if (cancel->load()) + return true; + return ctl.was_canceled(); + }; +} -}// namespace priv +} // namespace + +void Slic3r::GUI::Emboss::DataBase::write(ModelVolume &volume) const{ + volume.name = volume_name; + volume.emboss_shape = shape; + volume.emboss_shape->fix_3mf_tr.reset(); +} ///////////////// /// Create Volume -CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input) - : m_input(std::move(input)) -{ - assert(priv::check(m_input, true)); -} +CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input): m_input(std::move(input)){ assert(check(m_input, true)); } void CreateVolumeJob::process(Ctl &ctl) { - 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); + if (!check(m_input)) + throw std::runtime_error("Bad input data for EmbossCreateVolumeJob."); + m_result = create_mesh(*m_input.base, was_canceled(ctl, *m_input.base), ctl); } - void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; if (m_result.its.empty()) - return priv::create_message("Can't create empty volume."); - - priv::create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, m_input); + return create_message("Can't create empty volume."); + create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, *m_input.base, m_input.gizmo); } ///////////////// /// Create Object -CreateObjectJob::CreateObjectJob(DataCreateObject &&input) - : m_input(std::move(input)) -{ - assert(priv::check(m_input)); -} +CreateObjectJob::CreateObjectJob(DataCreateObject &&input): m_input(std::move(input)){ assert(check(m_input)); } void CreateObjectJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for EmbossCreateObjectJob."); + if (!check(m_input)) + throw JobException("Bad input data for EmbossCreateObjectJob."); - auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); }; - m_result = priv::create_mesh(m_input, was_canceled, ctl); + // can't create new object with using surface + if (m_input.base->shape.projection.use_surface) + m_input.base->shape.projection.use_surface = false; + + auto was_canceled = ::was_canceled(ctl, *m_input.base); + m_result = create_mesh(*m_input.base, was_canceled, ctl); if (was_canceled()) return; // Create new object @@ -185,12 +318,12 @@ void CreateObjectJob::process(Ctl &ctl) bed_shape_.reserve(m_input.bed_shape.size()); for (const Vec2d &p : m_input.bed_shape) bed_shape_.emplace_back(p.cast()); - Polygon bed(bed_shape_); + Slic3r::Polygon bed(bed_shape_); if (!bed.contains(bed_coor.cast())) // mouse pose is out of build plate so create object in center of plate bed_coor = bed.centroid().cast(); - double z = m_input.text_configuration.style.prop.emboss / 2; + double z = m_input.base->shape.projection.depth / 2; Vec3d offset(bed_coor.x(), bed_coor.y(), z); offset -= m_result.center(); Transform3d::TranslationType tt(offset.x(), offset.y(), offset.z()); @@ -199,32 +332,52 @@ void CreateObjectJob::process(Ctl &ctl) void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; // only for sure if (m_result.empty()) - return priv::create_message("Can't create empty object."); - - GUI_App &app = wxGetApp(); - Plater *plater = app.plater(); - ObjectList *obj_list = app.obj_list(); - GLCanvas3D *canvas = plater->canvas3D(); + return create_message("Can't create empty object."); + GUI_App &app = wxGetApp(); + Plater *plater = app.plater(); plater->take_snapshot(_L("Add Emboss text object")); - // Create new object and change selection - bool center = false; - obj_list->load_mesh_object(std::move(m_result), m_input.volume_name, - center, &m_input.text_configuration, - &m_transformation); + Model& model = plater->model(); +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ + { + // INFO: inspiration for create object is from ObjectList::load_mesh_object() + ModelObject *new_object = model.add_object(); + new_object->name = m_input.base->volume_name; + new_object->add_instance(); // each object should have at list one instance + + ModelVolume *new_volume = new_object->add_volume(std::move(m_result)); + // set a default extruder value, since user can't add it manually + new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); + // write emboss data into volume + m_input.base->write(*new_volume); + + // set transformation + Slic3r::Geometry::Transformation tr(m_transformation); + new_object->instances.front()->set_transformation(tr); + new_object->ensure_on_bed(); + + // Actualize right panel and set inside of selection + app.obj_list()->paste_objects_into_list({model.objects.size() - 1}); + } +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ // When add new object selection is empty. // When cursor move and no one object is selected than // Manager::reset_all() So Gizmo could be closed before end of creation object + GLCanvas3D *canvas = plater->canvas3D(); GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) - manager.open_gizmo(GLGizmosManager::Emboss); + if (manager.get_current_type() != m_input.gizmo) + manager.open_gizmo(m_input.gizmo); // redraw scene canvas->reload_scene(true); @@ -232,87 +385,88 @@ void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) ///////////////// /// Update Volume -UpdateJob::UpdateJob(DataUpdate&& input) - : m_input(std::move(input)) -{ - assert(priv::check(m_input, true)); -} +UpdateJob::UpdateJob(DataUpdate&& input): m_input(std::move(input)){ assert(check(m_input, true)); } void UpdateJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for EmbossUpdateJob."); + if (!check(m_input)) + throw JobException("Bad input data for EmbossUpdateJob."); - auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool { - if (cancel->load()) return true; - return ctl.was_canceled(); - }; - m_result = priv::try_create_mesh(m_input, was_canceled); + auto was_canceled = ::was_canceled(ctl, *m_input.base); + m_result = ::try_create_mesh(*m_input.base, was_canceled); if (was_canceled()) return; if (m_result.its.empty()) - throw priv::JobException("Created text volume is empty. Change text or font."); + throw JobException("Created text volume is empty. Change text or font."); } void UpdateJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; - priv::update_volume(std::move(m_result), m_input); + ::update_volume(std::move(m_result), m_input); } -namespace Slic3r::GUI::Emboss { - -SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) +void UpdateJob::update_volume(ModelVolume *volume, TriangleMesh &&mesh, const DataBase &base) { - SurfaceVolumeData::ModelSources result; - result.reserve(volumes.size() - 1); - for (const ModelVolume *v : volumes) { - if (text_volume_id.has_value() && v->id().id == *text_volume_id) continue; - // skip modifiers and negative volumes, ... - if (!v->is_model_part()) continue; - const TriangleMesh &tm = v->mesh(); - if (tm.empty()) continue; - if (tm.its.empty()) continue; - result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()}); + // check inputs + bool is_valid_input = volume != nullptr && !mesh.empty() && !base.volume_name.empty(); + assert(is_valid_input); + if (!is_valid_input) + return; + + // update volume + volume->set_mesh(std::move(mesh)); + volume->set_new_unique_id(); + volume->calculate_convex_hull(); + volume->get_object()->invalidate_bounding_box(); + + // write data from base into volume + base.write(*volume); + + GUI_App &app = wxGetApp(); // may be move to input + if (volume->name != base.volume_name) { + volume->name = base.volume_name; + + const ObjectList *obj_list = app.obj_list(); + if (obj_list != nullptr) + update_name_in_list(*obj_list, *volume); } - return result; + + // When text is object. + // When text positive volume is lowest part of object than modification of text + // have to move object on bed. + if (volume->type() == ModelVolumeType::MODEL_PART) + volume->get_object()->ensure_on_bed(); + + // redraw scene + GLCanvas3D *canvas = app.plater()->canvas3D(); + + bool refresh_immediately = false; + canvas->reload_scene(refresh_immediately); + + // Change buttons "Export G-code" into "Slice now" + canvas->post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } -SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume) -{ - if (text_volume == nullptr) return {}; - if (!text_volume->text_configuration.has_value()) return {}; - const ModelVolumePtrs &volumes = text_volume->get_object()->volumes; - // no other volume in object - if (volumes.size() <= 1) return {}; - return create_sources(volumes, text_volume->id().id); -} - - - -} // namespace Slic3r::GUI::Emboss - ///////////////// /// Create Surface volume CreateSurfaceVolumeJob::CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input) : m_input(std::move(input)) { - assert(priv::check(m_input, true)); + assert(check(m_input, true)); } void CreateSurfaceVolumeJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for CreateSurfaceVolumeJob."); - // check cancelation of process - auto was_canceled = [&ctl]() -> bool { return ctl.was_canceled(); }; - m_result = priv::cut_surface(m_input, m_input, was_canceled); + if (!check(m_input)) + throw JobException("Bad input data for CreateSurfaceVolumeJob."); + m_result = cut_surface(*m_input.base, m_input, was_canceled(ctl, *m_input.base)); } void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; - priv::create_volume(std::move(m_result), m_input.object_id, - m_input.volume_type, m_input.text_tr, m_input); + create_volume(std::move(m_result), m_input.object_id, + m_input.volume_type, m_input.transform, *m_input.base, m_input.gizmo); } ///////////////// @@ -320,173 +474,357 @@ void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { UpdateSurfaceVolumeJob::UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input) : m_input(std::move(input)) { - assert(priv::check(m_input, true)); + assert(check(m_input, true)); } void UpdateSurfaceVolumeJob::process(Ctl &ctl) { - if (!priv::check(m_input)) - throw std::runtime_error("Bad input data for UseSurfaceJob."); - - // check cancelation of process - auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool { - if (cancel->load()) return true; - return ctl.was_canceled(); - }; - m_result = priv::cut_surface(m_input, m_input, was_canceled); + if (!check(m_input)) + throw JobException("Bad input data for UseSurfaceJob."); + m_result = cut_surface(*m_input.base, m_input, was_canceled(ctl, *m_input.base)); } void UpdateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (!priv::finalize(canceled, eptr, m_input)) + if (!::finalize(canceled, eptr, *m_input.base)) return; // when start using surface it is wanted to move text origin on surface of model // also when repeteadly move above surface result position should match - Transform3d *tr = &m_input.text_tr; - priv::update_volume(std::move(m_result), m_input, tr); + ::update_volume(std::move(m_result), m_input, &m_input.transform); } +namespace { +/// +/// Check if volume type is possible use for new text volume +/// +/// Type +/// True when allowed otherwise false +bool is_valid(ModelVolumeType volume_type); + +/// +/// Start job for add new volume to object with given transformation +/// +/// Define where to queue the job. e.g. wxGetApp().plater()->get_ui_job_worker() +/// Define where to add +/// Wanted volume transformation, when not set will be calculated after creation to be near the object +/// Define what to emboss - shape +/// Type of volume: Part, negative, modifier +/// Define which gizmo open on the success +/// Nullptr when job is sucessfully add to worker otherwise return data to be processed different way +bool start_create_volume_job(Worker &worker, + const ModelObject &object, + const std::optional &volume_tr, + DataBasePtr data, + ModelVolumeType volume_type, + GLGizmosManager::EType gizmo); + +/// +/// Find volume in selected objects with closest convex hull to screen center. +/// +/// Define where to search for closest +/// Canvas center(dependent on camera settings) +/// Actual objects +/// OUT: coordinate of controid of closest volume +/// closest volume when exists otherwise nullptr +const GLVolume *find_closest( + const Selection &selection, const Vec2d &screen_center, const Camera &camera, const ModelObjectPtrs &objects, Vec2d *closest_center); + +/// +/// Start job for add object with text into scene +/// +/// Contain worker, build shape, gizmo +/// Define params for create volume +/// Screen coordinat, where to create new object laying on bed +/// True when can add job to worker otherwise FALSE +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor); + +/// +/// Start job to create volume on the surface of object +/// +/// Variabless needed to create volume +/// Describe what to emboss - shape +/// Where to add +/// True .. try to create volume without screen_coor, +/// False .. +/// Nullptr when job is sucessfully add to worker otherwise return data to be processed different way +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &screen_coor, bool try_no_coor); + +} // namespace + +namespace Slic3r::GUI::Emboss { + +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &text_volume) +{ + const ModelVolumePtrs &volumes = text_volume.get_object()->volumes; + // no other volume in object + if (volumes.size() <= 1) + return {}; + return ::create_sources(volumes, text_volume.id().id); +} + +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos) +{ + if (data == nullptr) + return false; + if (!check(input)) + return false; + + if (input.gl_volume == nullptr) + // object is not under mouse position soo create object on plater + return ::start_create_object_job(input, std::move(data), mouse_pos); + + bool try_no_coor = true; + return ::start_create_volume_on_surface_job(input, std::move(data), mouse_pos, try_no_coor); +} + +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data) +{ + assert(data != nullptr); + if (data == nullptr) + return false; + if (!check(input)) + return false; + + // select position by camera position and view direction + const Selection &selection = input.canvas.get_selection(); + int object_idx = selection.get_object_idx(); + + Size s = input.canvas.get_canvas_size(); + Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); + const ModelObjectPtrs &objects = selection.get_model()->objects; + + // No selected object so create new object + if (selection.is_empty() || object_idx < 0 || + static_cast(object_idx) >= objects.size()) + // create Object on center of screen + // when ray throw center of screen not hit bed it create object on center of bed + return ::start_create_object_job(input, std::move(data), screen_center); + + // create volume inside of selected object + Vec2d coor; + const Camera &camera = wxGetApp().plater()->get_camera(); + input.gl_volume = ::find_closest(selection, screen_center, camera, objects, &coor); + if (input.gl_volume == nullptr) + return ::start_create_object_job(input, std::move(data), screen_center); + + bool try_no_coor = false; + return ::start_create_volume_on_surface_job(input, std::move(data), coor, try_no_coor); +} + +#ifdef EXECUTE_UPDATE_ON_MAIN_THREAD +namespace { +// Run Job on main thread (blocking) - ONLY DEBUG +static inline bool execute_job(std::shared_ptr j) +{ + struct MyCtl : public Job::Ctl + { + void update_status(int st, const std::string &msg = "") override{}; + bool was_canceled() const override { return false; } + std::future call_on_main_thread(std::function fn) override { return std::future{}; } + } ctl; + j->process(ctl); + wxGetApp().plater()->CallAfter([j]() { + std::exception_ptr e_ptr = nullptr; + j->finalize(false, e_ptr); + }); + return true; +} +} // namespace +#endif + +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager& raycaster) +{ + assert(data.volume_id == volume.id()); + + // check cutting from source mesh + bool &use_surface = data.base->shape.projection.use_surface; + if (use_surface && volume.is_the_only_one_part()) + use_surface = false; + + std::unique_ptr job = nullptr; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_volume_sources(volume); + if (sources.empty()) + return false; + + Transform3d volume_tr = volume.get_matrix(); + const std::optional &fix_3mf = volume.emboss_shape->fix_3mf_tr; + if (fix_3mf.has_value()) + volume_tr = volume_tr * fix_3mf->inverse(); + + // when it is new applying of use surface than move origin onto surfaca + if (!volume.emboss_shape->projection.use_surface) { + auto offset = calc_surface_offset(selection, raycaster); + if (offset.has_value()) + volume_tr *= Eigen::Translation(*offset); + } + + UpdateSurfaceVolumeData surface_data{std::move(data), {volume_tr, std::move(sources)}}; + job = std::make_unique(std::move(surface_data)); + } else { + job = std::make_unique(std::move(data)); + } + +#ifndef EXECUTE_UPDATE_ON_MAIN_THREAD + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + return queue_job(worker, std::move(job)); +#else + // Run Job on main thread (blocking) - ONLY DEBUG + return execute_job(std::move(job)); +#endif // EXECUTE_UPDATE_ON_MAIN_THREAD +} + +} // namespace Slic3r::GUI::Emboss + //////////////////////////// /// private namespace implementation -bool priv::check(const DataBase &input, bool check_fontfile, bool use_surface) +namespace { +bool check(const DataBase &input, bool check_fontfile, bool use_surface) { bool res = true; - if (check_fontfile) { - assert(input.font_file.has_value()); - res &= input.font_file.has_value(); - } - assert(!input.text_configuration.fix_3mf_tr.has_value()); - res &= !input.text_configuration.fix_3mf_tr.has_value(); - assert(!input.text_configuration.text.empty()); - res &= !input.text_configuration.text.empty(); + // if (check_fontfile) { + // assert(input.font_file.has_value()); + // res &= input.font_file.has_value(); + // } + // assert(!input.text_configuration.fix_3mf_tr.has_value()); + // res &= !input.text_configuration.fix_3mf_tr.has_value(); + // assert(!input.text_configuration.text.empty()); + // res &= !input.text_configuration.text.empty(); assert(!input.volume_name.empty()); res &= !input.volume_name.empty(); - const FontProp& prop = input.text_configuration.style.prop; - assert(prop.use_surface == use_surface); - res &= prop.use_surface == use_surface; - 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(); - } + //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 priv::check(const DataCreateVolume &input, bool is_main_thread) { - bool check_fontfile = false; - bool res = check((DataBase) input, check_fontfile); - assert(input.volume_type != ModelVolumeType::INVALID); - res &= input.volume_type != ModelVolumeType::INVALID; - assert(input.object_id.id >= 0); - res &= input.object_id.id >= 0; - return res; + +bool check(GLGizmosManager::EType gizmo) +{ + assert(gizmo == GLGizmosManager::Emboss || gizmo == GLGizmosManager::Svg); + return gizmo == GLGizmosManager::Emboss || gizmo == GLGizmosManager::Svg; } -bool priv::check(const DataCreateObject &input) { + +bool check(const CreateVolumeParams &input) +{ + bool res = is_valid(input.volume_type); + auto gizmo_type = static_cast(input.gizmo); + res &= ::check(gizmo_type); + return res; +} + +bool check(const DataCreateVolume &input, bool is_main_thread) +{ bool check_fontfile = false; - bool res = check((DataBase) input, check_fontfile); + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile); + res &= is_valid(input.volume_type); + res &= check(input.gizmo); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; + return res; +} +bool check(const DataCreateObject &input) +{ + bool check_fontfile = false; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile); assert(input.screen_coor.x() >= 0.); res &= input.screen_coor.x() >= 0.; assert(input.screen_coor.y() >= 0.); res &= input.screen_coor.y() >= 0.; assert(input.bed_shape.size() >= 3); // at least triangle res &= input.bed_shape.size() >= 3; + res &= check(input.gizmo); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; return res; } -bool priv::check(const DataUpdate &input, bool is_main_thread, bool use_surface){ +bool check(const DataUpdate &input, bool is_main_thread, bool use_surface) +{ bool check_fontfile = true; - bool res = check((DataBase) input, check_fontfile, use_surface); - assert(input.volume_id.id >= 0); - res &= input.volume_id.id >= 0; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile, use_surface); if (is_main_thread) - assert(get_volume(wxGetApp().model().objects, input.volume_id) != nullptr); - assert(input.cancel != nullptr); - res &= input.cancel != nullptr; + assert(get_model_volume(input.volume_id, wxGetApp().model().objects) != nullptr); + assert(input.base->cancel != nullptr); + res &= input.base->cancel != nullptr; if (is_main_thread) - assert(!input.cancel->load()); + assert(!input.base->cancel->load()); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; return res; } -bool priv::check(const CreateSurfaceVolumeData &input, bool is_main_thread) +bool check(const CreateSurfaceVolumeData &input, bool is_main_thread) { bool use_surface = true; - bool res = check((DataBase)input, is_main_thread, use_surface); + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, is_main_thread, use_surface); assert(!input.sources.empty()); res &= !input.sources.empty(); + res &= check(input.gizmo); + assert(input.base->shape.projection.use_surface); + res &= input.base->shape.projection.use_surface; return res; } -bool priv::check(const UpdateSurfaceVolumeData &input, bool is_main_thread){ +bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread) +{ bool use_surface = true; - bool res = check((DataUpdate)input, is_main_thread, use_surface); + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, is_main_thread, use_surface); assert(!input.sources.empty()); res &= !input.sources.empty(); + assert(input.base->shape.projection.use_surface); + res &= input.base->shape.projection.use_surface; return res; } 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; +ExPolygons create_shape(DataBase &input, Fnc was_canceled) { + EmbossShape &es = input.create_shape(); + float delta = 50.f; + unsigned max_heal_iteration = 10; + HealedExPolygons result = union_with_delta(es.shapes_with_ids, delta, max_heal_iteration); + es.is_healed = result.is_healed; + for (const ExPolygonsWithId &e : es.shapes_with_ids) + if (!e.is_healed) + es.is_healed = false; + return result.expolygons; } //#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) +std::vector create_line_bounds(const ExPolygonsWithIds &shapes, size_t count_lines = 0) { - assert(text.size() == shapes.size()); if (count_lines == 0) - count_lines = get_count_lines(text); - assert(count_lines == get_count_lines(text)); + count_lines = get_count_lines(shapes); + assert(count_lines == get_count_lines(shapes)); 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]; + for (const ExPolygonsWithId &shape_id: shapes) { + const ExPolygons &shape = shape_id.expoly; 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'){ + if (shape_id.id == ENTER_UNICODE) { // skip enters on beginig and tail ++text_line_index; } @@ -498,28 +836,22 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w { // 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()) + const EmbossShape &shape = input.create_shape(); + if (shape.shapes_with_ids.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()); + assert(get_count_lines(shape.shapes_with_ids) == 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); + std::vector bbs = create_line_bounds(shape.shapes_with_ids, count_lines); + + double depth = shape.projection.depth / shape.scale; + auto scale_tr = Eigen::Scaling(shape.scale); // half of font em size for direction of letter emboss - double em_2_mm = prop.size_in_mm / 2.; + // double em_2_mm = prop.size_in_mm / 2.; // TODO: fix it + double em_2_mm = 5.; 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) @@ -527,7 +859,7 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w 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); + 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) { @@ -535,10 +867,11 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w 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; + Vec2d to_zero_vec = letter_bb.center().cast() * shape.scale; // [in mm] + float surface_offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (-shape.projection.depth + SAFE_SURFACE_OFFSET); + + if (input.from_surface.has_value()) + surface_offset += *input.from_surface; Eigen::Translation to_zero(-to_zero_vec.x(), 0., static_cast(surface_offset)); @@ -550,7 +883,7 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w 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]; + const ExPolygons &letter_shape = shape.shapes_with_ids[s_i_offset + i].expoly; assert(get_extents(letter_shape) == letter_bb); auto projectZ = std::make_unique(depth); ProjectTransform project(std::move(projectZ), tr); @@ -589,11 +922,9 @@ template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc w } return TriangleMesh(std::move(result)); } -} // namespace - template -TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) +TriangleMesh try_create_mesh(DataBase &input, const Fnc& was_canceled) { if (!input.text_lines.empty()) { TriangleMesh tm = create_mesh_per_glyph(input, was_canceled); @@ -601,17 +932,17 @@ TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) if (!tm.empty()) return tm; } - ExPolygons shapes = priv::create_shape(input, was_canceled); + ExPolygons shapes = 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; + double scale = input.shape.scale; + double depth = input.shape.projection.depth / scale; auto projectZ = std::make_unique(depth); - float offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (SAFE_SURFACE_OFFSET - prop.emboss); + float offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (SAFE_SURFACE_OFFSET - input.shape.projection.depth); + if (input.from_surface.has_value()) + offset += *input.from_surface; Transform3d tr = Eigen::Translation(0., 0.,static_cast(offset)) * Eigen::Scaling(scale); ProjectTransform project(std::move(projectZ), tr); if (was_canceled()) return {}; @@ -619,23 +950,21 @@ TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) } template -TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl) +TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl& ctl) { // It is neccessary to create some shape // Emboss text window is opened by creation new emboss text object - TriangleMesh result; - if (input.font_file.has_value()) { - result = try_create_mesh(input, was_canceled); - if (was_canceled()) return {}; - } + TriangleMesh result = try_create_mesh(input, was_canceled); + if (was_canceled()) + return {}; if (result.its.empty()) { - result = priv::create_default_mesh(); - if (was_canceled()) return {}; + result = create_default_mesh(); + if (was_canceled()) + return {}; // only info ctl.call_on_main_thread([]() { - create_message("It is used default volume for embossed " - "text, try to change text or font to fix it."); + create_message("It is used default volume for embossed text, try to change text or font to fix it."); }); } @@ -643,7 +972,7 @@ TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl) return result; } -TriangleMesh priv::create_default_mesh() +TriangleMesh create_default_mesh() { // When cant load any font use default object loaded from file std::string path = Slic3r::resources_dir() + "/data/embossed_text.obj"; @@ -655,105 +984,57 @@ TriangleMesh priv::create_default_mesh() return triangle_mesh; } -namespace{ -void update_volume_name(const ModelVolume &volume, const ObjectList *obj_list) +void update_name_in_list(const ObjectList& object_list, const ModelVolume& volume) { - if (obj_list == nullptr) + const ModelObjectPtrs *objects_ptr = object_list.objects(); + if (objects_ptr == nullptr) return; - const std::vector* objects = obj_list->objects(); - if (objects == nullptr) - return; + const ModelObjectPtrs &objects = *objects_ptr; + const ModelObject *object = volume.get_object(); + const ObjectID &object_id = object->id(); - int object_idx = -1; - int volume_idx = -1; - for (size_t oi = 0; oi < objects->size(); ++oi) { - const ModelObject *mo = objects->at(oi); - if (mo == nullptr) - continue; - if (volume.get_object()->id() != mo->id()) - continue; - const ModelVolumePtrs& volumes = mo->volumes; - for (size_t vi = 0; vi < volumes.size(); ++vi) { - const ModelVolume *mv = volumes[vi]; - if (mv == nullptr) - continue; - if (mv->id() == volume.id()){ - object_idx = static_cast(oi); - volume_idx = static_cast(vi); - break; - } - } - if (volume_idx > 0) + // search for index of object + int object_index = -1; + for (size_t i = 0; i < objects.size(); ++i) + if (objects[i]->id() == object_id) { + object_index = static_cast(i); break; - } - obj_list->update_name_in_list(object_idx, volume_idx); -} -} + } -void UpdateJob::update_volume(ModelVolume *volume, - TriangleMesh &&mesh, - const TextConfiguration &text_configuration, - std::string_view volume_name) -{ - // check inputs - bool is_valid_input = - volume != nullptr && - !mesh.empty() && - !volume_name.empty(); - assert(is_valid_input); - if (!is_valid_input) return; + const ModelVolumePtrs volumes = object->volumes; + const ObjectID &volume_id = volume.id(); - // update volume - volume->set_mesh(std::move(mesh)); - volume->set_new_unique_id(); - volume->calculate_convex_hull(); - volume->get_object()->invalidate_bounding_box(); - volume->text_configuration = text_configuration; + // search for index of volume + int volume_index = -1; + for (size_t i = 0; i < volumes.size(); ++i) + if (volumes[i]->id() == volume_id) { + volume_index = static_cast(i); + break; + } - // discard information about rotation, should not be stored in volume - volume->text_configuration->style.prop.angle.reset(); - - GUI_App &app = wxGetApp(); // may be move ObjectList and Plater to input? - - // update volume name in right panel( volume / object name) - if (volume->name != volume_name) { - volume->name = volume_name; - update_volume_name(*volume, app.obj_list()); - } - - // When text is object. - // When text positive volume is lowest part of object than modification of text - // have to move object on bed. - if (volume->type() == ModelVolumeType::MODEL_PART) - volume->get_object()->ensure_on_bed(); - - // redraw scene - Plater *plater = app.plater(); - if (plater == nullptr) + if (object_index < 0 || volume_index < 0) return; - // Update Model and redraw scene - plater->update(); + object_list.update_name_in_list(object_index, volume_index); } -void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3d* tr) +void update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr) { // for sure that some object will be created - if (mesh.its.empty()) + if (mesh.its.empty()) return create_message("Empty mesh can't be created."); - Plater *plater = wxGetApp().plater(); - GLCanvas3D *canvas = plater->canvas3D(); + Plater *plater = wxGetApp().plater(); + // Check gizmo is still open otherwise job should be canceled + assert(plater->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss || + plater->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Svg); - // Check emboss gizmo is still open - GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) - return; - - std::string snap_name = GUI::format(_L("Text: %1%"), data.text_configuration.text); + // TRN: This is the name of the action appearing in undo/redo stack. + std::string snap_name = _u8L("Text/SVG attribute change"); Plater::TakeSnapshot snapshot(plater, snap_name, UndoRedo::SnapshotType::GizmoAction); - ModelVolume *volume = get_volume(plater->model().objects, data.volume_id); + + ModelVolume *volume = get_model_volume(data.volume_id, plater->model().objects); // could appear when user delete edited volume if (volume == nullptr) @@ -763,18 +1044,21 @@ void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data, Transform3 volume->set_transformation(*tr); } else { // apply fix matrix made by store to .3mf - const auto &tc = volume->text_configuration; - assert(tc.has_value()); - if (tc.has_value() && tc->fix_3mf_tr.has_value()) - volume->set_transformation(volume->get_matrix() * tc->fix_3mf_tr->inverse()); + const std::optional &emboss_shape = volume->emboss_shape; + assert(emboss_shape.has_value()); + if (emboss_shape.has_value() && emboss_shape->fix_3mf_tr.has_value()) + volume->set_transformation(volume->get_matrix() * emboss_shape->fix_3mf_tr->inverse()); } - UpdateJob::update_volume(volume, std::move(mesh), data.text_configuration, data.volume_name); + UpdateJob::update_volume(volume, std::move(mesh), *data.base); } -void priv::create_volume( - TriangleMesh &&mesh, const ObjectID& object_id, - const ModelVolumeType type, const Transform3d trmat, const DataBase &data) +void create_volume(TriangleMesh &&mesh, + const ObjectID &object_id, + const ModelVolumeType type, + const std::optional &trmat, + const DataBase &data, + GLGizmosManager::EType gizmo) { GUI_App &app = wxGetApp(); Plater *plater = app.plater(); @@ -782,26 +1066,33 @@ void priv::create_volume( GLCanvas3D *canvas = plater->canvas3D(); ModelObjectPtrs &objects = plater->model().objects; - ModelObject *obj = nullptr; - size_t object_idx = 0; + ModelObject *obj = nullptr; + size_t object_idx = 0; for (; object_idx < objects.size(); ++object_idx) { ModelObject *o = objects[object_idx]; - if (o->id() == object_id) { + if (o->id() == object_id) { obj = o; break; - } + } } // Parent object for text volume was propably removed. // Assumption: User know what he does, so text volume is no more needed. if (obj == nullptr) - return priv::create_message("Bad object to create volume."); + return create_message("Bad object to create volume."); if (mesh.its.empty()) - return priv::create_message("Can't create empty volume."); + return create_message("Can't create empty volume."); plater->take_snapshot(_L("Add Emboss text Volume")); + BoundingBoxf3 instance_bb; + if (!trmat.has_value()) { + // used for align to instance + size_t instance_index = 0; // must exist + instance_bb = obj->instance_bounding_box(instance_index); + } + // NOTE: be carefull add volume also center mesh !!! // So first add simple shape(convex hull is also calculated) ModelVolume *volume = obj->add_volume(make_cube(1., 1., 1.), type); @@ -811,20 +1102,32 @@ void priv::create_volume( volume->set_mesh(std::move(mesh)); volume->calculate_convex_hull(); - // set a default extruder value, since user can't add it manually volume->config.set_key_value("extruder", new ConfigOptionInt(0)); // do not allow model reload from disk volume->source.is_from_builtin_objects = true; - volume->name = data.volume_name; // copy - volume->text_configuration = data.text_configuration; // copy + volume->name = data.volume_name; // copy - // discard information about rotation, should not be stored in volume - volume->text_configuration->style.prop.angle.reset(); + if (trmat.has_value()) { + volume->set_transformation(*trmat); + } else { + assert(!data.shape.projection.use_surface); + // Create transformation for volume near from object(defined by glVolume) + // Transformation is inspired add generic volumes in ObjectList::load_generic_subobject + Vec3d volume_size = volume->mesh().bounding_box().size(); + // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. + Vec3d offset_tr(0, // center of instance - Can't suggest width of text before it will be created + -instance_bb.size().y() / 2 - volume_size.y() / 2, // under + volume_size.z() / 2 - instance_bb.size().z() / 2); // lay on bed + // use same instance as for calculation of instance_bounding_box + Transform3d tr = obj->instances.front()->get_transformation().get_matrix_no_offset().inverse(); + Transform3d volume_trmat = tr * Eigen::Translation3d(offset_tr); + volume->set_transformation(volume_trmat); + } - volume->set_transformation(trmat); + data.write(*volume); // update printable state on canvas if (type == ModelVolumeType::MODEL_PART) { @@ -838,40 +1141,29 @@ void priv::create_volume( // select only actual volume // when new volume is created change selection to this volume auto add_to_selection = [volume](const ModelVolume *vol) { return vol == volume; }; - wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(object_idx, add_to_selection); - if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(object_idx, add_to_selection); + if (!sel.IsEmpty()) + obj_list->select_item(sel.front()); obj_list->selection_changed(); // Now is valid text volume selected open emboss gizmo GLGizmosManager &manager = canvas->get_gizmos_manager(); - if (manager.get_current_type() != GLGizmosManager::Emboss) - manager.open_gizmo(GLGizmosManager::Emboss); + if (manager.get_current_type() != gizmo) + manager.open_gizmo(gizmo); // update model and redraw scene //canvas->reload_scene(true); plater->update(); } -ModelVolume *priv::get_volume(ModelObjectPtrs &objects, - const ObjectID &volume_id) +OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range) { - for (ModelObject *obj : objects) - for (ModelVolume *vol : obj->volumes) - if (vol->id() == volume_id) return vol; - return nullptr; -}; - -OrthoProject priv::create_projection_for_cut( - Transform3d tr, - double shape_scale, - const std::pair &z_range) -{ - double min_z = z_range.first - priv::safe_extension; - double max_z = z_range.second + priv::safe_extension; + double min_z = z_range.first - safe_extension; + double max_z = z_range.second + safe_extension; assert(min_z < max_z); // range between min and max value - double projection_size = max_z - min_z; + double projection_size = max_z - min_z; Matrix3d transformation_for_vector = tr.linear(); // Projection must be negative value. // System of text coordinate @@ -887,8 +1179,7 @@ OrthoProject priv::create_projection_for_cut( return OrthoProject(tr, project_direction); } -OrthoProject3d priv::create_emboss_projection( - bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) +OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) { float front_move = (is_outside) ? emboss : SAFE_SURFACE_OFFSET, @@ -898,14 +1189,10 @@ OrthoProject3d priv::create_emboss_projection( return OrthoProject3d(from_front_to_back); } -namespace { - 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); + double shape_scale = input.shape.scale; const SurfaceVolumeData::ModelSource *biggest = &sources.front(); @@ -918,16 +1205,17 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor Transform3d mesh_tr_inv = s.tr.inverse(); Transform3d cut_projection_tr = mesh_tr_inv * tr; std::pair z_range{0., 1.}; - OrthoProject cut_projection = priv::create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + OrthoProject cut_projection = 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()) continue; + if (its.indices.empty()) + continue; if (biggest_count < its.vertices.size()) { biggest_count = its.vertices.size(); biggest = &s; } - size_t source_index = &s - &sources.front(); - size_t its_index = itss.size(); + size_t source_index = &s - &sources.front(); + size_t its_index = itss.size(); s_to_itss[source_index] = its_index; itss.emplace_back(std::move(its)); } @@ -937,29 +1225,30 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor Transform3d tr_inv = biggest->tr.inverse(); Transform3d cut_projection_tr = tr_inv * tr; - size_t itss_index = s_to_itss[biggest - &sources.front()]; + size_t itss_index = s_to_itss[biggest - &sources.front()]; BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); for (const SurfaceVolumeData::ModelSource &s : sources) { - size_t itss_index = s_to_itss[&s - &sources.front()]; - if (itss_index == std::numeric_limits::max()) continue; - if (&s == biggest) + itss_index = s_to_itss[&s - &sources.front()]; + if (itss_index == std::numeric_limits::max()) + continue; + if (&s == biggest) continue; - Transform3d tr = s.tr * tr_inv; - bool fix_reflected = true; + Transform3d tr = s.tr * tr_inv; + bool fix_reflected = true; indexed_triangle_set &its = itss[itss_index]; its_transform(its, tr, fix_reflected); - BoundingBoxf3 bb = bounding_box(its); - mesh_bb.merge(bb); + BoundingBoxf3 its_bb = bounding_box(its); + mesh_bb.merge(its_bb); } // tr_inv = transformation of mesh inverted - Transform3d emboss_tr = cut_projection_tr.inverse(); - BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); + 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 = 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); + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + float projection_ratio = (-z_range.first + safe_extension) / + (z_range.second - z_range.first + 2 * safe_extension); ExPolygons shapes_data; // is used only when text is reflected to reverse polygon points order const ExPolygons *shapes_ptr = &shapes; @@ -990,31 +1279,25 @@ indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transfor if (was_canceled()) return {}; // !! Projection needs to transform cut - OrthoProject3d projection = priv::create_emboss_projection(input.is_outside, fp.emboss, emboss_tr, cut); + OrthoProject3d projection = create_emboss_projection(input.is_outside, input.shape.projection.depth, 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 EmbossShape &es = input1.create_shape(); + if (was_canceled()) return {}; + if (es.shapes_with_ids.empty()) + throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); - const FontProp &prop = tc.style.prop; - FontFileWithCache &font = input1.font_file; - double shape_scale = get_shape_scale(prop, *font.font_file); - + assert(get_count_lines(es.shapes_with_ids) == input1.text_lines.size()); + size_t count_lines = input1.text_lines.size(); + std::vector bbs = create_line_bounds(es.shapes_with_ids, count_lines); + // half of font em size for direction of letter emboss - double em_2_mm = prop.size_in_mm / 2.; + double em_2_mm = 5.; // TODO: fix it 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) @@ -1022,7 +1305,7 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in 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); + PolygonPoints samples = sample_slice(line, line_bbs, es.scale); std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); for (size_t i = 0; i < line_bbs.size(); ++i) { @@ -1037,7 +1320,7 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in 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]; + ExPolygons glyph_shape = es.shapes_with_ids[s_i_offset + i].expoly; assert(get_extents(glyph_shape) == glyph_bb); Point offset(-glyph_bb.center().x(), 0); @@ -1045,7 +1328,7 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in s.translate(offset); Transform3d modify = offset_tr * rotate; - Transform3d tr = input2.text_tr * modify; + Transform3d tr = input2.transform * 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); @@ -1061,17 +1344,15 @@ TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &in if (was_canceled()) return {}; if (result.empty()) - throw priv::JobException(_u8L("There is no valid surface for text projection.").c_str()); + throw 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) +template +TriangleMesh cut_surface(DataBase& input1, const SurfaceVolumeData& input2, const Fnc& was_canceled) { - const FontProp &fp = input1.text_configuration.style.prop; - if (fp.per_glyph) + if (!input1.text_lines.empty()) return cut_per_glyph_surface(input1, input2, was_canceled); ExPolygons shapes = create_shape(input1, was_canceled); @@ -1079,7 +1360,7 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 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); + indexed_triangle_set its = cut_surface_to_its(shapes, input2.transform, 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()); @@ -1087,18 +1368,40 @@ TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2 return TriangleMesh(std::move(its)); } -bool priv::process(std::exception_ptr &eptr) { - if (!eptr) return false; +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) +{ + SurfaceVolumeData::ModelSources result; + result.reserve(volumes.size() - 1); + for (const ModelVolume *v : volumes) { + if (text_volume_id.has_value() && v->id().id == *text_volume_id) + continue; + // skip modifiers and negative volumes, ... + if (!v->is_model_part()) + continue; + const TriangleMesh &tm = v->mesh(); + if (tm.empty()) + continue; + if (tm.its.empty()) + continue; + result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()}); + } + return result; +} + +bool process(std::exception_ptr &eptr) +{ + if (!eptr) + return false; try { std::rethrow_exception(eptr); - } catch (priv::JobException &e) { + } catch (JobException &e) { create_message(e.what()); eptr = nullptr; } return true; } -bool priv::finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input) +bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input) { // doesn't care about exception when process was canceled by user if (canceled || input.cancel->load()) { @@ -1108,6 +1411,165 @@ bool priv::finalize(bool canceled, std::exception_ptr &eptr, const DataBase &inp return !process(eptr); } -void priv::create_message(const std::string &message) { +bool is_valid(ModelVolumeType volume_type) +{ + assert(volume_type != ModelVolumeType::INVALID); + assert(volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER); + if (volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER) + return true; + + BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int) volume_type; + return false; +} + +bool start_create_volume_job(Worker &worker, + const ModelObject &object, + const std::optional &volume_tr, + DataBasePtr data, + ModelVolumeType volume_type, + GLGizmosManager::EType gizmo) +{ + bool &use_surface = data->shape.projection.use_surface; + std::unique_ptr job; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_sources(object.volumes); + if (sources.empty() || !volume_tr.has_value()) { + use_surface = false; + } else { + SurfaceVolumeData sfvd{*volume_tr, std::move(sources)}; + CreateSurfaceVolumeData surface_data{std::move(sfvd), std::move(data), volume_type, object.id(), gizmo}; + job = std::make_unique(std::move(surface_data)); + } + } + if (!use_surface) { + // create volume + DataCreateVolume create_volume_data{std::move(data), volume_type, object.id(), volume_tr, gizmo}; + job = std::make_unique(std::move(create_volume_data)); + } + return queue_job(worker, std::move(job)); +} + +const GLVolume *find_closest( + const Selection &selection, const Vec2d &screen_center, const Camera &camera, const ModelObjectPtrs &objects, Vec2d *closest_center) +{ + assert(closest_center != nullptr); + const GLVolume *closest = nullptr; + const Selection::IndicesList &indices = selection.get_volume_idxs(); + assert(!indices.empty()); // no selected volume + if (indices.empty()) + return closest; + + double center_sq_distance = std::numeric_limits::max(); + for (unsigned int id : indices) { + const GLVolume *gl_volume = selection.get_volume(id); + if (const ModelVolume *volume = get_model_volume(*gl_volume, objects); + volume == nullptr || !volume->is_model_part()) + continue; + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *gl_volume); + Vec2d c = hull.centroid().cast(); + Vec2d d = c - screen_center; + bool is_bigger_x = std::fabs(d.x()) > std::fabs(d.y()); + if ((is_bigger_x && d.x() * d.x() > center_sq_distance) || + (!is_bigger_x && d.y() * d.y() > center_sq_distance)) + continue; + + double distance = d.squaredNorm(); + if (center_sq_distance < distance) + continue; + center_sq_distance = distance; + + *closest_center = c; + closest = gl_volume; + } + return closest; +} + +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor) +{ + const Pointfs &bed_shape = input.build_volume.bed_shape(); + auto gizmo_type = static_cast(input.gizmo); + DataCreateObject data{std::move(emboss_data), coor, input.camera, bed_shape, gizmo_type}; + auto job = std::make_unique(std::move(data)); + return queue_job(input.worker, std::move(job)); +} + +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &screen_coor, bool try_no_coor) +{ + auto on_bad_state = [&input, try_no_coor](DataBasePtr data_, const ModelObject *object = nullptr) { + if (try_no_coor) { + // Can't create on coordinate try to create somewhere + return start_create_volume_without_position(input, std::move(data_)); + } else { + // In centroid of convex hull is not hit with object. e.g. torid + // soo create transfomation on border of object + + // there is no point on surface so no use of surface will be applied + if (data_->shape.projection.use_surface) + data_->shape.projection.use_surface = false; + + if (object == nullptr) + return false; + + auto gizmo_type = static_cast(input.gizmo); + return start_create_volume_job(input.worker, *object, {}, std::move(data_), input.volume_type, gizmo_type); + } + }; + + assert(input.gl_volume != nullptr); + if (input.gl_volume == nullptr) + return on_bad_state(std::move(data)); + + const Model *model = input.canvas.get_model(); + + assert(model != nullptr); + if (model == nullptr) + return on_bad_state(std::move(data)); + + const ModelObjectPtrs &objects = model->objects; + const ModelVolume *volume = get_model_volume(*input.gl_volume, objects); + assert(volume != nullptr); + if (volume == nullptr) + return on_bad_state(std::move(data)); + + const ModelInstance *instance = get_model_instance(*input.gl_volume, objects); + assert(instance != nullptr); + if (instance == nullptr) + return on_bad_state(std::move(data)); + + const ModelObject *object = volume->get_object(); + assert(object != nullptr); + if (object == nullptr) + return on_bad_state(std::move(data)); + + auto cond = RaycastManager::AllowVolumes({volume->id().id}); + RaycastManager::Meshes meshes = create_meshes(input.canvas, cond); + input.raycaster.actualize(*instance, &cond, &meshes); + std::optional hit = ray_from_camera(input.raycaster, screen_coor, input.camera, &cond); + + // context menu for add text could be open only by right click on an + // object. After right click, object is selected and object_idx is set + // also hit must exist. But there is options to add text by object list + if (!hit.has_value()) + // When model is broken. It could appear that hit miss the object. + // So add part near by in simmilar manner as right panel do + return on_bad_state(std::move(data), object); + + // Create result volume transformation + Transform3d surface_trmat = create_transformation_onto_surface(hit->position, hit->normal, Slic3r::GUI::up_limit); + apply_transformation(input.angle, input.distance, surface_trmat); + Transform3d transform = instance->get_matrix().inverse() * surface_trmat; + auto gizmo_type = static_cast(input.gizmo); + // Try to cast ray into scene and find object for add volume + return start_create_volume_job(input.worker, *object, transform, std::move(data), input.volume_type, gizmo_type); +} + +void create_message(const std::string &message) { show_error(nullptr, message.c_str()); } + +} // namespace diff --git a/src/slic3r/GUI/Jobs/EmbossJob.hpp b/src/slic3r/GUI/Jobs/EmbossJob.hpp index 958433751a..8dec29572c 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.hpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.hpp @@ -9,42 +9,80 @@ #include #include #include -#include "slic3r/Utils/RaycastManager.hpp" +#include // ExPolygonsWithIds +#include "libslic3r/Point.hpp" // Transform3d +#include "libslic3r/ObjectID.hpp" + #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/TextLines.hpp" + #include "Job.hpp" +// forward declarations namespace Slic3r { -class ModelVolume; class TriangleMesh; -} +class ModelVolume; +enum class ModelVolumeType : int; +class BuildVolume; +namespace GUI { +class RaycastManager; +class Plater; +class GLCanvas3D; +class Worker; +class Selection; +}} namespace Slic3r::GUI::Emboss { /// -/// Base data holder for embossing +/// Base data hold data for create emboss shape /// -struct DataBase +class DataBase { - // Keep pointer on Data of font (glyph shapes) - Slic3r::Emboss::FontFileWithCache font_file; - // font item is not used for create object - TextConfiguration text_configuration; - // new volume name created from text - std::string volume_name; +public: + DataBase(const std::string& volume_name, std::shared_ptr> cancel) + : volume_name(volume_name), cancel(std::move(cancel)) {} + DataBase(const std::string& volume_name, std::shared_ptr> cancel, EmbossShape&& shape) + : volume_name(volume_name), cancel(std::move(cancel)), shape(std::move(shape)){} + DataBase(DataBase &&) = default; + virtual ~DataBase() = default; + + /// + /// Create shape + /// e.g. Text extract glyphs from font + /// Not 'const' function because it could modify shape + /// + virtual EmbossShape& create_shape() { return shape; }; + + /// + /// Write data how to reconstruct shape to volume + /// + /// Data object for store emboss params + virtual void write(ModelVolume &volume) const; // Define projection move - // True (raised) .. move outside from surface - // False (engraved).. move into object - bool is_outside; + // True (raised) .. move outside from surface (MODEL_PART) + // False (engraved).. move into object (NEGATIVE_VOLUME) + bool is_outside = true; + + // Define per letter projection on one text line + // [optional] It is not used when empty + Slic3r::Emboss::TextLines text_lines = {}; + + // [optional] Define distance for surface + // It is used only for flat surface (not cutted) + // Position of Zero(not set value) differ for MODEL_PART and NEGATIVE_VOLUME + std::optional from_surface; + + // new volume name + std::string volume_name; // 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; + // shape to emboss + EmbossShape shape; }; /// @@ -63,60 +101,16 @@ struct DataCreateVolume : public DataBase // new created volume transformation Transform3d trmat; }; - -/// -/// Create new TextVolume on the surface of ModelObject -/// Should not be stopped -/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! -/// -class CreateVolumeJob : public Job -{ - DataCreateVolume m_input; - TriangleMesh m_result; - -public: - CreateVolumeJob(DataCreateVolume&& input); - void process(Ctl &ctl) override; - void finalize(bool canceled, std::exception_ptr &eptr) override; -}; - -/// -/// Hold neccessary data to create ModelObject in job -/// Object is placed on bed under screen coor -/// OR to center of scene when it is out of bed shape -/// -struct DataCreateObject : public DataBase -{ - // define position on screen where to create object - Vec2d screen_coor; - - // projection property - Camera camera; - - // shape of bed in case of create volume on bed - std::vector bed_shape; -}; - -/// -/// Create new TextObject on the platter -/// Should not be stopped -/// -class CreateObjectJob : public Job -{ - DataCreateObject m_input; - TriangleMesh m_result; - Transform3d m_transformation; -public: - CreateObjectJob(DataCreateObject&& input); - void process(Ctl &ctl) override; - void finalize(bool canceled, std::exception_ptr &eptr) override; -}; +using DataBasePtr = std::unique_ptr; /// /// Hold neccessary data to update embossed text object in job /// -struct DataUpdate : public DataBase +struct DataUpdate { + // Hold data about shape + DataBasePtr base; + // unique identifier of volume to change ObjectID volume_id; }; @@ -132,7 +126,7 @@ class UpdateJob : public Job public: // move params to private variable - UpdateJob(DataUpdate &&input); + explicit UpdateJob(DataUpdate &&input); /// /// Create new embossed volume by m_input data and store to m_result @@ -154,18 +148,14 @@ public: /// /// Volume to be updated /// New Triangle mesh for volume - /// Parametric description of volume - /// Name of volume - static void update_volume(ModelVolume *volume, - TriangleMesh &&mesh, - const TextConfiguration &text_configuration, - std::string_view volume_name); + /// Data to write into volume + static void update_volume(ModelVolume *volume, TriangleMesh &&mesh, const DataBase &base); }; struct SurfaceVolumeData { - // Transformation of text volume inside of object - Transform3d text_tr; + // Transformation of volume inside of object + Transform3d transform; struct ModelSource { @@ -178,66 +168,98 @@ struct SurfaceVolumeData ModelSources sources; }; -/// -/// Hold neccessary data to create(cut) volume from surface object in job -/// -struct CreateSurfaceVolumeData : public DataBase, public SurfaceVolumeData{ - // define embossed volume type - ModelVolumeType volume_type; - - // parent ModelObject index where to create volume - ObjectID object_id; -}; - -/// -/// Cut surface from object and create cutted volume -/// Should not be stopped -/// -class CreateSurfaceVolumeJob : public Job -{ - CreateSurfaceVolumeData m_input; - TriangleMesh m_result; - -public: - CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input); - void process(Ctl &ctl) override; - void finalize(bool canceled, std::exception_ptr &eptr) override; -}; - /// /// Hold neccessary data to update embossed text object in job /// struct UpdateSurfaceVolumeData : public DataUpdate, public SurfaceVolumeData{}; -/// -/// Copied triangles from object to be able create mesh for cut surface from -/// -/// Source object volumes for cut surface from -/// Source volume id -/// Source data for cut surface from -SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id = {}); - -/// -/// Copied triangles from object to be able create mesh for cut surface from -/// -/// Define text in object -/// Source data for cut surface from -SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume); - /// /// Update text volume to use surface from object /// class UpdateSurfaceVolumeJob : public Job { UpdateSurfaceVolumeData m_input; - TriangleMesh m_result; + TriangleMesh m_result; public: // move params to private variable - UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input); + explicit UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input); void process(Ctl &ctl) override; void finalize(bool canceled, std::exception_ptr &eptr) override; }; + +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Define embossed volume +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &volume); + +/// +/// shorten params for start_crate_volume functions +/// +struct CreateVolumeParams +{ + GLCanvas3D &canvas; + + // Direction of ray into scene + const Camera &camera; + + // To put new object on the build volume + const BuildVolume &build_volume; + + // used to emplace job for execution + Worker &worker; + + // New created volume type + ModelVolumeType volume_type; + + // Contain AABB trees from scene + RaycastManager &raycaster; + + // Define which gizmo open on the success + unsigned char gizmo; // GLGizmosManager::EType + + // Volume define object to add new volume + const GLVolume *gl_volume; + + // Wanted additionl move in Z(emboss) direction of new created volume + std::optional distance = {}; + + // Wanted additionl rotation around Z of new created volume + std::optional angle = {}; +}; + +/// +/// Create new volume on position of mouse cursor +/// +/// canvas + camera + bed shape + +/// Shape of emboss +/// New created volume type +/// Knows object in scene +/// Define which gizmo open on the success - enum GLGizmosManager::EType +/// Define position where to create volume +/// Wanted additionl move in Z(emboss) direction of new created volume +/// Wanted additionl rotation around Z of new created volume +/// True on success otherwise False +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos); + +/// +/// Same as previous function but without mouse position +/// Need to suggest position or put near the selection +/// +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data); + +/// +/// Start job for update embossed volume +/// +/// define update data +/// Volume to be updated +/// Keep model and gl_volumes - when start use surface volume must be selected +/// Could cast ray to scene +/// True when start job otherwise false +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager &raycaster); + } // namespace Slic3r::GUI #endif // slic3r_EmbossJob_hpp_ diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index cdf490d8f2..1ee12c1fb5 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1392,10 +1392,10 @@ void MainFrame::init_menubar_as_editor() append_submenu(fileMenu, export_menu, wxID_ANY, _L("&Export"), ""); wxMenu* convert_menu = new wxMenu(); - append_menu_item(convert_menu, wxID_ANY, _L("Convert ascii G-code to &binary") + dots, _L("Convert a G-code file from ascii to binary format"), + append_menu_item(convert_menu, wxID_ANY, _L("Convert ASCII G-code to &binary") + dots, _L("Convert a G-code file from ASCII to binary format"), [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->convert_gcode_to_binary(); }, "convert_file", nullptr, []() { return true; }, this); - append_menu_item(convert_menu, wxID_ANY, _L("Convert binary G-code to &ascii") + dots, _L("Convert a G-code file from binary to ascii format"), + append_menu_item(convert_menu, wxID_ANY, _L("Convert binary G-code to &ASCII") + dots, _L("Convert a G-code file from binary to ASCII format"), [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->convert_gcode_to_ascii(); }, "convert_file", nullptr, []() { return true; }, this); append_submenu(fileMenu, convert_menu, wxID_ANY, _L("&Convert"), ""); diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index f8981cf706..babbbc0d7c 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -80,6 +80,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* pare const wxString& sub_obj_name, Slic3r::ModelVolumeType type, const bool is_text_volume, + const bool is_svg_volume, const wxString& extruder, const int idx/* = -1*/) : m_parent(parent), @@ -87,6 +88,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* pare m_type(itVolume), m_volume_type(type), m_is_text_volume(is_text_volume), + m_is_svg_volume(is_svg_volume), m_idx(idx), m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : "") { @@ -338,6 +340,7 @@ ObjectDataViewModel::ObjectDataViewModel() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); + m_svg_volume_bmps = MenuFactory::get_svg_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); @@ -360,7 +363,10 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) bool is_volume_node = vol_type >= 0; if (!node->has_warning_icon() && !node->has_lock()) { - node->SetBitmap(is_volume_node ? (node->is_text_volume() ? *m_text_volume_bmps.at(vol_type) : *m_volume_bmps.at(vol_type)) : m_empty_bmp); + node->SetBitmap(is_volume_node ? ( + node->is_text_volume() ? *m_text_volume_bmps.at(vol_type) : + node->is_svg_volume() ? *m_svg_volume_bmps.at(vol_type) : + *m_volume_bmps.at(vol_type)) : m_empty_bmp); return; } @@ -381,7 +387,10 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) if (node->has_lock()) bmps.emplace_back(&m_lock_bmp); if (is_volume_node) - bmps.emplace_back(node->is_text_volume() ? m_text_volume_bmps[vol_type] : m_volume_bmps[vol_type]); + bmps.emplace_back( + node->is_text_volume() ? m_text_volume_bmps[vol_type] : + node->is_svg_volume() ? m_svg_volume_bmps[vol_type] : + m_volume_bmps[vol_type]); bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); } @@ -418,6 +427,7 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent const int volume_idx, const Slic3r::ModelVolumeType volume_type, const bool is_text_volume, + const bool is_svg_volume, const std::string& warning_icon_name, const wxString& extruder) { @@ -429,7 +439,7 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent if (insert_position < 0) insert_position = get_root_idx(root, itInstanceRoot); - const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_text_volume, extruder, volume_idx); + const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_text_volume, is_svg_volume, extruder, volume_idx); UpdateBitmapForNode(node, warning_icon_name, root->has_lock() && volume_type < ModelVolumeType::PARAMETER_MODIFIER); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); @@ -1688,6 +1698,7 @@ void ObjectDataViewModel::UpdateBitmaps() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); + m_svg_volume_bmps = MenuFactory::get_svg_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 9d95ec4aaa..b366abe51e 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -89,6 +89,7 @@ class ObjectDataViewModelNode std::string m_action_icon_name = ""; ModelVolumeType m_volume_type{ -1 }; bool m_is_text_volume{ false }; + bool m_is_svg_volume{false}; InfoItemType m_info_item_type {InfoItemType::Undef}; public: @@ -107,6 +108,7 @@ public: const wxString& sub_obj_name, Slic3r::ModelVolumeType type, const bool is_text_volume, + const bool is_svg_volume, const wxString& extruder, const int idx = -1 ); @@ -242,6 +244,7 @@ public: bool update_settings_digest(const std::vector& categories); int volume_type() const { return int(m_volume_type); } bool is_text_volume() const { return m_is_text_volume; } + bool is_svg_volume() const { return m_is_svg_volume; } void sys_color_changed(); #ifndef NDEBUG @@ -268,7 +271,8 @@ class ObjectDataViewModel :public wxDataViewModel { std::vector m_objects; std::vector m_volume_bmps; - std::vector m_text_volume_bmps; + std::vector m_text_volume_bmps; + std::vector m_svg_volume_bmps; std::map m_info_bmps; wxBitmapBundle m_empty_bmp; wxBitmapBundle m_warning_bmp; @@ -290,6 +294,7 @@ public: const int volume_idx, const Slic3r::ModelVolumeType volume_type, const bool is_text_volume, + const bool is_svg_volume, const std::string& warning_icon_name, const wxString& extruder); wxDataViewItem AddSettingsChild(const wxDataViewItem &parent_item); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 1c9c8162c6..0bd5f21312 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -127,6 +127,7 @@ #include "MsgDialog.hpp" #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GLGizmoCut.hpp" #include "FileArchiveDialog.hpp" @@ -1641,6 +1642,29 @@ private: Plater& m_plater; }; +namespace { +bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) +{ + std::string svg_file_str = into_u8(svg_file); + GLCanvas3D* canvas = plater.canvas3D(); + if (canvas == nullptr) + return false; + auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg); + if (base_svg == nullptr) + return false; + GLGizmoSVG* svg = dynamic_cast(base_svg); + if (svg == nullptr) + return false; + + // Refresh hover state to find surface point under mouse + wxMouseEvent evt(wxEVT_MOTION); + evt.SetPosition(wxPoint(mouse_drop_position.x(), mouse_drop_position.y())); + canvas->on_mouse(evt); // call render where is call GLCanvas3D::_picking_pass() + + return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART); +} +} + bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) { #ifdef WIN32 @@ -1652,6 +1676,20 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi m_mainframe.select_tab(size_t(0)); if (wxGetApp().is_editor()) m_plater.select_view_3D("3D"); + + // When only one .svg file is dropped on scene + if (filenames.size() == 1) { + const wxString &filename = filenames.Last(); + const wxString file_extension = filename.substr(filename.length() - 4); + if (file_extension.CmpNoCase(".svg") == 0) { + const wxPoint offset = m_plater.GetPosition(); + Vec2d mouse_position(x - offset.x, y - offset.y); + // Scale for retina displays + const GLCanvas3D *canvas = m_plater.canvas3D(); + canvas->apply_retina_scale(mouse_position); + return emboss_svg(m_plater, filename, mouse_position); + } + } bool res = m_plater.load_files(filenames); m_mainframe.update_title(); return res; @@ -4471,9 +4509,13 @@ void Plater::priv::on_right_click(RBtnEvent& evt) const bool is_part = selection.is_single_volume_or_modifier() && ! selection.is_any_connector(); if (is_some_full_instances) menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); - else if (is_part) - menu = selection.is_single_text() ? menus.text_part_menu() : menus.part_menu(); - else + else if (is_part) { + const GLVolume* gl_volume = selection.get_first_volume(); + const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); + menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : + (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); + } else menu = menus.multi_selection_menu(); // } } @@ -6816,6 +6858,105 @@ void Plater::export_amf() } } +namespace { +std::string get_file_name(const std::string &file_path) +{ + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + size_t pos_point = file_path.find_last_of('.'); + size_t offset = pos_last_delimiter + 1; + size_t count = pos_point - pos_last_delimiter - 1; + return file_path.substr(offset, count); +} +using SvgFile = EmbossShape::SvgFile; +using SvgFiles = std::vector; +std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs) +{ + // const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp + std::string path_in_3mf = "3D/" + file + ".svg"; + size_t suffix_number = 0; + bool is_unique = false; + do{ + is_unique = true; + path_in_3mf = "3D/" + file + ((suffix_number++)? ("_" + std::to_string(suffix_number)) : "") + ".svg"; + for (SvgFile *svgfile : svgs) { + if (svgfile->path_in_3mf.empty()) + continue; + if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) { + is_unique = false; + break; + } + } + } while (!is_unique); + return path_in_3mf; +} + +bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs) +{ + // Try to find already used svg file + for (SvgFile *svg_ : svgs) { + if (svg_->path_in_3mf.empty()) + continue; + if (svg.path.compare(svg_->path) == 0) { + svg.path_in_3mf = svg_->path_in_3mf; + return true; + } + } + return false; +} + +/// +/// Function to secure private data before store to 3mf +/// +/// Data(also private) to clean before publishing +void publish(Model &model) { + + // SVG file publishing + bool exist_new = false; + SvgFiles svgfiles; + for (ModelObject *object: model.objects){ + for (ModelVolume *volume : object->volumes) { + if (!volume->emboss_shape.has_value()) + continue; + if (volume->text_configuration.has_value()) + continue; // text dosen't have svg path + + SvgFile* svg = &volume->emboss_shape->svg_file; + if (svg->path_in_3mf.empty()) + exist_new = true; + svgfiles.push_back(svg); + } + } + + if (exist_new){ + MessageDialog dialog(nullptr, + _L("Are you sure you want to store original SVGs with their local paths into the 3MF file?\n " + "If you hit 'NO', all SVGs in the project will not be editable any more."), + _L("Private protection"), wxYES_NO | wxICON_QUESTION); + if (dialog.ShowModal() == wxID_NO){ + for (ModelObject *object : model.objects) + for (ModelVolume *volume : object->volumes) + if (volume->emboss_shape.has_value()) + volume->emboss_shape.reset(); + } + } + + for (SvgFile* svgfile : svgfiles){ + if (!svgfile->path_in_3mf.empty()) + continue; // already suggested path (previous save) + + // create unique name for svgs, when local path differ + std::string filename = "unknown"; + if (!svgfile->path.empty()) { + if (set_by_local_path(*svgfile, svgfiles)) + continue; + // check whether original filename is already in: + filename = get_file_name(svgfile->path); + } + svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); + } +} +} + bool Plater::export_3mf(const boost::filesystem::path& output_path) { if (p->model.objects.empty()) { @@ -6836,6 +6977,10 @@ bool Plater::export_3mf(const boost::filesystem::path& output_path) if (!path.Lower().EndsWith(".3mf")) return false; + // take care about private data stored into .3mf + // modify model + publish(p->model); + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; @@ -7945,6 +8090,7 @@ void Plater::bring_instance_forward() wxMenu* Plater::object_menu() { return p->menus.object_menu(); } wxMenu* Plater::part_menu() { return p->menus.part_menu(); } wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } +wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } wxMenu* Plater::default_menu() { return p->menus.default_menu(); } wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 8b3e105626..b311dfcd5a 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -493,6 +493,7 @@ public: wxMenu* object_menu(); wxMenu* part_menu(); wxMenu* text_part_menu(); + wxMenu* svg_part_menu(); wxMenu* sla_object_menu(); wxMenu* default_menu(); wxMenu* instance_menu(); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 087ce33cc5..9e5e68ac59 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -3028,5 +3028,30 @@ const GLVolume *get_selected_gl_volume(const Selection &selection) return selection.get_volume(volume_idx); } +ModelVolume *get_selected_volume(const ObjectID &volume_id, const Selection &selection) { + const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); + const ModelObjectPtrs &model_objects = selection.get_model()->objects; + for (auto id : volume_ids) { + const GLVolume *selected_volume = selection.get_volume(id); + const GLVolume::CompositeID &cid = selected_volume->composite_id; + ModelObject *obj = model_objects[cid.object_id]; + ModelVolume *volume = obj->volumes[cid.volume_id]; + if (volume_id == volume->id()) + return volume; + } + return nullptr; +} + +ModelVolume *get_volume(const ObjectID &volume_id, const Selection &selection) { + const ModelObjectPtrs &objects = selection.get_model()->objects; + for (const ModelObject *object : objects) { + for (ModelVolume *volume : object->volumes) { + if (volume->id() == volume_id) + return volume; + } + } + return nullptr; +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index c9fe54ea94..a64519f2e8 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -18,6 +18,7 @@ class Shader; class Model; class ModelObject; class ModelVolume; +class ObjectID; class GLVolume; class GLArrow; class GLCurvedArrow; @@ -403,9 +404,12 @@ private: const Transform3d& transform, const Vec3d& world_pivot); }; -ModelVolume *get_selected_volume(const Selection &selection); +ModelVolume *get_selected_volume (const Selection &selection); const GLVolume *get_selected_gl_volume(const Selection &selection); +ModelVolume *get_selected_volume (const ObjectID &volume_id, const Selection &selection); +ModelVolume *get_volume (const ObjectID &volume_id, const Selection &selection); + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/SurfaceDrag.cpp b/src/slic3r/GUI/SurfaceDrag.cpp index 6d5cee8c6e..0730811c65 100644 --- a/src/slic3r/GUI/SurfaceDrag.cpp +++ b/src/slic3r/GUI/SurfaceDrag.cpp @@ -12,8 +12,57 @@ #include "slic3r/GUI/I18N.hpp" #include "libslic3r/Emboss.hpp" -namespace Slic3r::GUI { +using namespace Slic3r; +using namespace Slic3r::GUI; +namespace{ +// Distance of embossed volume from surface to be represented as distance surface +// Maximal distance is also enlarge by size of emboss depth +constexpr Slic3r::MinMax surface_distance_sq{1e-4, 10.}; // [in mm] + +/// +/// Extract position of mouse from mouse event +/// +/// Event +/// Position +Vec2d mouse_position(const wxMouseEvent &mouse_event); + +/// +/// Start dragging +/// +/// +/// +/// +/// +/// +/// +/// True on success start otherwise false +bool start_dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional &up_limit); + +/// +/// During dragging +/// +/// +/// +/// +/// +/// +/// +/// +bool dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + const RaycastManager &raycast_manager, + const std::optional &up_limit); +} + +namespace Slic3r::GUI { // Calculate scale in world for check in debug [[maybe_unused]] static std::optional calc_scale(const Matrix3d &from, const Matrix3d &to, const Vec3d &dir) { @@ -52,189 +101,16 @@ bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, if (mouse_event.Moving()) return false; - // detect start text dragging - if (mouse_event.LeftDown()) { - // selected volume - GLVolume *gl_volume = get_selected_gl_volume(canvas); - if (gl_volume == nullptr) - return false; - - // is selected volume closest hovered? - const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; - if (int hovered_idx = canvas.get_first_hover_volume_idx(); - hovered_idx < 0) - return false; - else if (auto hovered_idx_ = static_cast(hovered_idx); - hovered_idx_ >= gl_volumes.size() || - gl_volumes[hovered_idx_] != gl_volume) - return false; - - const ModelObjectPtrs &objects = canvas.get_model()->objects; - const ModelObject *object = get_model_object(*gl_volume, objects); - assert(object != nullptr); - if (object == nullptr) - return false; - - const ModelInstance *instance = get_model_instance(*gl_volume, *object); - const ModelVolume *volume = get_model_volume(*gl_volume, *object); - assert(instance != nullptr && volume != nullptr); - if (object == nullptr || instance == nullptr || volume == nullptr) - return false; - - // allowed drag&drop by canvas for object - if (volume->is_the_only_one_part()) - return false; - - const ModelVolumePtrs &volumes = object->volumes; - std::vector allowed_volumes_id; - if (volumes.size() > 1) { - allowed_volumes_id.reserve(volumes.size() - 1); - for (auto &v : volumes) { - // skip actual selected object - if (v->id() == volume->id()) - continue; - // drag only above part not modifiers or negative surface - if (!v->is_model_part()) - continue; - allowed_volumes_id.emplace_back(v->id().id); - } - } - RaycastManager::AllowVolumes condition(std::move(allowed_volumes_id)); - RaycastManager::Meshes meshes = create_meshes(canvas, condition); - // initialize raycasters - // INFO: It could slows down for big objects - // (may be move to thread and do not show drag until it finish) - raycast_manager.actualize(*instance, &condition, &meshes); - - // wxCoord == int --> wx/types.h - Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - Vec2d mouse_pos = mouse_coord.cast(); - - // world_matrix_fixed() without sla shift - Transform3d to_world = world_matrix_fixed(*gl_volume, objects); - - // zero point of volume in world coordinate system - Vec3d volume_center = to_world.translation(); - // screen coordinate of volume center - Vec2i coor = CameraUtils::project(camera, volume_center); - Vec2d mouse_offset = coor.cast() - mouse_pos; - Vec2d mouse_offset_without_sla_shift = mouse_offset; - if (double sla_shift = gl_volume->get_sla_shift_z(); !is_approx(sla_shift, 0.)) { - Transform3d to_world_without_sla_move = instance->get_matrix() * volume->get_matrix(); - if (volume->text_configuration.has_value() && volume->text_configuration->fix_3mf_tr.has_value()) - to_world_without_sla_move = to_world_without_sla_move * (*volume->text_configuration->fix_3mf_tr); - // zero point of volume in world coordinate system - volume_center = to_world_without_sla_move.translation(); - // screen coordinate of volume center - coor = CameraUtils::project(camera, volume_center); - mouse_offset_without_sla_shift = coor.cast() - mouse_pos; - } - - Transform3d volume_tr = gl_volume->get_volume_transformation().get_matrix(); - - if (volume->text_configuration.has_value()) { - const TextConfiguration &tc = *volume->text_configuration; - // fix baked transformation from .3mf store process - if (tc.fix_3mf_tr.has_value()) - volume_tr = volume_tr * tc.fix_3mf_tr->inverse(); - } - - Transform3d instance_tr = instance->get_matrix(); - Transform3d instance_tr_inv = instance_tr.inverse(); - Transform3d world_tr = instance_tr * volume_tr; - std::optional start_angle; - if (up_limit.has_value()) - start_angle = Emboss::calc_up(world_tr, *up_limit); - surface_drag = SurfaceDrag{mouse_offset, world_tr, instance_tr_inv, gl_volume, condition, start_angle, true, mouse_offset_without_sla_shift}; - - // disable moving with object by mouse - canvas.enable_moving(false); - canvas.enable_picking(false); - return true; - } + if (mouse_event.LeftDown()) + return start_dragging(mouse_position(mouse_event), camera, surface_drag, canvas, raycast_manager, up_limit); // Dragging starts out of window if (!surface_drag.has_value()) return false; - if (mouse_event.Dragging()) { - // wxCoord == int --> wx/types.h - Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - Vec2d mouse_pos = mouse_coord.cast(); - Vec2d offseted_mouse = mouse_pos + surface_drag->mouse_offset_without_sla_shift; + if (mouse_event.Dragging()) + return dragging(mouse_position(mouse_event), camera, surface_drag, canvas, raycast_manager, up_limit); - std::optional hit = ray_from_camera( - raycast_manager, offseted_mouse, camera, &surface_drag->condition); - - surface_drag->exist_hit = hit.has_value(); - if (!hit.has_value()) { - // cross hair need redraw - canvas.set_as_dirty(); - return true; - } - - auto world_linear = surface_drag->world.linear(); - // Calculate offset: transformation to wanted position - { - // Reset skew of the text Z axis: - // Project the old Z axis into a new Z axis, which is perpendicular to the old XY plane. - Vec3d old_z = world_linear.col(2); - Vec3d new_z = world_linear.col(0).cross(world_linear.col(1)); - world_linear.col(2) = new_z * (old_z.dot(new_z) / new_z.squaredNorm()); - } - - Vec3d text_z_world = world_linear.col(2); // world_linear * Vec3d::UnitZ() - auto z_rotation = Eigen::Quaternion::FromTwoVectors(text_z_world, hit->normal); - Transform3d world_new = z_rotation * surface_drag->world; - auto world_new_linear = world_new.linear(); - - // Fix direction of up vector to zero initial rotation - if(up_limit.has_value()){ - Vec3d z_world = world_new_linear.col(2); - z_world.normalize(); - Vec3d wanted_up = Emboss::suggest_up(z_world, *up_limit); - - Vec3d y_world = world_new_linear.col(1); - auto y_rotation = Eigen::Quaternion::FromTwoVectors(y_world, wanted_up); - - world_new = y_rotation * world_new; - world_new_linear = world_new.linear(); - } - - // Edit position from right - Transform3d volume_new{Eigen::Translation(surface_drag->instance_inv * hit->position)}; - volume_new.linear() = surface_drag->instance_inv.linear() * world_new_linear; - - // Check that transformation matrix is valid transformation - assert(volume_new.matrix()(0, 0) == volume_new.matrix()(0, 0)); // Check valid transformation not a NAN - if (volume_new.matrix()(0, 0) != volume_new.matrix()(0, 0)) - return true; - - // Check that scale in world did not changed - assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitY()).has_value()); - assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitZ()).has_value()); - - const ModelVolume *volume = get_model_volume(*surface_drag->gl_volume, canvas.get_model()->objects); - if (volume != nullptr && volume->text_configuration.has_value()) { - const TextConfiguration &tc = *volume->text_configuration; - // fix baked transformation from .3mf store process - if (tc.fix_3mf_tr.has_value()) - volume_new = volume_new * (*tc.fix_3mf_tr); - - // apply move in Z direction and rotation by up vector - Emboss::apply_transformation(surface_drag->start_angle, tc.style.prop.distance, volume_new); - } - - // Update transformation for all instances - for (GLVolume *vol : canvas.get_volumes().volumes) { - if (vol->object_idx() != surface_drag->gl_volume->object_idx() || vol->volume_idx() != surface_drag->gl_volume->volume_idx()) - continue; - vol->set_volume_transformation(volume_new); - } - - canvas.set_as_dirty(); - return true; - } return false; } @@ -257,12 +133,11 @@ std::optional calc_surface_offset(const Selection &selection, RaycastMana auto cond = RaycastManager::SkipVolume(volume->id().id); raycast_manager.actualize(*instance, &cond); - Transform3d to_world = world_matrix_fixed(gl_volume, selection.get_model()->objects); - Vec3d point = to_world * Vec3d::Zero(); - Vec3d direction = to_world.linear() * (-Vec3d::UnitZ()); - + Transform3d to_world = world_matrix_fixed(gl_volume, objects); + Vec3d point = to_world.translation(); + Vec3d dir = -get_z_base(to_world); // ray in direction of text projection(from volume zero to z-dir) - std::optional hit_opt = raycast_manager.closest_hit(point, direction, &cond); + std::optional hit_opt = raycast_manager.closest_hit(point, dir, &cond); // Try to find closest point when no hit object in emboss direction if (!hit_opt.has_value()) { @@ -297,6 +172,62 @@ std::optional calc_surface_offset(const Selection &selection, RaycastMana return offset_volume; } +std::optional calc_distance(const GLVolume &gl_volume, RaycastManager &raycaster, GLCanvas3D &canvas) +{ + const ModelObject *object = get_model_object(gl_volume, canvas.get_model()->objects); + assert(object != nullptr); + if (object == nullptr) + return {}; + + const ModelInstance *instance = get_model_instance(gl_volume, *object); + const ModelVolume *volume = get_model_volume(gl_volume, *object); + assert(instance != nullptr && volume != nullptr); + if (object == nullptr || instance == nullptr || volume == nullptr) + return {}; + + if (volume->is_the_only_one_part()) + return {}; + + RaycastManager::AllowVolumes condition = create_condition(object->volumes, volume->id()); + RaycastManager::Meshes meshes = create_meshes(canvas, condition); + raycaster.actualize(*instance, &condition, &meshes); + return calc_distance(gl_volume, raycaster, &condition); +} + +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, const RaycastManager::ISkip *condition) +{ + Transform3d w = gl_volume.world_matrix(); + Vec3d p = w.translation(); + Vec3d dir = -get_z_base(w); + auto hit_opt = raycaster.closest_hit(p, dir, condition); + if (!hit_opt.has_value()) + return {}; + + const RaycastManager::Hit &hit = *hit_opt; + // NOTE: hit.squared_distance is in volume space not world + + const Transform3d &tr = raycaster.get_transformation(hit.tr_key); + Vec3d hit_world = tr * hit.position; + Vec3d p_to_hit = hit_world - p; + double distance_sq = p_to_hit.squaredNorm(); + + // too small distance is calculated as zero distance + if (distance_sq < ::surface_distance_sq.min) + return {}; + + // check maximal distance + const BoundingBoxf3& bb = gl_volume.bounding_box(); + double max_squared_distance = std::max(std::pow(2 * bb.size().z(), 2), ::surface_distance_sq.max); + if (distance_sq > max_squared_distance) + return {}; + + // calculate sign + float sign = (p_to_hit.dot(dir) > 0)? 1.f : -1.f; + + // distiguish sign + return sign * static_cast(sqrt(distance_sq)); +} + Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs &objects) { Transform3d res = gl_volume.world_matrix(); @@ -305,11 +236,11 @@ Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs if (!mv) return res; - const std::optional &tc = mv->text_configuration; - if (!tc.has_value()) + const std::optional &es = mv->emboss_shape; + if (!es.has_value()) return res; - const std::optional &fix = tc->fix_3mf_tr; + const std::optional &fix = es->fix_3mf_tr; if (!fix.has_value()) return res; @@ -326,4 +257,329 @@ Transform3d world_matrix_fixed(const Selection &selection) return world_matrix_fixed(*gl_volume, selection.get_model()->objects); } -} // namespace Slic3r::GUI \ No newline at end of file +void selection_transform(Selection &selection, const std::function &selection_transformation_fnc, const ModelVolume *volume) +{ + GLVolume *gl_volume = selection.get_volume(*selection.get_volume_idxs().begin()); + if (gl_volume == nullptr) + return selection_transformation_fnc(); + + if (volume == nullptr) { + volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume == nullptr) + return selection_transformation_fnc(); + } + + if (!volume->emboss_shape.has_value()) + return selection_transformation_fnc(); + + const std::optional &fix_tr = volume->emboss_shape->fix_3mf_tr; + if (!fix_tr.has_value()) + return selection_transformation_fnc(); + + Transform3d volume_tr = gl_volume->get_volume_transformation().get_matrix(); + gl_volume->set_volume_transformation(volume_tr * fix_tr->inverse()); + selection.setup_cache(); + + selection_transformation_fnc(); + + volume_tr = gl_volume->get_volume_transformation().get_matrix(); + gl_volume->set_volume_transformation(volume_tr * (*fix_tr)); + selection.setup_cache(); +} + +bool face_selected_volume_to_camera(const Camera &camera, GLCanvas3D &canvas) +{ + 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; +} + +void do_local_z_rotate(GLCanvas3D &canvas, double relative_angle) +{ + Selection &selection = canvas.get_selection(); + + assert(!selection.is_empty()); + if(selection.is_empty()) return; + + assert(selection.is_single_full_object() || selection.is_single_volume()); + if (!selection.is_single_full_object() && !selection.is_single_volume()) return; + + // Fix angle for mirrored volume + bool is_mirrored = false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume != nullptr) { + if (selection.is_single_full_object()) { + const ModelInstance *instance = get_model_instance(*gl_volume, selection.get_model()->objects); + if (instance != nullptr) + is_mirrored = has_reflection(instance->get_matrix()); + } else { + // selection.is_single_volume() + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume != nullptr) + is_mirrored = has_reflection(volume->get_matrix()); + } + } + if (is_mirrored) + relative_angle *= -1; + + + selection.setup_cache(); + + auto selection_rotate_fnc = [&selection, &relative_angle](){ + TransformationType transformation_type = selection.is_single_volume() ? + TransformationType::Local_Relative_Independent : + TransformationType::Instance_Relative_Independent; + selection.rotate(Vec3d(0., 0., relative_angle), transformation_type); + }; + selection_transform(selection, selection_rotate_fnc); + + std::string snapshot_name; // empty meand no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended + // inside function do_move + // snapshot_name = L("Set text rotation"); + canvas.do_rotate(snapshot_name); +} + +void do_local_z_move(GLCanvas3D &canvas, double relative_move) { + + Selection &selection = canvas.get_selection(); + assert(!selection.is_empty()); + if (selection.is_empty()) return; + + selection.setup_cache(); + auto selection_translate_fnc = [&selection, relative_move]() { + Vec3d translate = Vec3d::UnitZ() * relative_move; + selection.translate(translate, TransformationType::Local); + }; + selection_transform(selection, selection_translate_fnc); + + std::string snapshot_name; // empty mean no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended inside + // function do_move + // snapshot_name = L("Set surface distance"); + canvas.do_move(snapshot_name); +} + +} // namespace Slic3r::GUI + +// private implementation +namespace { + +Vec2d mouse_position(const wxMouseEvent &mouse_event){ + // wxCoord == int --> wx/types.h + Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + return mouse_coord.cast(); +} + +bool start_dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional&up_limit) +{ + // selected volume + GLVolume *gl_volume_ptr = get_selected_gl_volume(canvas); + if (gl_volume_ptr == nullptr) + return false; + const GLVolume &gl_volume = *gl_volume_ptr; + + // is selected volume closest hovered? + const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; + if (int hovered_idx = canvas.get_first_hover_volume_idx(); hovered_idx < 0) + return false; + else if (auto hovered_idx_ = static_cast(hovered_idx); + hovered_idx_ >= gl_volumes.size() || gl_volumes[hovered_idx_] != gl_volume_ptr) + return false; + + const ModelObjectPtrs &objects = canvas.get_model()->objects; + const ModelObject *object = get_model_object(gl_volume, objects); + assert(object != nullptr); + if (object == nullptr) + return false; + + const ModelInstance *instance = get_model_instance(gl_volume, *object); + const ModelVolume *volume = get_model_volume(gl_volume, *object); + assert(instance != nullptr && volume != nullptr); + if (object == nullptr || instance == nullptr || volume == nullptr) + return false; + + // allowed drag&drop by canvas for object + if (volume->is_the_only_one_part()) + return false; + + RaycastManager::AllowVolumes condition = create_condition(object->volumes, volume->id()); + RaycastManager::Meshes meshes = create_meshes(canvas, condition); + // initialize raycasters + // INFO: It could slows down for big objects + // (may be move to thread and do not show drag until it finish) + raycast_manager.actualize(*instance, &condition, &meshes); + + // world_matrix_fixed() without sla shift + Transform3d to_world = world_matrix_fixed(gl_volume, objects); + + // zero point of volume in world coordinate system + Vec3d volume_center = to_world.translation(); + // screen coordinate of volume center + Vec2i coor = CameraUtils::project(camera, volume_center); + Vec2d mouse_offset = coor.cast() - mouse_pos; + Vec2d mouse_offset_without_sla_shift = mouse_offset; + if (double sla_shift = gl_volume.get_sla_shift_z(); !is_approx(sla_shift, 0.)) { + Transform3d to_world_without_sla_move = instance->get_matrix() * volume->get_matrix(); + if (volume->emboss_shape.has_value() && volume->emboss_shape->fix_3mf_tr.has_value()) + to_world_without_sla_move = to_world_without_sla_move * (*volume->emboss_shape->fix_3mf_tr); + // zero point of volume in world coordinate system + volume_center = to_world_without_sla_move.translation(); + // screen coordinate of volume center + coor = CameraUtils::project(camera, volume_center); + mouse_offset_without_sla_shift = coor.cast() - mouse_pos; + } + + Transform3d volume_tr = gl_volume.get_volume_transformation().get_matrix(); + + // fix baked transformation from .3mf store process + if (const std::optional &es_opt = volume->emboss_shape; es_opt.has_value()) { + const std::optional &fix = es_opt->fix_3mf_tr; + if (fix.has_value()) + volume_tr = volume_tr * fix->inverse(); + } + + Transform3d instance_tr = instance->get_matrix(); + Transform3d instance_tr_inv = instance_tr.inverse(); + Transform3d world_tr = instance_tr * volume_tr; + std::optional start_angle; + if (up_limit.has_value()) + start_angle = Emboss::calc_up(world_tr, *up_limit); + + std::optional start_distance; + if (!volume->emboss_shape->projection.use_surface) + start_distance = calc_distance(gl_volume, raycast_manager, &condition); + surface_drag = SurfaceDrag{mouse_offset, world_tr, instance_tr_inv, + gl_volume_ptr, condition, start_angle, + start_distance, true, mouse_offset_without_sla_shift}; + + // disable moving with object by mouse + canvas.enable_moving(false); + canvas.enable_picking(false); + return true; +} + +bool dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + const RaycastManager &raycast_manager, + const std::optional &up_limit) +{ + Vec2d offseted_mouse = mouse_pos + surface_drag->mouse_offset_without_sla_shift; + std::optional hit = ray_from_camera( + raycast_manager, offseted_mouse, camera, &surface_drag->condition); + + surface_drag->exist_hit = hit.has_value(); + if (!hit.has_value()) { + // cross hair need redraw + canvas.set_as_dirty(); + return true; + } + + auto world_linear = surface_drag->world.linear(); + // Calculate offset: transformation to wanted position + { + // Reset skew of the text Z axis: + // Project the old Z axis into a new Z axis, which is perpendicular to the old XY plane. + Vec3d old_z = world_linear.col(2); + Vec3d new_z = world_linear.col(0).cross(world_linear.col(1)); + world_linear.col(2) = new_z * (old_z.dot(new_z) / new_z.squaredNorm()); + } + + Vec3d text_z_world = world_linear.col(2); // world_linear * Vec3d::UnitZ() + auto z_rotation = Eigen::Quaternion::FromTwoVectors(text_z_world, hit->normal); + Transform3d world_new = z_rotation * surface_drag->world; + auto world_new_linear = world_new.linear(); + + // Fix direction of up vector to zero initial rotation + if(up_limit.has_value()){ + Vec3d z_world = world_new_linear.col(2); + z_world.normalize(); + Vec3d wanted_up = Emboss::suggest_up(z_world, *up_limit); + + Vec3d y_world = world_new_linear.col(1); + auto y_rotation = Eigen::Quaternion::FromTwoVectors(y_world, wanted_up); + + world_new = y_rotation * world_new; + world_new_linear = world_new.linear(); + } + + // Edit position from right + Transform3d volume_new{Eigen::Translation(surface_drag->instance_inv * hit->position)}; + volume_new.linear() = surface_drag->instance_inv.linear() * world_new_linear; + + // Check that transformation matrix is valid transformation + assert(volume_new.matrix()(0, 0) == volume_new.matrix()(0, 0)); // Check valid transformation not a NAN + if (volume_new.matrix()(0, 0) != volume_new.matrix()(0, 0)) + return true; + + // Check that scale in world did not changed + assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitY()).has_value()); + assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitZ()).has_value()); + + const ModelVolume *volume = get_model_volume(*surface_drag->gl_volume, canvas.get_model()->objects); + // fix baked transformation from .3mf store process + if (volume != nullptr && volume->emboss_shape.has_value()) { + const std::optional &fix = volume->emboss_shape->fix_3mf_tr; + if (fix.has_value()) + volume_new = volume_new * (*fix); + + // apply move in Z direction and rotation by up vector + Emboss::apply_transformation(surface_drag->start_angle, surface_drag->start_distance, volume_new); + } + + // Update transformation for all instances + for (GLVolume *vol : canvas.get_volumes().volumes) { + if (vol->object_idx() != surface_drag->gl_volume->object_idx() || vol->volume_idx() != surface_drag->gl_volume->volume_idx()) + continue; + vol->set_volume_transformation(volume_new); + } + + canvas.set_as_dirty(); + return true; +} + +} // namespace diff --git a/src/slic3r/GUI/SurfaceDrag.hpp b/src/slic3r/GUI/SurfaceDrag.hpp index 48d6a33fe2..d1b154a945 100644 --- a/src/slic3r/GUI/SurfaceDrag.hpp +++ b/src/slic3r/GUI/SurfaceDrag.hpp @@ -5,9 +5,11 @@ #include "libslic3r/Point.hpp" // Vec2d, Transform3d #include "slic3r/Utils/RaycastManager.hpp" #include "wx/event.h" // wxMouseEvent +#include namespace Slic3r { class GLVolume; +class ModelVolume; } // namespace Slic3r namespace Slic3r::GUI { @@ -37,6 +39,9 @@ struct SurfaceDrag // initial rotation in Z axis of volume std::optional start_angle; + // initial Z distance from surface + std::optional start_distance; + // Flag whether coordinate hit some volume bool exist_hit = true; @@ -44,6 +49,10 @@ struct SurfaceDrag Vec2d mouse_offset_without_sla_shift; }; +// Limit direction of up vector on model +// Between side and top surface +constexpr double up_limit = 0.9; + /// /// Mouse event handler, when move(drag&drop) volume over model surface /// NOTE: Dragged volume has to be selected. And also has to be hovered on start of dragging. @@ -71,6 +80,16 @@ bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, /// Offset of volume in volume coordinate std::optional calc_surface_offset(const Selection &selection, RaycastManager &raycast_manager); +/// +/// Calculate distance by ray to surface of object in emboss direction +/// +/// Define embossed volume +/// Way to cast rays to object +/// Contain model +/// Calculated distance from surface +std::optional calc_distance(const GLVolume &gl_volume, RaycastManager &raycaster, GLCanvas3D &canvas); +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, const RaycastManager::ISkip *condition); + /// /// Get transformation to world /// - use fix after store to 3mf when exists @@ -89,5 +108,37 @@ Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs& /// Fixed Transformation of selected volume in selection Transform3d world_matrix_fixed(const Selection &selection); +/// +/// Wrap function around selection transformation to apply fix transformation +/// Fix transformation is needed because of (store/load) volume (to/from) 3mf +/// +/// Selected gl volume will be modified +/// Function modified Selection transformation +/// Same as selected GLVolume, volume may(or may not) contain fix matrix, +/// when nullptr it is gathered from selection +void selection_transform(Selection &selection, const std::function& selection_transformation_fnc, const ModelVolume *volume = nullptr); + +/// +/// Apply camera direction for emboss direction +/// +/// Define view vector +/// Containe Selected ModelVolume to modify orientation +/// True when apply change otherwise false +bool face_selected_volume_to_camera(const Camera &camera, GLCanvas3D &canvas); + +/// +/// Rotation around z Axis(emboss direction) +/// +/// Selected volume for rotation +/// Relative angle to rotate around emboss direction +void do_local_z_rotate(GLCanvas3D &canvas, double relative_angle); + +/// +/// Translation along local z Axis (emboss direction) +/// +/// Selected volume for translate +/// Relative move along emboss direction +void do_local_z_move(GLCanvas3D &canvas, double relative_move); + } // namespace Slic3r::GUI #endif // slic3r_SurfaceDrag_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/TextLines.cpp b/src/slic3r/GUI/TextLines.cpp index ca24746be8..258d4916e4 100644 --- a/src/slic3r/GUI/TextLines.cpp +++ b/src/slic3r/GUI/TextLines.cpp @@ -217,25 +217,6 @@ GLModel::Geometry create_geometry(const TextLines &lines, float radius, bool is_ } return geometry; } - -bool get_line_height_offset(const FontProp &fp, const FontFile &ff, double &line_height_mm, double &line_offset_mm) -{ - double third_ascent_shape_size = get_font_info(ff, fp).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; -} } // namespace void TextLinesModel::init(const Transform3d &text_tr, @@ -259,8 +240,9 @@ void TextLinesModel::init(const Transform3d &text_tr, FontProp::VerticalAlign align = fp.align.second; - double line_height_mm, line_offset_mm; - if (!get_line_height_offset(fp, ff, line_height_mm, line_offset_mm)) + double line_height_mm = calc_line_height_in_mm(ff, fp); + assert(line_height_mm > 0); + if (line_height_mm <= 0) return; m_model.reset(); @@ -364,9 +346,9 @@ void TextLinesModel::render(const Transform3d &text_world) shader->stop_using(); } -double TextLinesModel::calc_line_height(const Slic3r::Emboss::FontFile &ff, const FontProp &fp) +double TextLinesModel::calc_line_height_in_mm(const Slic3r::Emboss::FontFile &ff, const FontProp &fp) { int line_height = Slic3r::Emboss::get_line_height(ff, fp); // In shape size - double scale = Slic3r::Emboss::get_shape_scale(fp, ff); + double scale = Slic3r::Emboss::get_text_shape_scale(fp, ff); return line_height * scale; } diff --git a/src/slic3r/GUI/TextLines.hpp b/src/slic3r/GUI/TextLines.hpp index a6a7b19adf..f6e297cc22 100644 --- a/src/slic3r/GUI/TextLines.hpp +++ b/src/slic3r/GUI/TextLines.hpp @@ -32,7 +32,7 @@ public: 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 + static double calc_line_height_in_mm(const Slic3r::Emboss::FontFile& ff, const FontProp& fp); // return lineheight in mm private: Slic3r::Emboss::TextLines m_lines; diff --git a/src/slic3r/Utils/EmbossStyleManager.cpp b/src/slic3r/Utils/EmbossStyleManager.cpp index 564cb163a9..5c368673a1 100644 --- a/src/slic3r/Utils/EmbossStyleManager.cpp +++ b/src/slic3r/Utils/EmbossStyleManager.cpp @@ -3,28 +3,24 @@ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #include "EmbossStyleManager.hpp" +#include #include // Imgui texture #include // ImTextCharFromUtf8 -#include "WxFontUtils.hpp" -#include "libslic3r/Utils.hpp" // ScopeGuard +#include +#include // ScopeGuard +#include "WxFontUtils.hpp" #include "slic3r/GUI/3DScene.hpp" // ::glsafe #include "slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" // check of font ranges -#include "slic3r/Utils/EmbossStylesSerializable.hpp" - using namespace Slic3r; using namespace Slic3r::Emboss; using namespace Slic3r::GUI::Emboss; -StyleManager::StyleManager(const ImWchar *language_glyph_range, std::function create_default_styles) - : m_imgui_init_glyph_range(language_glyph_range) - , m_create_default_styles(create_default_styles) - , m_exist_style_images(false) - , m_temp_style_images(nullptr) - , m_app_config(nullptr) - , m_last_style_index(std::numeric_limits::max()) +StyleManager::StyleManager(const ImWchar *language_glyph_range, const std::function& create_default_styles) + : m_create_default_styles(create_default_styles) + , m_imgui_init_glyph_range(language_glyph_range) {} StyleManager::~StyleManager() { @@ -32,53 +28,66 @@ StyleManager::~StyleManager() { free_style_images(); } +/// +/// For store/load emboss style to/from AppConfig +/// +namespace { +void store_style_index(AppConfig &cfg, size_t index); +::std::optional load_style_index(const AppConfig &cfg); + +StyleManager::Styles load_styles(const AppConfig &cfg); +void store_styles(AppConfig &cfg, const StyleManager::Styles &styles); +void make_unique_name(const StyleManager::Styles &styles, std::string &name); +} // namespace + void StyleManager::init(AppConfig *app_config) { + assert(app_config != nullptr); m_app_config = app_config; - EmbossStyles styles = (app_config != nullptr) ? - EmbossStylesSerializable::load_styles(*app_config) : - EmbossStyles{}; - if (styles.empty()) - styles = m_create_default_styles(); - for (EmbossStyle &style : styles) { - make_unique_name(style.name); - m_style_items.push_back({style}); + m_styles = ::load_styles(*app_config); + + if (m_styles.empty()) { + // No styles loaded from ini file so use default + EmbossStyles styles = m_create_default_styles(); + for (EmbossStyle &style : styles) { + ::make_unique_name(m_styles, style.name); + m_styles.push_back({style}); + } } std::optional active_index_opt = (app_config != nullptr) ? - EmbossStylesSerializable::load_style_index(*app_config) : + ::load_style_index(*app_config) : std::optional{}; size_t active_index = 0; if (active_index_opt.has_value()) active_index = *active_index_opt; - if (active_index >= m_style_items.size()) active_index = 0; + if (active_index >= m_styles.size()) active_index = 0; // find valid font item if (load_style(active_index)) return; // style is loaded // Try to fix that style can't be loaded - m_style_items.erase(m_style_items.begin() + active_index); + m_styles.erase(m_styles.begin() + active_index); load_valid_style(); } -bool StyleManager::store_styles_to_app_config(bool use_modification, - bool store_active_index) +bool StyleManager::store_styles_to_app_config(bool use_modification, bool store_active_index) { assert(m_app_config != nullptr); if (m_app_config == nullptr) return false; if (use_modification) { if (exist_stored_style()) { // update stored item - m_style_items[m_style_cache.style_index].style = m_style_cache.style; + m_styles[m_style_cache.style_index] = m_style_cache.style; } else { // add new into stored list EmbossStyle &style = m_style_cache.style; - make_unique_name(style.name); + ::make_unique_name(m_styles, style.name); m_style_cache.truncated_name.clear(); - m_style_cache.style_index = m_style_items.size(); - m_style_items.push_back({style}); + m_style_cache.style_index = m_styles.size(); + m_styles.push_back({style}); } m_style_cache.stored_wx_font = m_style_cache.wx_font; } @@ -88,30 +97,27 @@ bool StyleManager::store_styles_to_app_config(bool use_modification, size_t style_index = exist_stored_style() ? m_style_cache.style_index : m_last_style_index; - EmbossStylesSerializable::store_style_index(*m_app_config, style_index); + store_style_index(*m_app_config, style_index); } - EmbossStyles styles; - styles.reserve(m_style_items.size()); - for (const Item &item : m_style_items) styles.push_back(item.style); - EmbossStylesSerializable::store_styles(*m_app_config, styles); + store_styles(*m_app_config, m_styles); return true; } void StyleManager::add_style(const std::string &name) { EmbossStyle& style = m_style_cache.style; style.name = name; - make_unique_name(style.name); - m_style_cache.style_index = m_style_items.size(); + ::make_unique_name(m_styles, style.name); + m_style_cache.style_index = m_styles.size(); m_style_cache.stored_wx_font = m_style_cache.wx_font; m_style_cache.truncated_name.clear(); - m_style_items.push_back({style}); + m_styles.push_back({style}); } void StyleManager::swap(size_t i1, size_t i2) { - if (i1 >= m_style_items.size() || - i2 >= m_style_items.size()) return; - std::swap(m_style_items[i1], m_style_items[i2]); + if (i1 >= m_styles.size() || + i2 >= m_styles.size()) return; + std::swap(m_styles[i1], m_styles[i2]); // fix selected index if (!exist_stored_style()) return; if (m_style_cache.style_index == i1) { @@ -135,7 +141,7 @@ void StyleManager::discard_style_changes() { } void StyleManager::erase(size_t index) { - if (index >= m_style_items.size()) return; + if (index >= m_styles.size()) return; // fix selected index if (exist_stored_style()) { @@ -144,15 +150,15 @@ void StyleManager::erase(size_t index) { else if (index == i) i = std::numeric_limits::max(); } - m_style_items.erase(m_style_items.begin() + index); + m_styles.erase(m_styles.begin() + index); } void StyleManager::rename(const std::string& name) { m_style_cache.style.name = name; m_style_cache.truncated_name.clear(); if (exist_stored_style()) { - Item &it = m_style_items[m_style_cache.style_index]; - it.style.name = name; + Style &it = m_styles[m_style_cache.style_index]; + it.name = name; it.truncated_name.clear(); } } @@ -160,28 +166,28 @@ void StyleManager::rename(const std::string& name) { void StyleManager::load_valid_style() { // iterate over all known styles - while (!m_style_items.empty()) { + while (!m_styles.empty()) { if (load_style(0)) return; // can't load so erase it from list - m_style_items.erase(m_style_items.begin()); + m_styles.erase(m_styles.begin()); } // no one style is loadable // set up default font list EmbossStyles def_style = m_create_default_styles(); for (EmbossStyle &style : def_style) { - make_unique_name(style.name); - m_style_items.push_back({std::move(style)}); + ::make_unique_name(m_styles, style.name); + m_styles.push_back({std::move(style)}); } // iterate over default styles // There have to be option to use build in font - while (!m_style_items.empty()) { + while (!m_styles.empty()) { if (load_style(0)) return; // can't load so erase it from list - m_style_items.erase(m_style_items.begin()); + m_styles.erase(m_styles.begin()); } // This OS doesn't have TTF as default font, @@ -191,15 +197,15 @@ void StyleManager::load_valid_style() bool StyleManager::load_style(size_t style_index) { - if (style_index >= m_style_items.size()) return false; - if (!load_style(m_style_items[style_index].style)) return false; + if (style_index >= m_styles.size()) return false; + if (!load_style(m_styles[style_index])) return false; m_style_cache.style_index = style_index; m_style_cache.stored_wx_font = m_style_cache.wx_font; // copy m_last_style_index = style_index; return true; } -bool StyleManager::load_style(const EmbossStyle &style) { +bool StyleManager::load_style(const Style &style) { if (style.type == EmbossStyle::Type::file_path) { std::unique_ptr font_ptr = create_font_file(style.path.c_str()); @@ -212,13 +218,13 @@ bool StyleManager::load_style(const EmbossStyle &style) { m_style_cache.stored_wx_font = {}; return true; } - if (style.type != WxFontUtils::get_actual_type()) return false; + if (style.type != WxFontUtils::get_current_type()) return false; std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); if (!wx_font_opt.has_value()) return false; return load_style(style, *wx_font_opt); } -bool StyleManager::load_style(const EmbossStyle &style, const wxFont &font) +bool StyleManager::load_style(const Style &style, const wxFont &font) { m_style_cache.style = style; // copy @@ -269,12 +275,19 @@ bool StyleManager::is_font_changed() const return is_bold != is_stored_bold; } +bool StyleManager::is_unique_style_name(const std::string &name) const { + for (const StyleManager::Style &style : m_styles) + if (style.name == name) + return false; + return true; +} + bool StyleManager::is_active_font() { return m_style_cache.font_file.has_value(); } -const EmbossStyle* StyleManager::get_stored_style() const +const StyleManager::Style *StyleManager::get_stored_style() const { - if (m_style_cache.style_index >= m_style_items.size()) return nullptr; - return &m_style_items[m_style_cache.style_index].style; + if (m_style_cache.style_index >= m_styles.size()) return nullptr; + return &m_styles[m_style_cache.style_index]; } void StyleManager::clear_glyphs_cache() @@ -286,25 +299,6 @@ 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; @@ -321,44 +315,11 @@ ImFont *StyleManager::get_imgui_font() return font; } -const std::vector &StyleManager::get_styles() const{ return m_style_items; } - -void StyleManager::make_unique_name(std::string &name) -{ - auto is_unique = [&](const std::string &name) -> bool { - for (const Item &it : m_style_items) - if (it.style.name == name) return false; - return true; - }; - - // Style name can't be empty so default name is set - if (name.empty()) name = "Text style"; - - // When name is already unique, nothing need to be changed - if (is_unique(name)) return; - - // when there is previous version of style name only find number - const char *prefix = " ("; - const char suffix = ')'; - auto pos = name.find_last_of(prefix); - if (name.c_str()[name.size() - 1] == suffix && - pos != std::string::npos) { - // short name by ord number - name = name.substr(0, pos); - } - - int order = 1; // start with value 2 to represents same font name - std::string new_name; - do { - new_name = name + prefix + std::to_string(++order) + suffix; - } while (!is_unique(new_name)); - name = new_name; -} - +const StyleManager::Styles &StyleManager::get_styles() const{ return m_styles; } void StyleManager::init_trunc_names(float max_width) { - for (auto &s : m_style_items) + for (auto &s : m_styles) if (s.truncated_name.empty()) { - std::string name = s.style.name; + std::string name = s.name; ImGuiWrapper::escape_double_hash(name); s.truncated_name = ImGuiWrapper::trunc(name, max_width); } @@ -391,9 +352,9 @@ void StyleManager::init_style_images(const Vec2i &max_size, StyleImagesData::Item &style = m_temp_style_images->styles[index]; // find style in font list and copy to it - for (auto &it : m_style_items) { - if (it.style.name != style.text || - !(it.style.prop == style.prop)) + for (auto &it : m_styles) { + if (it.name != style.text || + !(it.prop == style.prop)) continue; it.image = image; break; @@ -410,9 +371,8 @@ void StyleManager::init_style_images(const Vec2i &max_size, // create job for init images m_temp_style_images = std::make_shared(); StyleImagesData::Items styles; - styles.reserve(m_style_items.size()); - for (const Item &item : m_style_items) { - const EmbossStyle &style = item.style; + styles.reserve(m_styles.size()); + for (const Style &style : m_styles) { std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); if (!wx_font_opt.has_value()) continue; std::unique_ptr font_file = @@ -439,7 +399,7 @@ void StyleManager::init_style_images(const Vec2i &max_size, void StyleManager::free_style_images() { if (!m_exist_style_images) return; GLuint tex_id = 0; - for (Item &it : m_style_items) { + for (Style &it : m_styles) { if (tex_id == 0 && it.image.has_value()) tex_id = (GLuint)(intptr_t) it.image->texture_id; it.image.reset(); @@ -551,10 +511,278 @@ bool StyleManager::set_wx_font(const wxFont &wx_font, std::unique_ptr FontFileWithCache(std::move(font_file)); EmbossStyle &style = m_style_cache.style; - style.type = WxFontUtils::get_actual_type(); + style.type = WxFontUtils::get_current_type(); // update string path style.path = WxFontUtils::store_wxFont(wx_font); WxFontUtils::update_property(style.prop, wx_font); clear_imgui_font(); return true; } + +#include +#include "WxFontUtils.hpp" +#include "fast_float/fast_float.h" + +// StylesSerializable +namespace { + +using namespace Slic3r; +using namespace Slic3r::GUI; +using Section = std::map; + +const std::string APP_CONFIG_FONT_NAME = "name"; +const std::string APP_CONFIG_FONT_DESCRIPTOR = "descriptor"; +const std::string APP_CONFIG_FONT_LINE_HEIGHT = "line_height"; +const std::string APP_CONFIG_FONT_DEPTH = "depth"; +const std::string APP_CONFIG_FONT_USE_SURFACE = "use_surface"; +const std::string APP_CONFIG_FONT_BOLDNESS = "boldness"; +const std::string APP_CONFIG_FONT_SKEW = "skew"; +const std::string APP_CONFIG_FONT_DISTANCE = "distance"; +const std::string APP_CONFIG_FONT_ANGLE = "angle"; +const std::string APP_CONFIG_FONT_COLLECTION = "collection"; +const std::string APP_CONFIG_FONT_CHAR_GAP = "char_gap"; +const std::string APP_CONFIG_FONT_LINE_GAP = "line_gap"; + +const std::string APP_CONFIG_ACTIVE_FONT = "active_font"; + +std::string create_section_name(unsigned index) +{ + return AppConfig::SECTION_EMBOSS_STYLE + ':' + std::to_string(index); +} + +// check only existence of flag +bool read(const Section §ion, const std::string &key, bool &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + + value = true; + return true; +} + +bool read(const Section §ion, const std::string &key, float &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) + return false; + + value = value_; + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + int value_ = std::atoi(data.c_str()); + if (value_ == 0) + return false; + + value = value_; + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + int value_ = std::atoi(data.c_str()); + if (value_ <= 0) + return false; + + value = static_cast(value_); + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) + return false; + + value = value_; + return true; +} + +std::optional load_style(const Section &app_cfg_section) +{ + auto path_it = app_cfg_section.find(APP_CONFIG_FONT_DESCRIPTOR); + if (path_it == app_cfg_section.end()) + return {}; + + StyleManager::Style s; + EmbossProjection& ep = s.projection; + FontProp& fp = s.prop; + + s.path = path_it->second; + s.type = WxFontUtils::get_current_type(); + auto name_it = app_cfg_section.find(APP_CONFIG_FONT_NAME); + const std::string default_name = "font_name"; + s.name = (name_it == app_cfg_section.end()) ? default_name : name_it->second; + + read(app_cfg_section, APP_CONFIG_FONT_LINE_HEIGHT, fp.size_in_mm); + float depth = 1.; + read(app_cfg_section, APP_CONFIG_FONT_DEPTH, depth); + ep.depth = depth; + read(app_cfg_section, APP_CONFIG_FONT_USE_SURFACE, ep.use_surface); + read(app_cfg_section, APP_CONFIG_FONT_BOLDNESS, fp.boldness); + read(app_cfg_section, APP_CONFIG_FONT_SKEW, fp.skew); + read(app_cfg_section, APP_CONFIG_FONT_DISTANCE, s.distance); + read(app_cfg_section, APP_CONFIG_FONT_ANGLE, s.angle); + read(app_cfg_section, APP_CONFIG_FONT_COLLECTION, fp.collection_number); + read(app_cfg_section, APP_CONFIG_FONT_CHAR_GAP, fp.char_gap); + read(app_cfg_section, APP_CONFIG_FONT_LINE_GAP, fp.line_gap); + return s; +} + +void store_style(AppConfig &cfg, const StyleManager::Style &s, unsigned index) +{ + const EmbossProjection &ep = s.projection; + Section data; + data[APP_CONFIG_FONT_NAME] = s.name; + data[APP_CONFIG_FONT_DESCRIPTOR] = s.path; + const FontProp &fp = s.prop; + data[APP_CONFIG_FONT_LINE_HEIGHT] = std::to_string(fp.size_in_mm); + data[APP_CONFIG_FONT_DEPTH] = std::to_string(ep.depth); + if (ep.use_surface) + data[APP_CONFIG_FONT_USE_SURFACE] = "true"; + if (fp.boldness.has_value()) + data[APP_CONFIG_FONT_BOLDNESS] = std::to_string(*fp.boldness); + if (fp.skew.has_value()) + data[APP_CONFIG_FONT_SKEW] = std::to_string(*fp.skew); + if (s.distance.has_value()) + data[APP_CONFIG_FONT_DISTANCE] = std::to_string(*s.distance); + if (s.angle.has_value()) + data[APP_CONFIG_FONT_ANGLE] = std::to_string(*s.angle); + if (fp.collection_number.has_value()) + data[APP_CONFIG_FONT_COLLECTION] = std::to_string(*fp.collection_number); + if (fp.char_gap.has_value()) + data[APP_CONFIG_FONT_CHAR_GAP] = std::to_string(*fp.char_gap); + if (fp.line_gap.has_value()) + data[APP_CONFIG_FONT_LINE_GAP] = std::to_string(*fp.line_gap); + cfg.set_section(create_section_name(index), std::move(data)); +} + +void store_style_index(AppConfig &cfg, size_t index) +{ + // store actual font index + // active font first index is +1 to correspond with section name + Section data; + data[APP_CONFIG_ACTIVE_FONT] = std::to_string(index); + cfg.set_section(AppConfig::SECTION_EMBOSS_STYLE, std::move(data)); +} + +std::optional load_style_index(const AppConfig &cfg) +{ + if (!cfg.has_section(AppConfig::SECTION_EMBOSS_STYLE)) + return {}; + + auto section = cfg.get_section(AppConfig::SECTION_EMBOSS_STYLE); + auto it = section.find(APP_CONFIG_ACTIVE_FONT); + if (it == section.end()) + return {}; + + size_t active_font = static_cast(std::atoi(it->second.c_str())); + // order in config starts with number 1 + return active_font - 1; +} + +::StyleManager::Styles load_styles(const AppConfig &cfg) +{ + StyleManager::Styles result; + // human readable index inside of config starts from 1 !! + unsigned index = 1; + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + std::optional style_opt = load_style(cfg.get_section(section_name)); + if (style_opt.has_value()) { + make_unique_name(result, style_opt->name); + result.emplace_back(*style_opt); + } + + section_name = create_section_name(++index); + } + return result; +} + +void store_styles(AppConfig &cfg, const StyleManager::Styles &styles) +{ + EmbossStyle::Type current_type = WxFontUtils::get_current_type(); + // store styles + unsigned index = 1; + for (const StyleManager::Style &style : styles) { + // skip file paths + fonts from other OS(loaded from .3mf) + assert(style.type == current_type); + if (style.type != current_type) + continue; + store_style(cfg, style, index); + ++index; + } + + // remove rest of font sections (after deletation) + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + cfg.clear_section(section_name); + section_name = create_section_name(index); + ++index; + } +} + +void make_unique_name(const StyleManager::Styles& styles, std::string &name) +{ + auto is_unique = [&styles](const std::string &name){ + for (const StyleManager::Style &it : styles) + if (it.name == name) return false; + return true; + }; + + // Style name can't be empty so default name is set + if (name.empty()) name = "Text style"; + + // When name is already unique, nothing need to be changed + if (is_unique(name)) return; + + // when there is previous version of style name only find number + const char *prefix = " ("; + const char suffix = ')'; + auto pos = name.find_last_of(prefix); + if (name.c_str()[name.size() - 1] == suffix && + pos != std::string::npos) { + // short name by ord number + name = name.substr(0, pos); + } + + int order = 1; // start with value 2 to represents same font name + std::string new_name; + do { + new_name = name + prefix + std::to_string(++order) + suffix; + } while (!is_unique(new_name)); + name = new_name; +} + +} // namespace diff --git a/src/slic3r/Utils/EmbossStyleManager.hpp b/src/slic3r/Utils/EmbossStyleManager.hpp index f52ac95d6d..d36f062f48 100644 --- a/src/slic3r/Utils/EmbossStyleManager.hpp +++ b/src/slic3r/Utils/EmbossStyleManager.hpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include namespace Slic3r::GUI::Emboss { @@ -26,11 +28,10 @@ namespace Slic3r::GUI::Emboss { class StyleManager { friend class CreateFontStyleImagesJob; // access to StyleImagesData - public: /// Character to load for imgui when initialize imgui font /// Function to create default styles - StyleManager(const ImWchar *language_glyph_range, std::function create_default_styles); + StyleManager(const ImWchar *language_glyph_range, const std::function& create_default_styles); /// /// Release imgui font and style images from GPU @@ -61,11 +62,11 @@ public: void add_style(const std::string& name); /// - /// Change order of style item in m_style_items. + /// Change order of style item in m_styles. /// Fix selected font index when (i1 || i2) == m_font_selected /// - /// First index to m_style_items - /// Second index to m_style_items + /// First index to m_styles + /// Second index to m_styles void swap(size_t i1, size_t i2); /// @@ -75,7 +76,7 @@ public: void discard_style_changes(); /// - /// Remove style from m_style_items. + /// Remove style from m_styles. /// Fix selected font index when index is under m_font_selected /// /// Index of style to be removed @@ -96,13 +97,14 @@ public: /// Change active font /// When font not loaded roll back activ font /// - /// New font index(from m_style_items range) + /// New font index(from m_styles range) /// True on succes. False on fail load font bool load_style(size_t font_index); // load font style not stored in list - bool load_style(const EmbossStyle &style); + struct Style; + bool load_style(const Style &style); // fastering load font on index by wxFont, ignore type and descriptor - bool load_style(const EmbossStyle &style, const wxFont &font); + bool load_style(const Style &style, const wxFont &font); // clear actual selected glyphs cache void clear_glyphs_cache(); @@ -110,15 +112,11 @@ 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 EmbossStyle *get_stored_style() const; + const Style *get_stored_style() const; - const EmbossStyle &get_style() const { return m_style_cache.style; } - EmbossStyle &get_style() { return m_style_cache.style; } + const Style &get_style() const { return m_style_cache.style; } + Style &get_style() { return m_style_cache.style; } size_t get_style_index() const { return m_style_cache.style_index; } std::string &get_truncated_name() { return m_style_cache.truncated_name; } const ImFontAtlas &get_atlas() const { return m_style_cache.atlas; } @@ -139,6 +137,8 @@ public: /// bool is_font_changed() const; + bool is_unique_style_name(const std::string &name) const; + /// /// Setter on wx_font when changed /// @@ -173,30 +173,49 @@ public: void init_style_images(const Vec2i& max_size, const std::string &text); void free_style_images(); - struct Item; // access to all managed font styles - const std::vector &get_styles() const; + const std::vector