Propagate warnings about unhealed shape to UI.

This commit is contained in:
Filip Sykala - NTB T15p 2023-10-16 11:44:08 +02:00
parent 4724d6791a
commit 82182ac8b8
8 changed files with 177 additions and 52 deletions

View File

@ -30,6 +30,7 @@ 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;
@ -432,7 +433,7 @@ bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double dist
return true;
}
std::pair<ExPolygons, bool> Emboss::heal_polygons(const Polygons &shape, bool is_non_zero, unsigned int max_iteration)
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 ?
@ -621,7 +622,7 @@ bool heal_dupl_inter(ExPolygons &shape, unsigned max_iteration)
if (fill_trouble_holes(holes, duplicate_points, intersection_points, shape)) {
holes.clear();
continue;
}
}
holes.clear();
holes.reserve(intersections.size() + duplicate_points.size());
@ -785,7 +786,7 @@ std::optional<Glyph> get_glyph(const stbtt_fontinfo &font_info, int unicode_lett
// 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).first;
glyph.shape = Emboss::heal_polygons(glyph_polygons, is_non_zero, max_iteration);
}
return glyph;
}
@ -1279,7 +1280,7 @@ const int CANCEL_CHECK = 10;
} // namespace
/// Union shape defined by glyphs
ExPolygons Slic3r::union_ex(const ExPolygonsWithIds &shapes)
HealedExPolygons Slic3r::union_ex(const ExPolygonsWithIds &shapes, unsigned max_heal_iteration)
{
// unify to one expolygon
ExPolygons result;
@ -1289,11 +1290,12 @@ ExPolygons Slic3r::union_ex(const ExPolygonsWithIds &shapes)
expolygons_append(result, shape.expoly);
}
result = union_ex(result);
bool is_healed = heal_expolygons(result);
return result;
bool is_healed = heal_expolygons(result, max_heal_iteration);
return {result, is_healed};
}
ExPolygons Slic3r::union_with_delta(const ExPolygonsWithIds &shapes, float delta)
HealedExPolygons Slic3r::union_with_delta(const ExPolygonsWithIds &shapes, float delta, unsigned max_heal_iteration)
{
// unify to one expolygons
ExPolygons expolygons;
@ -1303,22 +1305,22 @@ ExPolygons Slic3r::union_with_delta(const ExPolygonsWithIds &shapes, float delta
expolygons_append(expolygons, offset_ex(shape.expoly, delta));
}
ExPolygons result = union_ex(expolygons);
result = offset_ex(result, -delta);
bool is_healed = heal_expolygons(result);
return result;
result = offset_ex(result, -delta);
bool is_healed = heal_expolygons(result, max_heal_iteration);
return {result, is_healed};
}
void Slic3r::translate(ExPolygonsWithIds &e, const Point &p)
void Slic3r::translate(ExPolygonsWithIds &expolygons_with_ids, const Point &p)
{
for (auto &[id, expoly] : e)
translate(expoly, p);
for (ExPolygonsWithId &expolygons_with_id : expolygons_with_ids)
translate(expolygons_with_id.expoly, p);
}
BoundingBox Slic3r::get_extents(const ExPolygonsWithIds &e)
BoundingBox Slic3r::get_extents(const ExPolygonsWithIds &expolygons_with_ids)
{
BoundingBox bb;
for (auto &[id, expoly] : e)
bb.merge(get_extents(expoly));
for (const ExPolygonsWithId &expolygons_with_id : expolygons_with_ids)
bb.merge(get_extents(expolygons_with_id.expoly));
return bb;
}
@ -1328,11 +1330,13 @@ void Slic3r::center(ExPolygonsWithIds &e)
translate(e, -bb.center());
}
ExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, const char *text, const FontProp &font_prop, const std::function<bool()>& was_canceled)
HealedExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, const char *text, const FontProp &font_prop, const std::function<bool()>& was_canceled)
{
std::wstring text_w = boost::nowide::widen(text);
ExPolygonsWithIds vshapes = text2vshapes(font_with_cache, text_w, font_prop, was_canceled);
return union_ex(vshapes);
float delta = static_cast<float>(1. / SHAPE_SCALE);
return union_with_delta(vshapes, delta, MAX_HEAL_ITERATION_OF_TEXT);
}
namespace {

View File

@ -18,6 +18,13 @@
namespace Slic3r {
// Extend expolygons with information whether it was successfull healed
struct HealedExPolygons{
ExPolygons expolygons;
bool is_healed;
operator ExPolygons&() { return expolygons; }
};
/// <summary>
/// class with only static function add ability to engraved OR raised
/// text OR polygons onto model surface
@ -153,7 +160,7 @@ namespace Emboss
/// <param name="font_prop">User defined property of the font</param>
/// <param name="was_canceled">Way to interupt processing</param>
/// <returns>Inner polygon cw(outer ccw)</returns>
ExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function<bool()> &was_canceled = []() {return false;});
HealedExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function<bool()> &was_canceled = []() {return false;});
ExPolygonsWithIds text2vshapes(FontFileWithCache &font, const std::wstring& text, const FontProp &font_prop, const std::function<bool()>& was_canceled = []() {return false;});
const unsigned ENTER_UNICODE = static_cast<unsigned>('\n');
@ -169,7 +176,7 @@ namespace Emboss
/// <param name="is_non_zero">Fill type ClipperLib::pftNonZero for overlapping otherwise </param>
/// <param name="max_iteration">Look at heal_expolygon()::max_iteration</param>
/// <returns>Healed shapes with flag is fully healed</returns>
std::pair<ExPolygons, bool> heal_polygons(const Polygons &shape, bool is_non_zero = true, unsigned max_iteration = 10);
HealedExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true, unsigned max_iteration = 10);
/// <summary>
/// NOTE: call Slic3r::union_ex before this call
@ -467,9 +474,9 @@ namespace Emboss
void translate(ExPolygonsWithIds &e, const Point &p);
BoundingBox get_extents(const ExPolygonsWithIds &e);
void center(ExPolygonsWithIds &e);
ExPolygons union_ex(const ExPolygonsWithIds &shapes);
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)
ExPolygons union_with_delta(const ExPolygonsWithIds &shapes, float delta);
HealedExPolygons union_with_delta(const ExPolygonsWithIds &shapes, float delta, unsigned max_heal_iteration);
} // namespace Slic3r
#endif // slic3r_Emboss_hpp_

