Add dashed line loading from svg file

Note: Inaccurate offset of dashes on curves. (length is calculated after flattening -> on line segments)
This commit is contained in:
Filip Sykala - NTB T15p 2023-09-04 12:24:55 +02:00
parent d9b0fad80c
commit 0ac8afa75c
3 changed files with 178 additions and 27 deletions

View File

@ -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<float, max_dash_array_size> 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<float>(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<float>(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<float>().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 &param)
{
// convert stroke to polygon
@ -394,11 +500,11 @@ 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<float>(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<NSVGlineCap>(shape.strokeLineCap)) {
@ -406,7 +512,21 @@ ExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &sh
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);
}

View File

@ -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) {
/// <param name="image">Input svg loaded to shapes</param>
/// <returns>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) </returns>
std::vector<std::string> create_shape_warnings(const NSVGimage &image){
std::vector<std::string> create_shape_warnings(const NSVGimage &image, float scale){
std::vector<std::string> result;
auto add_warning = [&result, &image](size_t index, const std::string &message) {
if (result.empty())
result = std::vector<std::string>(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<std::string>(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<std::string>(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();
}

View File

@ -179,6 +179,7 @@ private:
std::optional<float> m_scale_height;
std::optional<float> m_scale_depth;
void calculate_scale();
float get_scale_for_tolerance();
// keep SVG data rendered on GPU
Texture m_texture;