From 0ac8afa75c726413c2cf8a674865d8c00c17009d Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Mon, 4 Sep 2023 12:24:55 +0200 Subject: [PATCH] Add dashed line loading from svg file Note: Inaccurate offset of dashes on curves. (length is calculated after flattening -> on line segments) --- src/libslic3r/NSVGUtils.cpp | 128 ++++++++++++++++++++++++++- src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp | 76 +++++++++++----- src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp | 1 + 3 files changed, 178 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp index ffa0f258e6..3235411de5 100644 --- a/src/libslic3r/NSVGUtils.cpp +++ b/src/libslic3r/NSVGUtils.cpp @@ -382,6 +382,112 @@ ExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shap return union_ex(fill, fill_type); } +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 (is_line) + dash.append(prev_point); + dash_length -= line_segment_length; + prev_point = point; // copy + } + + // 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 stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) { // convert stroke to polygon @@ -394,19 +500,33 @@ ExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &sh double mitter = shape.miterLimit * param.scale; if (join_type == ClipperLib::JoinType::jtRound) { - // mitter is ArcTolerance + // 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); - Polygons result = contour_to_polygons(lines_path.polygons, stroke_width, join_type, mitter); 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_append(result, offset(lines_path.polylines, stroke_width / 2, join_type, mitter, end_type)); + } + + 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)); + } + return union_ex(result, ClipperLib::pftNonZero); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp index 675e567ca9..7ef1ccc98b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -920,24 +920,28 @@ void add_comma_separated(std::string &result, const std::string &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 <= warning_preccission) + if ((shape.opacity - 1.f) <= warning_preccission) 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("Fill gradient (%1%)"), shape.fillGradient)); + 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 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 fill gradient")); + add_comma_separated(warning, _u8L("Radial gradient")); break; // case NSVG_PAINT_NONE: // case NSVG_PAINT_COLOR: @@ -951,25 +955,37 @@ std::string create_fill_warning(const NSVGshape &shape) { } 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) + 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("Stroke gradient (%1%)"), shape.strokeGradient)); + 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 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 stroke gradient")); + add_comma_separated(warning, _u8L("Radial gradient")); break; // case NSVG_PAINT_COLOR: // case NSVG_PAINT_NONE: // default: break; } + + if (shape.opacity) + return warning; } @@ -979,23 +995,33 @@ std::string create_stroke_warning(const NSVGshape &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 NSVGimage &image){ +std::vector create_shape_warnings(const NSVGimage &image, float scale){ 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); + result[index] = message; + }; + size_t shape_index = 0; for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next, ++shape_index) { - std::string fill_warning = create_fill_warning(*shape); - if (!fill_warning.empty()) { - if (result.empty()) - result = std::vector(get_shapes_count(image) * 2); - result[shape_index * 2] = GUI::format(_L("Fill of shape(%1%) contain unsupported: %2% "), shape->id, fill_warning); + if (!(shape->flags & NSVG_FLAGS_VISIBLE)){ + add_warning(shape_index * 2, GUI::format(_L("Shape(%1%) is marked as invisible "), shape->id)); + continue; } - std::string stroke_warning = create_stroke_warning(*shape); - if (!stroke_warning.empty()) { - if (result.empty()) - result = std::vector(get_shapes_count(image) * 2); - result[shape_index * 2 + 1] = GUI::format(_L("Stroke of shape(%1%) contain unsupported: %2% "), shape->id, stroke_warning); + std::string fill_warning = create_fill_warning(*shape); + if (!fill_warning.empty()) + add_warning(shape_index * 2, GUI::format(_L("Fill of shape(%1%) contain 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%) contain unsupported: %2% "), shape->id, stroke_warning)); } return result; } @@ -1044,7 +1070,7 @@ void GLGizmoSVG::set_volume_by_selection() NSVGimage* image = init_image(es.svg_file); assert(image != nullptr); if (image != nullptr){ - NSVGLineParams params{get_tesselation_tolerance(std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)))}; + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; shape_ids = create_shape_with_ids(*image, params); } } @@ -1055,7 +1081,7 @@ void GLGizmoSVG::set_volume_by_selection() m_volume_id = volume->id(); m_volume_shape = *volume->emboss_shape; // copy assert(es.svg_file.image.get() != nullptr); - m_shape_warnings = create_shape_warnings(*es.svg_file.image); + m_shape_warnings = create_shape_warnings(*es.svg_file.image, get_scale_for_tolerance()); // Calculate current angle of up vector m_angle = calc_up(gl_volume->world_matrix(), Slic3r::GUI::up_limit); @@ -1107,6 +1133,9 @@ void GLGizmoSVG::calculate_scale() { 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 @@ -1430,11 +1459,12 @@ void GLGizmoSVG::draw_filename(){ } if (file_changed) { - double tes_tol = get_tesselation_tolerance(std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f))); + 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.image = std::move(es_.svg_file.image); m_volume_shape.shapes_with_ids = std::move(es_.shapes_with_ids); - m_shape_warnings = create_shape_warnings(*m_volume_shape.svg_file.image); + m_shape_warnings = create_shape_warnings(*m_volume_shape.svg_file.image, scale); init_texture(m_texture, m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px); process(); } @@ -1584,7 +1614,7 @@ void GLGizmoSVG::draw_size() NSVGimage *img = m_volume_shape.svg_file.image.get(); assert(img != NULL); if (img != NULL){ - NSVGLineParams params{get_tesselation_tolerance(std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)))}; + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; m_volume_shape.shapes_with_ids = create_shape_with_ids(*img, params); process(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp index 88f2d079be..426f4ee703 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp @@ -179,6 +179,7 @@ private: 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;