View File

@ -61,6 +61,9 @@ struct ExPolygonsWithId
// 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<ExPolygonsWithId>;
@ -103,7 +106,11 @@ struct EmbossShape
std::shared_ptr<std::string> 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<class Archive> void save(Archive &ar) const
{

View File

@ -181,6 +181,7 @@ 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";
@ -3855,12 +3856,15 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
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 << "=\"";
@ -3872,6 +3876,8 @@ void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume
std::optional<EmbossShape> 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);
@ -3893,7 +3899,7 @@ std::optional<EmbossShape> read_emboss_shape(const char **attributes, unsigned i
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)};
return EmbossShape{shapes, scale, std::move(projection), std::move(fix_tr_mat), std::move(svg), is_healed};
}

View File

@ -18,8 +18,8 @@ struct LinesPath{
Polygons polygons;
Polylines polylines; };
LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams &param);
ExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param);
ExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param);
HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param);
HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param);
} // namespace
namespace Slic3r {
@ -45,11 +45,13 @@ ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLinePa
if (is_fill_used) {
unsigned unique_id = static_cast<unsigned>(2 * shape_id);
result.push_back({unique_id, fill_to_expolygons(lines_path, shape, param)});
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<unsigned>(2 * shape_id + 1);
result.push_back({unique_id, stroke_to_expolygons(lines_path, shape, param)});
HealedExPolygons expoly = stroke_to_expolygons(lines_path, shape, param);
result.push_back({unique_id, expoly.expolygons, expoly.is_healed});
}
}
@ -352,7 +354,7 @@ LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams &param)
return result;
}
ExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param)
HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param)
{
Polygons fill = lines_path.polygons; // copy
@ -366,7 +368,7 @@ ExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shap
if (shape.fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD)
is_non_zero = false;
return Emboss::heal_polygons(fill, is_non_zero, param.max_heal_iteration).first;
return Emboss::heal_polygons(fill, is_non_zero, param.max_heal_iteration);
}
struct DashesParam{
@ -475,7 +477,7 @@ Polylines to_dashes(const Polyline &polyline, const DashesParam& param)
return dashes;
}
ExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param)
HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams &param)
{
// convert stroke to polygon
ClipperLib::JoinType join_type = ClipperLib::JoinType::jtSquare;
@ -515,7 +517,7 @@ ExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &sh
}
bool is_non_zero = true;
return Emboss::heal_polygons(result, is_non_zero, param.max_heal_iteration).first;
return Emboss::heal_polygons(result, is_non_zero, param.max_heal_iteration);
}
} // namespace

View File

@ -777,8 +777,57 @@ void wu_draw_line(Linef line,
}
}
template<unsigned int N> // N .. count of channels per pixel
void draw_side_outline(const ExPolygons &shape, const std::array<unsigned char, N> &color, std::vector<unsigned char> &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<unsigned char>(std::round(brightess * 255));
} else if (alpha != 255){
alpha = static_cast<unsigned char>(std::min(255, int(alpha) + static_cast<int>(std::round(brightess * 255))));
}
};
BoundingBox bb_unscaled = get_extents(shape);
Linesf lines = to_linesf(shape);
BoundingBoxf bb(bb_unscaled.min.cast<double>(), bb_unscaled.max.cast<double>());
// 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);
}
/// <summary>
/// Draw filled ExPolygon into data
/// Draw filled ExPolygon into data
/// line by line inspired by: http://alienryderflex.com/polygon_fill/
/// </summary>
/// <typeparam name="N">Count channels for one pixel(RGBA = 4)</typeparam>
@ -809,7 +858,6 @@ void draw_filled(const ExPolygons &shape, const std::array<unsigned char, N>& co
bb.min *= scale;
bb.max *= scale;
}
auto tree = Slic3r::AABBTreeLines::build_aabb_tree_over_indexed_lines(lines);
int count_lines = data.size() / (N * data_width);
size_t data_line = N * data_width;
@ -839,8 +887,11 @@ void draw_filled(const ExPolygons &shape, const std::array<unsigned char, N>& co
alpha = static_cast<unsigned char>(std::min(255, int(alpha) + static_cast<int>(std::round(brightess * 255))));
}
};
for (const Linef& line: lines) wu_draw_line_side(line, draw);
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;
@ -883,7 +934,7 @@ void draw_filled(const ExPolygons &shape, const std::array<unsigned char, N>& co
}
// init texture by draw expolygons into texture
bool init_texture(Texture &texture, const ExPolygonsWithIds& shapes_with_ids, unsigned max_size_px){
bool init_texture(Texture &texture, const ExPolygonsWithIds& shapes_with_ids, unsigned max_size_px, const std::vector<std::string>& shape_warnings){
BoundingBox bb = get_extents(shapes_with_ids);
Point bb_size = bb.size();
double bb_width = bb_size.x(); // [in mm]
@ -914,12 +965,35 @@ bool init_texture(Texture &texture, const ExPolygonsWithIds& shapes_with_ids, un
shape = union_ex(shape);
// align to texture
for (ExPolygon& expolygon: shape)
expolygon.translate(-bb.min);
translate(shape, -bb.min);
size_t texture_width = static_cast<size_t>(texture.width);
unsigned char alpha = 255; // without transparency
std::array<unsigned char, 4> color{201, 201, 201, alpha};
draw_filled<4>(shape, color, data, (size_t)texture.width, scale);
std::array<unsigned char, 4> color_shape{201, 201, 201, alpha}; // from degin by @JosefZachar
std::array<unsigned char, 4> color_error{237, 28, 36, alpha}; // from icon: resources/icons/flag_red.svg
std::array<unsigned char, 4> 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));
@ -1027,14 +1101,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, float scale){
std::vector<std::string> 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<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;
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)){
@ -1114,8 +1207,8 @@ void GLGizmoSVG::set_volume_by_selection()
m_volume = volume;
m_volume_id = volume->id();
m_volume_shape = *volume->emboss_shape; // copy
m_shape_warnings = create_shape_warnings(image, get_scale_for_tolerance());
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);
@ -1303,7 +1396,7 @@ void GLGizmoSVG::draw_preview(){
// 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);
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);
@ -1535,8 +1628,8 @@ void GLGizmoSVG::draw_filename(){
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.svg_file.image, scale);
init_texture(m_texture, m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px);
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();
}
}

View File

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

View File

@ -791,9 +791,15 @@ bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread)
template<typename Fnc>
ExPolygons create_shape(DataBase &input, Fnc was_canceled) {
const EmbossShape &es = input.create_shape();
EmbossShape &es = input.create_shape();
float delta = 50.f;
return union_with_delta(es.shapes_with_ids, delta);
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