diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp index c4b8f030f..a8f920ae2 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -135,7 +135,7 @@ ArchiveData extract_sla_archive(const std::string &zipfname, ExPolygons rings_to_expolygons(const std::vector &rings, double px_w, double px_h) { - ExPolygons polys; polys.reserve(rings.size()); + auto polys = reserve_vector(rings.size()); for (const marchsq::Ring &ring : rings) { Polygon poly; Points &pts = poly.points; @@ -147,7 +147,7 @@ ExPolygons rings_to_expolygons(const std::vector &rings, polys.emplace_back(poly); } - // reverse the raster transformations + // TODO: Is a union necessary? return union_ex(polys); } @@ -270,11 +270,11 @@ std::vector extract_slices_from_sla_archive( png::ReadBuf rb{arch.images[i].buf.data(), arch.images[i].buf.size()}; if (!png::decode_png(rb, img)) return; - auto rings = marchsq::execute(img, 128, rstp.win); + uint8_t isoval = 128; + auto rings = marchsq::execute(img, isoval, rstp.win); ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h); - // Invert the raster transformations indicated in - // the profile metadata + // Invert the raster transformations indicated in the profile metadata invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); slices[i] = std::move(expolys); @@ -310,7 +310,24 @@ ConfigSubstitutions import_sla_archive( std::string exclude_entries{"thumbnail"}; ArchiveData arch = extract_sla_archive(zipfname, exclude_entries); DynamicPrintConfig profile_in, profile_use; - ConfigSubstitutions config_substitutions = profile_in.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); + ConfigSubstitutions config_substitutions = + profile_in.load(arch.profile, + ForwardCompatibilitySubstitutionRule::Enable); + + if (profile_in.empty()) { // missing profile... do guess work + // try to recover the layer height from the config.ini which was + // present in all versions of sl1 files. + if (auto lh_opt = arch.config.find("layerHeight"); + lh_opt != arch.config.not_found()) + { + auto lh_str = lh_opt->second.data(); + try { + double lh = std::stod(lh_str); // TODO replace with std::from_chars + profile_out.set("layer_height", lh); + profile_out.set("initial_layer_height", lh); + } catch(...) {} + } + } // If the archive contains an empty profile, use the one that was passed as output argument // then replace it with the readed profile to report that it was empty. diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 8531ab07e..81627347b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1263,10 +1263,10 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr instances[instance]->get_mirror() ); - z -= instances[instance]->get_offset()(2); + z -= instances[instance]->get_offset().z(); - // Lower part per-instance bounding boxes - std::vector lower_bboxes { instances.size() }; + // Displacement (in instance coordinates) to be applied to place the upper parts + Vec3d local_displace = Vec3d::Zero(); for (ModelVolume *volume : volumes) { const auto volume_matrix = volume->get_matrix(); @@ -1286,8 +1286,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr if (attributes.has(ModelObjectCutAttribute::KeepLower)) lower->add_volume(*volume); } - else if (! volume->mesh().empty()) { - + else if (! volume->mesh().empty()) { // Transform the mesh by the combined transformation matrix. // Flip the triangles in case the composite transformation is left handed. TriangleMesh mesh(volume->mesh()); @@ -1327,13 +1326,10 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr assert(vol->config.id() != volume->config.id()); vol->set_material(volume->material_id(), *volume->material()); - // Compute the lower part instances' bounding boxes to figure out where to place - // the upper part - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { - for (size_t i = 0; i < instances.size(); i++) { - lower_bboxes[i].merge(instances[i]->transform_mesh_bounding_box(lower_mesh, true)); - } - } + // Compute the displacement (in instance coordinates) to be applied to place the upper parts + // The upper part displacement is set to half of the lower part bounding box + // this is done in hope at least a part of the upper part will always be visible and draggable + local_displace = lower->full_raw_mesh_bounding_box().size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); } } } @@ -1341,17 +1337,18 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr ModelObjectPtrs res; if (attributes.has(ModelObjectCutAttribute::KeepUpper) && upper->volumes.size() > 0) { - upper->invalidate_bounding_box(); - upper->center_around_origin(); + if (!upper->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { + upper->center_around_origin(); + upper->translate_instances(-upper->origin_translation); + upper->origin_translation = Vec3d::Zero(); + } // Reset instance transformation except offset and Z-rotation - for (size_t i = 0; i < instances.size(); i++) { + for (size_t i = 0; i < instances.size(); ++i) { auto &instance = upper->instances[i]; const Vec3d offset = instance->get_offset(); - const double rot_z = instance->get_rotation()(2); - // The upper part displacement is set to half of the lower part bounding box - // this is done in hope at least a part of the upper part will always be visible and draggable - const Vec3d displace = lower_bboxes[i].size().cwiseProduct(Vec3d(-0.5, -0.5, 0.0)); + const double rot_z = instance->get_rotation().z(); + const Vec3d displace = Geometry::assemble_transform(Vec3d::Zero(), instance->get_rotation()) * local_displace; instance->set_transformation(Geometry::Transformation()); instance->set_offset(offset + displace); @@ -1361,14 +1358,16 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, ModelObjectCutAttr res.push_back(upper); } if (attributes.has(ModelObjectCutAttribute::KeepLower) && lower->volumes.size() > 0) { - lower->invalidate_bounding_box(); - lower->center_around_origin(); + if (!lower->origin_translation.isApprox(Vec3d::Zero()) && instances[instance]->get_offset().isApprox(Vec3d::Zero())) { + lower->center_around_origin(); + lower->translate_instances(-lower->origin_translation); + lower->origin_translation = Vec3d::Zero(); + } // Reset instance transformation except offset and Z-rotation for (auto *instance : lower->instances) { const Vec3d offset = instance->get_offset(); - const double rot_z = instance->get_rotation()(2); - + const double rot_z = instance->get_rotation().z(); instance->set_transformation(Geometry::Transformation()); instance->set_offset(offset); instance->set_rotation(Vec3d(attributes.has(ModelObjectCutAttribute::FlipLower) ? Geometry::deg2rad(180.0) : 0.0, 0.0, rot_z)); diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 11e263299..7041f7b82 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1113,7 +1113,7 @@ static inline Polygon to_polygon(const std::vector &lines) // It iterates through all nodes on the border between two different colors, and from this point, // start selection always left most edges for every node to construct CCW polygons. // Assumes that graph is planar (without self-intersection edges) -static std::vector> extract_colored_segments(const MMU_Graph &graph) +static std::vector extract_colored_segments(const MMU_Graph &graph, const size_t num_extruders) { std::vector used_arcs(graph.arcs.size(), false); // When there is no next arc, then is returned original_arc or edge with is marked as used @@ -1153,7 +1153,7 @@ static std::vector> extract_colored_segments(const MM return std::all_of(node.arc_idxs.cbegin(), node.arc_idxs.cend(), [&used_arcs](const size_t &arc_idx) -> bool { return used_arcs[arc_idx]; }); }; - std::vector> polygons_segments; + std::vector expolygons_segments(num_extruders + 1); for (size_t node_idx = 0; node_idx < graph.all_border_points; ++node_idx) { const MMU_Graph::Node &node = graph.nodes[node_idx]; @@ -1183,12 +1183,11 @@ static std::vector> extract_colored_segments(const MM p_arc = &next; } while (graph.nodes[p_arc->to_idx].point != start_p || !all_arc_used(graph.nodes[p_arc->to_idx])); - Polygon poly = to_polygon(face_lines); - if (poly.is_counter_clockwise() && poly.is_valid()) - polygons_segments.emplace_back(poly, arc.color); + if (Polygon poly = to_polygon(face_lines); poly.is_counter_clockwise() && poly.is_valid()) + expolygons_segments[arc.color].emplace_back(std::move(poly)); } } - return polygons_segments; + return expolygons_segments; } // Used in remove_multiple_edges_in_vertices() @@ -1269,21 +1268,20 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto } } -static void cut_segmented_layers(const std::vector &input_expolygons, - std::vector>> &segmented_regions, - const float cut_width, - const std::function &throw_on_cancel_callback) +static void cut_segmented_layers(const std::vector &input_expolygons, + std::vector> &segmented_regions, + const float cut_width, + const std::function &throw_on_cancel_callback) { BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - cutting segmented layers in parallel - begin"; tbb::parallel_for(tbb::blocked_range(0, segmented_regions.size()),[&segmented_regions, &input_expolygons, &cut_width, &throw_on_cancel_callback](const tbb::blocked_range& range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { throw_on_cancel_callback(); - std::vector> segmented_regions_cuts; - for (const std::pair &colored_expoly : segmented_regions[layer_idx]) { - ExPolygons cut_colored_expoly = diff_ex(colored_expoly.first, offset_ex(input_expolygons[layer_idx], cut_width)); - for (ExPolygon &expoly : cut_colored_expoly) - segmented_regions_cuts.emplace_back(std::move(expoly), colored_expoly.second); - } + const size_t num_extruders_plus_one = segmented_regions[layer_idx].size(); + std::vector segmented_regions_cuts(num_extruders_plus_one); // Indexed by extruder_id + for (size_t extruder_idx = 0; extruder_idx < num_extruders_plus_one; ++extruder_idx) + if (const ExPolygons &ex_polygons = segmented_regions[layer_idx][extruder_idx]; !ex_polygons.empty()) + segmented_regions_cuts[extruder_idx] = diff_ex(ex_polygons, offset_ex(input_expolygons[layer_idx], cut_width)); segmented_regions[layer_idx] = std::move(segmented_regions_cuts); } }); // end of parallel_for @@ -1323,7 +1321,7 @@ static inline std::vector> mmu_segmentation_top_and_bott // Project upwards pointing painted triangles over top surfaces, // project downards pointing painted triangles over bottom surfaces. std::vector> top_raw(num_extruders), bottom_raw(num_extruders); - std::vector zs = zs_from_layers(print_object.layers()); + std::vector zs = zs_from_layers(layers); Transform3d object_trafo = print_object.trafo_centered(); #ifdef MMU_SEGMENTATION_DEBUG_TOP_BOTTOM @@ -1532,31 +1530,42 @@ static inline std::vector> mmu_segmentation_top_and_bott return triangles_by_color_merged; } -static std::vector>> merge_segmented_layers( - const std::vector>> &segmented_regions, - std::vector> &&top_and_bottom_layers, - const std::function &throw_on_cancel_callback) +static std::vector> merge_segmented_layers( + const std::vector> &segmented_regions, + std::vector> &&top_and_bottom_layers, + const size_t num_extruders, + const std::function &throw_on_cancel_callback) { - std::vector>> segmented_regions_merged(segmented_regions.size()); + const size_t num_layers = segmented_regions.size(); + std::vector> segmented_regions_merged(num_layers); + segmented_regions_merged.assign(num_layers, std::vector(num_extruders)); + assert(num_extruders + 1 == top_and_bottom_layers.size()); BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - merging segmented layers in parallel - begin"; - tbb::parallel_for(tbb::blocked_range(0, segmented_regions.size()), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_extruders, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { - for (const std::pair &colored_expoly : segmented_regions[layer_idx]) { + assert(segmented_regions[layer_idx].size() == num_extruders + 1); + // Zero is skipped because it is the default color of the volume + for (size_t extruder_id = 1; extruder_id < num_extruders + 1; ++extruder_id) { throw_on_cancel_callback(); - // Zero is the default color of the volume. - if(colored_expoly.second == 0) - continue; - ExPolygons cut_colored_expoly = {colored_expoly.first}; - for (const std::vector &top_and_bottom_layer : top_and_bottom_layers) - cut_colored_expoly = diff_ex(cut_colored_expoly, top_and_bottom_layer[layer_idx]); - for (ExPolygon &ex_poly : cut_colored_expoly) - segmented_regions_merged[layer_idx].emplace_back(std::move(ex_poly), colored_expoly.second - 1); - } + if (!segmented_regions[layer_idx][extruder_id].empty()) { + ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id]; + for (const std::vector &top_and_bottom_by_extruder : top_and_bottom_layers) + if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) + segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]); - for (size_t color_idx = 1; color_idx < top_and_bottom_layers.size(); ++color_idx) - for (ExPolygon &expoly : top_and_bottom_layers[color_idx][layer_idx]) - segmented_regions_merged[layer_idx].emplace_back(std::move(expoly), color_idx - 1); + segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed); + } + + if (!top_and_bottom_layers[extruder_id][layer_idx].empty()) { + bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty(); + append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]); + + // Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers. + if (!was_top_and_bottom_empty) + segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON)); + } + } } }); // end of parallel_for BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - merging segmented layers in parallel - end"; @@ -1565,7 +1574,7 @@ static std::vector>> merge_segmented_la } #ifdef MMU_SEGMENTATION_DEBUG_REGIONS -static void export_regions_to_svg(const std::string &path, const std::vector> ®ions, const ExPolygons &lslices) +static void export_regions_to_svg(const std::string &path, const std::vector ®ions, const ExPolygons &lslices) { const std::vector colors = {"blue", "cyan", "red", "orange", "magenta", "pink", "purple", "yellow"}; coordf_t stroke_width = scale_(0.05); @@ -1574,12 +1583,12 @@ static void export_regions_to_svg(const std::string &path, const std::vector ®ion : regions) { - int region_color = int(region.second); - if (region_color >= 0 && region_color < int(colors.size())) - svg.draw(region.first, colors[region_color]); + for (const ExPolygons &by_extruder : regions) { + size_t extrude_idx = &by_extruder - ®ions.front(); + if (extrude_idx >= 0 && extrude_idx < int(colors.size())) + svg.draw(by_extruder, colors[extrude_idx], stroke_width); else - svg.draw(region.first, "black"); + svg.draw(by_extruder, "black", stroke_width); } } #endif // MMU_SEGMENTATION_DEBUG_REGIONS @@ -1667,20 +1676,23 @@ static bool has_layer_only_one_color(const std::vector> return true; } -std::vector>> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function &throw_on_cancel_callback) +std::vector> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function &throw_on_cancel_callback) { - std::vector>> segmented_regions(print_object.layers().size()); - std::vector> painted_lines(print_object.layers().size()); - std::array painted_lines_mutex; - std::vector edge_grids(print_object.layers().size()); - const ConstLayerPtrsAdaptor layers = print_object.layers(); - std::vector input_expolygons(layers.size()); + const size_t num_extruders = print_object.print()->config().nozzle_diameter.size(); + const size_t num_layers = print_object.layers().size(); + std::vector> segmented_regions(num_layers); + segmented_regions.assign(num_layers, std::vector(num_extruders + 1)); + std::vector> painted_lines(num_layers); + std::array painted_lines_mutex; + std::vector edge_grids(num_layers); + const ConstLayerPtrsAdaptor layers = print_object.layers(); + std::vector input_expolygons(num_layers); throw_on_cancel_callback(); // Merge all regions and remove small holes BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - slices preparation in parallel - begin"; - tbb::parallel_for(tbb::blocked_range(0, layers.size()), [&layers, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(0, num_layers), [&layers, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { throw_on_cancel_callback(); ExPolygons ex_polygons; @@ -1711,7 +1723,7 @@ std::vector>> multi_material_segmentati }); // end of parallel_for BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - slices preparation in parallel - end"; - for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { + for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { throw_on_cancel_callback(); BoundingBox bbox(get_extents(layers[layer_idx]->regions())); bbox.merge(get_extents(input_expolygons[layer_idx])); @@ -1723,8 +1735,7 @@ std::vector>> multi_material_segmentati BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - projection of painted triangles - begin"; for (const ModelVolume *mv : print_object.model_object()->volumes) { - const size_t num_extruders = print_object.print()->config().nozzle_diameter.size() + 1; - tbb::parallel_for(tbb::blocked_range(1, num_extruders), [&mv, &print_object, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(1, num_extruders + 1), [&mv, &print_object, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) { throw_on_cancel_callback(); const indexed_triangle_set custom_facets = mv->mmu_segmentation_facets.get_facets(*mv, EnforcerBlockerType(extruder_idx)); @@ -1732,7 +1743,7 @@ std::vector>> multi_material_segmentati continue; const Transform3f tr = print_object.trafo().cast() * mv->get_matrix().cast(); - tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) { float min_z = std::numeric_limits::max(); float max_z = std::numeric_limits::lowest(); @@ -1748,15 +1759,15 @@ std::vector>> multi_material_segmentati std::sort(facet.begin(), facet.end(), [](const Vec3f &p1, const Vec3f &p2) { return p1.z() < p2.z(); }); // Find lowest slice not below the triangle. - auto first_layer = std::upper_bound(print_object.layers().begin(), print_object.layers().end(), float(min_z - EPSILON), + auto first_layer = std::upper_bound(layers.begin(), layers.end(), float(min_z - EPSILON), [](float z, const Layer *l1) { return z < l1->slice_z; }); - auto last_layer = std::upper_bound(print_object.layers().begin(), print_object.layers().end(), float(max_z + EPSILON), + auto last_layer = std::upper_bound(layers.begin(), layers.end(), float(max_z + EPSILON), [](float z, const Layer *l1) { return z < l1->slice_z; }); --last_layer; for (auto layer_it = first_layer; layer_it != (last_layer + 1); ++layer_it) { const Layer *layer = *layer_it; - size_t layer_idx = layer_it - print_object.layers().begin(); + size_t layer_idx = layer_it - layers.begin(); if (input_expolygons[layer_idx].empty() || facet[0].z() > layer->slice_z || layer->slice_z > facet[2].z()) continue; @@ -1799,7 +1810,7 @@ std::vector>> multi_material_segmentati << std::count_if(painted_lines.begin(), painted_lines.end(), [](const std::vector &pl) { return !pl.empty(); }); BOOST_LOG_TRIVIAL(debug) << "MMU segmentation - layers segmentation in parallel - begin"; - tbb::parallel_for(tbb::blocked_range(0, print_object.layers().size()), [&edge_grids, &input_expolygons, &painted_lines, &segmented_regions, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(0, num_layers), [&edge_grids, &input_expolygons, &painted_lines, &segmented_regions, &num_extruders, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { throw_on_cancel_callback(); if (!painted_lines[layer_idx].empty()) { @@ -1832,8 +1843,7 @@ std::vector>> multi_material_segmentati assert(!color_poly.front().empty()); if (has_layer_only_one_color(color_poly)) { // If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer. - for (const ExPolygon &ex_polygon : input_expolygons[layer_idx]) - segmented_regions[layer_idx].emplace_back(ex_polygon, size_t(color_poly.front().front().color)); + segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx]; } else { MMU_Graph graph = build_graph(layer_idx, color_poly); remove_multiple_edges_in_vertices(graph, color_poly); @@ -1846,9 +1856,7 @@ std::vector>> multi_material_segmentati } #endif // MMU_SEGMENTATION_DEBUG_GRAPH - std::vector> segmentation = extract_colored_segments(graph); - for (std::pair ®ion : segmentation) - segmented_regions[layer_idx].emplace_back(std::move(region)); + segmented_regions[layer_idx] = extract_colored_segments(graph, num_extruders); } #ifdef MMU_SEGMENTATION_DEBUG_REGIONS @@ -1868,11 +1876,11 @@ std::vector>> multi_material_segmentati throw_on_cancel_callback(); } -// return segmented_regions; - std::vector> top_and_bottom_layers = mmu_segmentation_top_and_bottom_layers(print_object, input_expolygons, throw_on_cancel_callback); + // The first index is extruder number (includes default extruder), and the second one is layer number + std::vector> top_and_bottom_layers = mmu_segmentation_top_and_bottom_layers(print_object, input_expolygons, throw_on_cancel_callback); throw_on_cancel_callback(); - std::vector>> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), throw_on_cancel_callback); + std::vector> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_extruders, throw_on_cancel_callback); throw_on_cancel_callback(); #ifdef MMU_SEGMENTATION_DEBUG_REGIONS diff --git a/src/libslic3r/MultiMaterialSegmentation.hpp b/src/libslic3r/MultiMaterialSegmentation.hpp index 07767111a..4efdc6951 100644 --- a/src/libslic3r/MultiMaterialSegmentation.hpp +++ b/src/libslic3r/MultiMaterialSegmentation.hpp @@ -11,7 +11,7 @@ class PrintObject; class ExPolygon; // Returns MMU segmentation based on painting in MMU segmentation gizmo -std::vector>> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function &throw_on_cancel_callback); +std::vector> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function &throw_on_cancel_callback); } // namespace Slic3r diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 09cd38468..0487ddaec 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -534,6 +534,7 @@ static std::vector s_Preset_sla_print_options { }; static std::vector s_Preset_sla_material_options { + "material_colour", "material_type", "initial_layer_height", "bottle_cost", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 232e724e7..e165c731f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3163,6 +3163,13 @@ void PrintConfigDef::init_sla_params() // SLA Material settings. + + def = this->add("material_colour", coStrings); + def->label = L("Color"); + def->tooltip = L("This is only used in the Slic3r interface as a visual help."); + def->gui_type = ConfigOptionDef::GUIType::color; + def->set_default_value(new ConfigOptionStrings{ "#29B2B2" }); + def = this->add("material_type", coString); def->label = L("SLA material type"); def->tooltip = L("SLA material type"); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index e2844a624..84b212938 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -538,7 +538,7 @@ template static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCancel throw_on_cancel) { // Returns MMU segmentation based on painting in MMU segmentation gizmo - std::vector>> segmentation = multi_material_segmentation_by_painting(print_object, throw_on_cancel); + std::vector> segmentation = multi_material_segmentation_by_painting(print_object, throw_on_cancel); assert(segmentation.size() == print_object.layer_count()); tbb::parallel_for( tbb::blocked_range(0, segmentation.size(), std::max(segmentation.size() / 128, size_t(1))), @@ -568,9 +568,7 @@ static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCance bool layer_split = false; for (size_t extruder_id = 0; extruder_id < num_extruders; ++ extruder_id) { ByExtruder ®ion = by_extruder[extruder_id]; - for (const std::pair &colored_polygon : segmentation[layer_id]) - if (colored_polygon.second == extruder_id) - region.expolygons.emplace_back(std::move(colored_polygon.first)); + append(region.expolygons, std::move(segmentation[layer_id][extruder_id])); if (! region.expolygons.empty()) { region.bbox = get_extents(region.expolygons); layer_split = true; @@ -632,6 +630,13 @@ static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCance if (mine.empty()) break; } + // Filter out unprintable polygons produced by subtraction multi-material painted regions from layerm.region(). + // ExPolygon returned from multi-material segmentation does not precisely match ExPolygons in layerm.region() + // (because of preprocessing of the input regions in multi-material segmentation). Therefore, subtraction from + // layerm.region() could produce a huge number of small unprintable regions for the model's base extruder. + // This could, on some models, produce bulges with the model's base color (#7109). + if (! mine.empty()) + mine = opening(union_ex(mine), float(scale_(5 * EPSILON)), float(scale_(5 * EPSILON))); if (! mine.empty()) { ByRegion &dst = by_region[layerm.region().print_object_region_id()]; if (dst.expolygons.empty()) { diff --git a/src/libslic3r/SlicesToTriangleMesh.cpp b/src/libslic3r/SlicesToTriangleMesh.cpp index 7a2975d12..3b55cf066 100644 --- a/src/libslic3r/SlicesToTriangleMesh.cpp +++ b/src/libslic3r/SlicesToTriangleMesh.cpp @@ -1,3 +1,4 @@ +#include #include "SlicesToTriangleMesh.hpp" @@ -22,11 +23,16 @@ inline indexed_triangle_set wall_strip(const Polygon &poly, ret.vertices.reserve(ret.vertices.size() + 2 *offs); + // The expression unscaled(p).cast().eval() is important here + // as it ensures identical conversion of 2D scaled coordinates to float 3D + // to that used by the tesselation. This way, the duplicated vertices in the + // output mesh can be found with the == operator of the points. + // its_merge_vertices will then reliably remove the duplicates. for (const Point &p : poly.points) - ret.vertices.emplace_back(to_3d(unscaled(p), float(lower_z_mm))); + ret.vertices.emplace_back(to_3d(unscaled(p).cast().eval(), float(lower_z_mm))); for (const Point &p : poly.points) - ret.vertices.emplace_back(to_3d(unscaled(p), float(upper_z_mm))); + ret.vertices.emplace_back(to_3d(unscaled(p).cast().eval(), float(upper_z_mm))); for (size_t i = startidx + 1; i < startidx + offs; ++i) { ret.indices.emplace_back(i - 1, i, i + offs - 1); @@ -84,12 +90,14 @@ indexed_triangle_set slices_to_mesh( const ExPolygons &upper = slices[i + 1]; const ExPolygons &lower = slices[i]; - ExPolygons dff1 = diff_ex(lower, upper); - ExPolygons dff2 = diff_ex(upper, lower); - its_merge(layers[i], triangulate_expolygons_3d(dff1, grid[i], NORMALS_UP)); - its_merge(layers[i], triangulate_expolygons_3d(dff2, grid[i], NORMALS_DOWN)); + // Small 0 area artefacts can be created by diff_ex, and the + // tesselation also can create 0 area triangles. These will be removed + // by its_remove_degenerate_faces. + ExPolygons free_top = diff_ex(lower, upper); + ExPolygons overhang = diff_ex(upper, lower); + its_merge(layers[i], triangulate_expolygons_3d(free_top, grid[i], NORMALS_UP)); + its_merge(layers[i], triangulate_expolygons_3d(overhang, grid[i], NORMALS_DOWN)); its_merge(layers[i], straight_walls(upper, grid[i], grid[i + 1])); - }); auto merge_fn = []( const indexed_triangle_set &a, const indexed_triangle_set &b ) { @@ -99,37 +107,30 @@ indexed_triangle_set slices_to_mesh( auto ret = execution::reduce(ex_tbb, layers.begin(), layers.end(), indexed_triangle_set{}, merge_fn); - // sla::Contour3D ret = tbb::parallel_reduce( - // tbb::blocked_range(layers.begin(), layers.end()), - // sla::Contour3D{}, - // [](const tbb::blocked_range& r, sla::Contour3D - // init) { - // for(auto it = r.begin(); it != r.end(); ++it ) - // init.merge(*it); return init; - // }, - // []( const sla::Contour3D &a, const sla::Contour3D &b ) { - // sla::Contour3D res{a}; res.merge(b); return res; - // }); - its_merge(ret, triangulate_expolygons_3d(slices.front(), zmin, NORMALS_DOWN)); its_merge(ret, straight_walls(slices.front(), zmin, grid.front())); its_merge(ret, triangulate_expolygons_3d(slices.back(), grid.back(), NORMALS_UP)); - + + // FIXME: these repairs do not fix the mesh entirely. There will be cracks + // in the output. It is very hard to do the meshing in a way that does not + // leave errors. + its_merge_vertices(ret); + its_remove_degenerate_faces(ret); + its_compactify_vertices(ret); + return ret; } void slices_to_mesh(indexed_triangle_set & mesh, - const std::vector &slices, - double zmin, - double lh, - double ilh) + const std::vector &slices, + double zmin, + double lh, + double ilh) { - std::vector wall_meshes(slices.size()); std::vector grid(slices.size(), zmin + ilh); - - for (size_t i = 1; i < grid.size(); ++i) - grid[i] = grid[i - 1] + lh; - + + for (size_t i = 1; i < grid.size(); ++i) grid[i] = grid[i - 1] + lh; + indexed_triangle_set cntr = slices_to_mesh(slices, zmin, grid); its_merge(mesh, cntr); } diff --git a/src/libslic3r/SlicesToTriangleMesh.hpp b/src/libslic3r/SlicesToTriangleMesh.hpp index 2fd177885..57b540d9f 100644 --- a/src/libslic3r/SlicesToTriangleMesh.hpp +++ b/src/libslic3r/SlicesToTriangleMesh.hpp @@ -7,10 +7,10 @@ namespace Slic3r { void slices_to_mesh(indexed_triangle_set & mesh, - const std::vector &slices, - double zmin, - double lh, - double ilh); + const std::vector &slices, + double zmin, + double lh, + double ilh); inline indexed_triangle_set slices_to_mesh( const std::vector &slices, double zmin, double lh, double ilh) diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index fb2621225..efd8e97f7 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -705,22 +705,16 @@ void its_flip_triangles(indexed_triangle_set &its) int its_remove_degenerate_faces(indexed_triangle_set &its, bool shrink_to_fit) { - int last = 0; - for (int i = 0; i < int(its.indices.size()); ++ i) { - const stl_triangle_vertex_indices &face = its.indices[i]; - if (face(0) != face(1) && face(0) != face(2) && face(1) != face(2)) { - if (last < i) - its.indices[last] = its.indices[i]; - ++ last; - } - } - int removed = int(its.indices.size()) - last; - if (removed) { - its.indices.erase(its.indices.begin() + last, its.indices.end()); - // Optionally shrink the vertices. - if (shrink_to_fit) - its.indices.shrink_to_fit(); - } + auto it = std::remove_if(its.indices.begin(), its.indices.end(), [](auto &face) { + return face(0) == face(1) || face(0) == face(2) || face(1) == face(2); + }); + + int removed = std::distance(it, its.indices.end()); + its.indices.erase(it, its.indices.end()); + + if (removed && shrink_to_fit) + its.indices.shrink_to_fit(); + return removed; } diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 23713dd11..77a74bffe 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -22,6 +22,7 @@ #include "../GUI/GUI_App.hpp" #include "../GUI/I18N.hpp" #include "../GUI/MainFrame.hpp" +#include "../GUI/MsgDialog.hpp" #include @@ -591,7 +592,7 @@ bool take_config_snapshot_cancel_on_error(const AppConfig &app_config, Snapshot: SnapshotDB::singleton().take_snapshot(app_config, reason, comment); return true; } catch (std::exception &err) { - wxRichMessageDialog dlg(static_cast(wxGetApp().mainframe), + RichMessageDialog dlg(static_cast(wxGetApp().mainframe), _L("PrusaSlicer has encountered an error while taking a configuration snapshot.") + "\n\n" + from_u8(err.what()) + "\n\n" + from_u8(message), _L("PrusaSlicer error"), wxYES_NO); diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index ee28fd718..6e7174baf 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1131,29 +1131,45 @@ void GLVolumeCollection::update_colors_by_extruder(const DynamicPrintConfig* con if (config == nullptr) return; - const ConfigOptionStrings* extruders_opt = dynamic_cast(config->option("extruder_colour")); - if (extruders_opt == nullptr) - return; - - const ConfigOptionStrings* filamemts_opt = dynamic_cast(config->option("filament_colour")); - if (filamemts_opt == nullptr) - return; - - unsigned int colors_count = std::max((unsigned int)extruders_opt->values.size(), (unsigned int)filamemts_opt->values.size()); - if (colors_count == 0) - return; - - std::vector colors(colors_count); - unsigned char rgb[3]; - for (unsigned int i = 0; i < colors_count; ++i) { - const std::string& txt_color = config->opt_string("extruder_colour", i); + std::vector colors; + + if (static_cast(config->opt_int("printer_technology")) == ptSLA) + { + const ConfigOptionStrings* resin_clr = dynamic_cast(config->option("material_colour")); + if (resin_clr == nullptr) + return; + assert(resin_clr->values.size() == 1); + colors.resize(1); + + const std::string& txt_color = config->opt_string("material_colour", 0); if (Slic3r::GUI::BitmapCache::parse_color(txt_color, rgb)) - colors[i].set(txt_color, rgb); - else { - const std::string& txt_color = config->opt_string("filament_colour", i); + colors[0].set(txt_color, rgb); + } + else + { + const ConfigOptionStrings* extruders_opt = dynamic_cast(config->option("extruder_colour")); + if (extruders_opt == nullptr) + return; + + const ConfigOptionStrings* filamemts_opt = dynamic_cast(config->option("filament_colour")); + if (filamemts_opt == nullptr) + return; + + unsigned int colors_count = std::max((unsigned int)extruders_opt->values.size(), (unsigned int)filamemts_opt->values.size()); + if (colors_count == 0) + return; + colors.resize(colors_count); + + for (unsigned int i = 0; i < colors_count; ++i) { + const std::string& txt_color = config->opt_string("extruder_colour", i); if (Slic3r::GUI::BitmapCache::parse_color(txt_color, rgb)) colors[i].set(txt_color, rgb); + else { + const std::string& txt_color = config->opt_string("filament_colour", i); + if (Slic3r::GUI::BitmapCache::parse_color(txt_color, rgb)) + colors[i].set(txt_color, rgb); + } } } diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index b3b00f60c..762de2cf5 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -555,7 +555,7 @@ PagePrinters::PagePrinters(ConfigWizard *parent, wizard_p()->on_printer_pick(this, evt); }); - append(new wxStaticLine(this)); + append(new StaticLine(this)); append(picker); printer_pickers.push_back(picker); @@ -2800,11 +2800,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) auto *vsizer = new wxBoxSizer(wxVERTICAL); auto *topsizer = new wxBoxSizer(wxHORIZONTAL); - wxStaticLine* hline = nullptr; -#ifdef _MSW_DARK_MODE - if (!NppDarkMode::IsEnabled()) -#endif //_MSW_DARK_MODE - hline = new wxStaticLine(this); + auto* hline = new StaticLine(this); p->btnsizer = new wxBoxSizer(wxHORIZONTAL); // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. @@ -2880,8 +2876,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) p->index->go_to(size_t{0}); vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); - if (hline) - vsizer->Add(hline, 0, wxEXPAND); + vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); SetSizer(vsizer); diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp index 0214652b2..01407b09a 100644 --- a/src/slic3r/GUI/GLShadersManager.cpp +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -79,7 +79,9 @@ std::pair GLShadersManager::init() // For Apple's on Arm CPU computed triangle normals inside fragment shader using dFdx and dFdy has the opposite direction. // Because of this, objects had darker colors inside the multi-material gizmo. // Based on https://stackoverflow.com/a/66206648, the similar behavior was also spotted on some other devices with Arm CPU. - if (platform_flavor() == PlatformFlavor::OSXOnArm) + // Since macOS 12 (Monterey), this issue with the opposite direction on Apple's Arm CPU seems to be fixed, and computed + // triangle normals inside fragment shader have the right direction. + if (platform_flavor() == PlatformFlavor::OSXOnArm && wxPlatformInfo::Get().GetOSMajorVersion() < 12) valid &= append_shader("mm_gouraud", {"mm_gouraud.vs", "mm_gouraud.fs"}, {"FLIP_TRIANGLE_NORMALS"sv}); else valid &= append_shader("mm_gouraud", {"mm_gouraud.vs", "mm_gouraud.fs"}); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index bab07b466..9c16a782e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -411,7 +411,7 @@ bool static check_old_linux_datadir(const wxString& app_name) { "location again.\n\n" "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); - wxRichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); + RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); if (dlg.ShowModal() != wxID_NO) return false; @@ -846,7 +846,7 @@ bool GUI_App::check_older_app_config(Semver current_version, bool backup) return false; BOOST_LOG_TRIVIAL(info) << "last app config file used: " << m_older_data_dir_path; // ask about using older data folder - wxRichMessageDialog msg(nullptr, backup ? + RichMessageDialog msg(nullptr, backup ? wxString::Format(_L("PrusaSlicer detected another configuration folder at %s." "\nIts version is %s." "\nLast version you used in current configuration folder is %s." @@ -936,7 +936,7 @@ bool GUI_App::on_init_inner() // win32 build on win64 and viceversa #ifdef _WIN64 if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "") { - wxRichMessageDialog dlg(nullptr, + RichMessageDialog dlg(nullptr, _L("You have started PrusaSlicer for 64-bit architecture on 32-bit system." "\nPlease download and install correct version at https://www.prusa3d.cz/prusaslicer/." "\nDo you wish to continue?"), @@ -946,7 +946,7 @@ bool GUI_App::on_init_inner() } #elif _WIN32 if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { - wxRichMessageDialog dlg(nullptr, + RichMessageDialog dlg(nullptr, _L("You have started PrusaSlicer for 32-bit architecture on 64-bit system." "\nPlease download and install correct version at https://www.prusa3d.cz/prusaslicer/." "\nDo you wish to continue?"), @@ -991,7 +991,7 @@ bool GUI_App::on_init_inner() bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); if (!msg.empty() && !ssl_accept) { - wxRichMessageDialog + RichMessageDialog dlg(nullptr, wxString::Format(_L("%s\nDo you want to continue?"), msg), "PrusaSlicer", wxICON_QUESTION | wxYES_NO); @@ -1620,6 +1620,7 @@ void GUI_App::update_ui_from_settings() m_force_colors_update = false; mainframe->force_color_changed(); mainframe->diff_dialog.force_color_changed(); + mainframe->printhost_queue_dlg()->force_color_changed(); #ifdef _MSW_DARK_MODE update_scrolls(mainframe); #endif //_MSW_DARK_MODE @@ -2855,7 +2856,7 @@ bool GUI_App::open_browser_with_warning_dialog(const wxString& url, int flags/* bool launch = true; if (get_app_config()->get("suppress_hyperlinks").empty()) { - wxRichMessageDialog dialog(nullptr, _L("Should we open this hyperlink in your default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + RichMessageDialog dialog(nullptr, _L("Should we open this hyperlink in your default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); dialog.ShowCheckBox(_L("Remember my choice")); int answer = dialog.ShowModal(); launch = answer == wxID_YES; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 8a24bee2e..7674fe908 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2044,8 +2044,7 @@ void ObjectList::split() void ObjectList::merge(bool to_multipart_object) { // merge selected objects to the multipart object - if (to_multipart_object) - { + if (to_multipart_object) { auto get_object_idxs = [this](std::vector& obj_idxs, wxDataViewItemArray& sels) { // check selections and split instances to the separated objects... @@ -2056,8 +2055,7 @@ void ObjectList::merge(bool to_multipart_object) break; } - if (!instance_selection) - { + if (!instance_selection) { for (wxDataViewItem item : sels) { assert(m_objects_model->GetItemType(item) & itObject); obj_idxs.emplace_back(m_objects_model->GetIdByItem(item)); @@ -2069,8 +2067,7 @@ void ObjectList::merge(bool to_multipart_object) std::map> sel_map; std::set empty_set; for (wxDataViewItem item : sels) { - if (m_objects_model->GetItemType(item) & itObject) - { + if (m_objects_model->GetItemType(item) & itObject) { int obj_idx = m_objects_model->GetIdByItem(item); int inst_cnt = (*m_objects)[obj_idx]->instances.size(); if (inst_cnt == 1) @@ -2087,8 +2084,7 @@ void ObjectList::merge(bool to_multipart_object) // all objects, created from the instances will be added to the end of list int new_objects_cnt = 0; // count of this new objects - for (auto map_item : sel_map) - { + for (auto map_item : sel_map) { int obj_idx = map_item.first; // object with just 1 instance if (map_item.second.empty()) { @@ -2148,37 +2144,36 @@ void ObjectList::merge(bool to_multipart_object) new_object->name = _u8L("Merged"); ModelConfig &config = new_object->config; - for (int obj_idx : obj_idxs) - { + for (int obj_idx : obj_idxs) { ModelObject* object = (*m_objects)[obj_idx]; const Geometry::Transformation& transformation = object->instances[0]->get_transformation(); - Vec3d scale = transformation.get_scaling_factor(); - Vec3d mirror = transformation.get_mirror(); - Vec3d rotation = transformation.get_rotation(); + const Vec3d scale = transformation.get_scaling_factor(); + const Vec3d mirror = transformation.get_mirror(); + const Vec3d rotation = transformation.get_rotation(); if (object->id() == (*m_objects)[obj_idxs.front()]->id()) new_object->add_instance(); - Transform3d volume_offset_correction = new_object->instances[0]->get_transformation().get_matrix().inverse() * transformation.get_matrix(); + const Transform3d& volume_offset_correction = transformation.get_matrix(); // merge volumes for (const ModelVolume* volume : object->volumes) { ModelVolume* new_volume = new_object->add_volume(*volume); //set rotation - Vec3d vol_rot = new_volume->get_rotation() + rotation; + const Vec3d vol_rot = new_volume->get_rotation() + rotation; new_volume->set_rotation(vol_rot); // set scale - Vec3d vol_sc_fact = new_volume->get_scaling_factor().cwiseProduct(scale); + const Vec3d vol_sc_fact = new_volume->get_scaling_factor().cwiseProduct(scale); new_volume->set_scaling_factor(vol_sc_fact); // set mirror - Vec3d vol_mirror = new_volume->get_mirror().cwiseProduct(mirror); + const Vec3d vol_mirror = new_volume->get_mirror().cwiseProduct(mirror); new_volume->set_mirror(vol_mirror); // set offset - Vec3d vol_offset = volume_offset_correction* new_volume->get_offset(); + const Vec3d vol_offset = volume_offset_correction* new_volume->get_offset(); new_volume->set_offset(vol_offset); } new_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); @@ -2211,6 +2206,11 @@ void ObjectList::merge(bool to_multipart_object) for (const auto& range : object->layer_config_ranges) new_object->layer_config_ranges.emplace(range); } + + new_object->center_around_origin(); + new_object->translate_instances(-new_object->origin_translation); + new_object->origin_translation = Vec3d::Zero(); + // remove selected objects remove(); @@ -2221,8 +2221,7 @@ void ObjectList::merge(bool to_multipart_object) } // merge all parts to the one single object // all part's settings will be lost - else - { + else { wxDataViewItem item = GetSelection(); if (!item) return; diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index c279fad90..f5a274d69 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -866,6 +866,9 @@ void ObjectManipulation::change_rotation_value(int axis, double value) void ObjectManipulation::change_scale_value(int axis, double value) { + if (value <= 0.0) + return; + if (std::abs(m_cache.scale_rounded(axis) - value) < EPSILON) return; @@ -882,6 +885,9 @@ void ObjectManipulation::change_scale_value(int axis, double value) void ObjectManipulation::change_size_value(int axis, double value) { + if (value <= 0.0) + return; + if (std::abs(m_cache.size_rounded(axis) - value) < EPSILON) return; @@ -947,10 +953,26 @@ void ObjectManipulation::on_change(const std::string& opt_key, int axis, double change_position_value(axis, new_value); else if (opt_key == "rotation") change_rotation_value(axis, new_value); - else if (opt_key == "scale") - change_scale_value(axis, new_value); - else if (opt_key == "size") - change_size_value(axis, new_value); + else if (opt_key == "scale") { + if (new_value > 0.0) + change_scale_value(axis, new_value); + else { + new_value = m_cache.scale(axis); + m_cache.scale(axis) = 0.0; + m_cache.scale_rounded(axis) = DBL_MAX; + change_scale_value(axis, new_value); + } + } + else if (opt_key == "size") { + if (new_value > 0.0) + change_size_value(axis, new_value); + else { + new_value = m_cache.size(axis); + m_cache.size(axis) = 0.0; + m_cache.size_rounded(axis) = DBL_MAX; + change_size_value(axis, new_value); + } + } } void ObjectManipulation::set_uniform_scaling(const bool new_value) diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index c4465edba..0d42cec2d 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -122,7 +122,9 @@ public: std::string err; ConfigSubstitutions config_substitutions; - priv(Plater *plt) : plater{plt} {} + ImportDlg import_dlg; + + priv(Plater *plt) : plater{plt}, import_dlg{plt} {} }; SLAImportJob::SLAImportJob(std::shared_ptr pri, Plater *plater) @@ -176,14 +178,12 @@ void SLAImportJob::prepare() { reset(); - ImportDlg dlg{p->plater}; - - if (dlg.ShowModal() == wxID_OK) { - auto path = dlg.get_path(); + if (p->import_dlg.ShowModal() == wxID_OK) { + auto path = p->import_dlg.get_path(); auto nm = wxFileName(path); p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); - p->sel = dlg.get_selection(); - p->win = dlg.get_marchsq_windowsize(); + p->sel = p->import_dlg.get_selection(); + p->win = p->import_dlg.get_marchsq_windowsize(); p->config_substitutions.clear(); } else { p->path = ""; @@ -236,7 +236,7 @@ void SLAImportJob::finalize() if (!p->mesh.empty()) { bool is_centered = false; - p->plater->sidebar().obj_list()->load_mesh_object(TriangleMesh{p->mesh}, + p->plater->sidebar().obj_list()->load_mesh_object(TriangleMesh{std::move(p->mesh)}, name, is_centered); } diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index c4cdde3d9..bc17dd9ce 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -27,7 +27,7 @@ namespace GUI { MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &headline, wxWindowID button_id, wxBitmap bitmap) : wxDialog(parent ? parent : dynamic_cast(wxGetApp().mainframe), wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , boldfont(wxGetApp().normal_font()/*wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)*/) + , boldfont(wxGetApp().normal_font()) , content_sizer(new wxBoxSizer(wxVERTICAL)) , btn_sizer(new wxBoxSizer(wxHORIZONTAL)) { @@ -36,6 +36,7 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he this->SetFont(wxGetApp().normal_font()); this->CenterOnParent(); + auto *main_sizer = new wxBoxSizer(wxVERTICAL); auto *topsizer = new wxBoxSizer(wxHORIZONTAL); auto *rightsizer = new wxBoxSizer(wxVERTICAL); @@ -46,6 +47,7 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he rightsizer->AddSpacer(VERT_SPACING); rightsizer->Add(content_sizer, 1, wxEXPAND); + btn_sizer->AddStretchSpacer(); if (button_id != wxID_NONE) { auto *button = new wxButton(this, button_id); @@ -53,8 +55,6 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he btn_sizer->Add(button); } - rightsizer->Add(btn_sizer, 0, wxALIGN_RIGHT); - if (! bitmap.IsOk()) { bitmap = create_scaled_bitmap("PrusaSlicer_192px.png", this, 192); } @@ -64,7 +64,11 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he topsizer->Add(logo, 0, wxALL, BORDER); topsizer->Add(rightsizer, 1, wxTOP | wxBOTTOM | wxRIGHT | wxEXPAND, BORDER); - SetSizerAndFit(topsizer); + main_sizer->Add(topsizer, 1, wxEXPAND); + main_sizer->Add(new StaticLine(this), 0, wxEXPAND | wxLEFT | wxRIGHT, HORIZ_SPACING); + main_sizer->Add(btn_sizer, 0, wxALL | wxEXPAND, VERT_SPACING); + + SetSizerAndFit(main_sizer); } void MsgDialog::add_btn(wxWindowID btn_id, bool set_focus /*= false*/) @@ -72,7 +76,7 @@ void MsgDialog::add_btn(wxWindowID btn_id, bool set_focus /*= false*/) wxButton* btn = new wxButton(this, btn_id); if (set_focus) btn->SetFocus(); - btn_sizer->Add(btn, 0, wxRIGHT, HORIZ_SPACING); + btn_sizer->Add(btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, HORIZ_SPACING); btn->Bind(wxEVT_BUTTON, [this, btn_id](wxCommandEvent&) { this->EndModal(btn_id); }); }; @@ -209,33 +213,38 @@ MessageDialog::MessageDialog(wxWindow* parent, apply_style(style); finalize(); } -#endif -// MessageWithCheckDialog +// RichMessageDialog -MessageWithCheckDialog::MessageWithCheckDialog( wxWindow* parent, - const wxString& message, - const wxString& checkbox_label, - const wxString& caption/* = wxEmptyString*/, - long style/* = wxOK*/) +RichMessageDialog::RichMessageDialog(wxWindow* parent, + const wxString& message, + const wxString& caption/* = wxEmptyString*/, + long style/* = wxOK*/) : MsgDialog(parent, caption.IsEmpty() ? wxString::Format(_L("%s info"), SLIC3R_APP_NAME) : caption, wxEmptyString, wxID_NONE) { add_msg_content(this, content_sizer, message); - m_check = new wxCheckBox(this, wxID_ANY, checkbox_label); - content_sizer->Add(m_check, 0, wxTOP, 10); + m_checkBox = new wxCheckBox(this, wxID_ANY, m_checkBoxText); + m_checkBox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { m_checkBoxValue = m_checkBox->GetValue(); }); + + btn_sizer->Insert(0, m_checkBox, wxALIGN_CENTER_VERTICAL); apply_style(style); finalize(); } -bool MessageWithCheckDialog::GetCheckVal() +int RichMessageDialog::ShowModal() { - if (m_check) - return m_check->GetValue(); - return false; + if (m_checkBoxText.IsEmpty()) + m_checkBox->Hide(); + else + m_checkBox->SetLabelText(m_checkBoxText); + Layout(); + + return wxDialog::ShowModal(); } +#endif // InfoDialog diff --git a/src/slic3r/GUI/MsgDialog.hpp b/src/slic3r/GUI/MsgDialog.hpp index d3263f970..a4807fd06 100644 --- a/src/slic3r/GUI/MsgDialog.hpp +++ b/src/slic3r/GUI/MsgDialog.hpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include class wxBoxSizer; class wxCheckBox; @@ -17,7 +20,6 @@ namespace Slic3r { namespace GUI { - // A message / query dialog with a bitmap on the left and any content on the right // with buttons underneath. struct MsgDialog : wxDialog @@ -87,6 +89,23 @@ public: }; #ifdef _WIN32 +// Generic static line, used intead of wxStaticLine +class StaticLine: public wxTextCtrl +{ +public: + StaticLine( wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxLI_HORIZONTAL, + const wxString& name = wxString::FromAscii(wxTextCtrlNameStr)) + : wxTextCtrl(parent, id, wxEmptyString, pos, size!=wxDefaultSize ? size : (style == wxLI_HORIZONTAL ? wxSize(10, 1) : wxSize(1, 10)), wxSIMPLE_BORDER, wxDefaultValidator, name) + { + this->Enable(false); + } + ~StaticLine() {} +}; + // Generic message dialog, used intead of wxMessageDialog class MessageDialog : public MsgDialog { @@ -101,7 +120,158 @@ public: MessageDialog &operator=(const MessageDialog&) = delete; virtual ~MessageDialog() = default; }; + +// Generic rich message dialog, used intead of wxRichMessageDialog +class RichMessageDialog : public MsgDialog +{ + wxCheckBox* m_checkBox{ nullptr }; + wxString m_checkBoxText; + bool m_checkBoxValue{ false }; + +public: + RichMessageDialog( wxWindow *parent, + const wxString& message, + const wxString& caption = wxEmptyString, + long style = wxOK); + RichMessageDialog(RichMessageDialog&&) = delete; + RichMessageDialog(const RichMessageDialog&) = delete; + RichMessageDialog &operator=(RichMessageDialog&&) = delete; + RichMessageDialog &operator=(const RichMessageDialog&) = delete; + virtual ~RichMessageDialog() = default; + + int ShowModal() override; + + void ShowCheckBox(const wxString& checkBoxText, bool checked = false) + { + m_checkBoxText = checkBoxText; + m_checkBoxValue = checked; + } + + wxString GetCheckBoxText() const { return m_checkBoxText; } + bool IsCheckBoxChecked() const { return m_checkBoxValue; } + +// This part o fcode isported from the "wx\msgdlg.h" + using wxMD = wxMessageDialogBase; + // customization of the message box buttons + virtual bool SetYesNoLabels(const wxMD::ButtonLabel& yes, const wxMD::ButtonLabel& no) + { + DoSetCustomLabel(m_yes, yes); + DoSetCustomLabel(m_no, no); + return true; + } + + virtual bool SetYesNoCancelLabels(const wxMD::ButtonLabel& yes, + const wxMD::ButtonLabel& no, + const wxMD::ButtonLabel& cancel) + { + DoSetCustomLabel(m_yes, yes); + DoSetCustomLabel(m_no, no); + DoSetCustomLabel(m_cancel, cancel); + return true; + } + + virtual bool SetOKLabel(const wxMD::ButtonLabel& ok) + { + DoSetCustomLabel(m_ok, ok); + return true; +} + + virtual bool SetOKCancelLabels(const wxMD::ButtonLabel& ok, + const wxMD::ButtonLabel& cancel) + { + DoSetCustomLabel(m_ok, ok); + DoSetCustomLabel(m_cancel, cancel); + return true; + } + + virtual bool SetHelpLabel(const wxMD::ButtonLabel& help) + { + DoSetCustomLabel(m_help, help); + return true; + } + // test if any custom labels were set + bool HasCustomLabels() const + { + return !(m_ok.empty() && m_cancel.empty() && m_help.empty() && + m_yes.empty() && m_no.empty()); + } + + // these functions return the label to be used for the button which is + // either a custom label explicitly set by the user or the default label, + // i.e. they always return a valid string + wxString GetYesLabel() const + { + return m_yes.empty() ? GetDefaultYesLabel() : m_yes; + } + wxString GetNoLabel() const + { + return m_no.empty() ? GetDefaultNoLabel() : m_no; + } + wxString GetOKLabel() const + { + return m_ok.empty() ? GetDefaultOKLabel() : m_ok; + } + wxString GetCancelLabel() const + { + return m_cancel.empty() ? GetDefaultCancelLabel() : m_cancel; + } + wxString GetHelpLabel() const + { + return m_help.empty() ? GetDefaultHelpLabel() : m_help; + } + +protected: + // this function is called by our public SetXXXLabels() and should assign + // the value to var with possibly some transformation (e.g. Cocoa version + // currently uses this to remove any accelerators from the button strings + // while GTK+ one handles stock items specifically here) + void DoSetCustomLabel(wxString& var, const wxMD::ButtonLabel& label) + { + var = label.GetAsString(); + } + + // these functions return the custom label or empty string and should be + // used only in specific circumstances such as creating the buttons with + // these labels (in which case it makes sense to only use a custom label if + // it was really given and fall back on stock label otherwise), use the + // Get{Yes,No,OK,Cancel}Label() methods above otherwise + const wxString& GetCustomYesLabel() const { return m_yes; } + const wxString& GetCustomNoLabel() const { return m_no; } + const wxString& GetCustomOKLabel() const { return m_ok; } + const wxString& GetCustomHelpLabel() const { return m_help; } + const wxString& GetCustomCancelLabel() const { return m_cancel; } + +private: + // these functions may be overridden to provide different defaults for the + // default button labels (this is used by wxGTK) + virtual wxString GetDefaultYesLabel() const { return wxGetTranslation("Yes"); } + virtual wxString GetDefaultNoLabel() const { return wxGetTranslation("No"); } + virtual wxString GetDefaultOKLabel() const { return wxGetTranslation("OK"); } + virtual wxString GetDefaultCancelLabel() const { return wxGetTranslation("Cancel"); } + virtual wxString GetDefaultHelpLabel() const { return wxGetTranslation("Help"); } + + // labels for the buttons, initially empty meaning that the defaults should + // be used, use GetYes/No/OK/CancelLabel() to access them + wxString m_yes, + m_no, + m_ok, + m_cancel, + m_help; +}; #else +// just a wrapper for wxStaticLine to use the same code on all platforms +class StaticLine : public wxStaticLine +{ +public: + StaticLine(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxLI_HORIZONTAL, + const wxString& name = wxString::FromAscii(wxStaticLineNameStr)) + : wxStaticLine(parent, id, pos, size, style, name) {} + ~StaticLine() {} +}; // just a wrapper to wxMessageBox to use the same code on all platforms class MessageDialog : public wxMessageDialog { @@ -113,25 +283,19 @@ public: : wxMessageDialog(parent, message, caption, style) {} ~MessageDialog() {} }; -#endif -class MessageWithCheckDialog : public MsgDialog +// just a wrapper to wxRichMessageBox to use the same code on all platforms +class RichMessageDialog : public wxRichMessageDialog { - wxCheckBox* m_check{ nullptr }; public: - MessageWithCheckDialog(wxWindow* parent, + RichMessageDialog(wxWindow* parent, const wxString& message, - const wxString& checkbox_label, const wxString& caption = wxEmptyString, - long style = wxOK); - MessageWithCheckDialog(MessageWithCheckDialog&&) = delete; - MessageWithCheckDialog(const MessageWithCheckDialog&) = delete; - MessageWithCheckDialog& operator=(MessageWithCheckDialog&&) = delete; - MessageWithCheckDialog& operator=(const MessageWithCheckDialog&) = delete; - virtual ~MessageWithCheckDialog() = default; - - bool GetCheckVal(); + long style = wxOK) + : wxRichMessageDialog(parent, message, caption, style) {} + ~RichMessageDialog() {} }; +#endif // Generic info dialog, used for displaying exceptions class InfoDialog : public MsgDialog diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index eb64fb08c..fe8c72fea 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1920,7 +1920,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "bed_shape", "bed_custom_texture", "bed_custom_model", "complete_objects", "duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance", "brim_width", "brim_separation", "brim_type", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material", "wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim_width", - "extruder_colour", "filament_colour", "max_print_height", "printer_model", "printer_technology", + "extruder_colour", "filament_colour", "material_colour", "max_print_height", "printer_model", "printer_technology", // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. "layer_height", "first_layer_height", "min_layer_height", "max_layer_height", "brim_width", "perimeters", "perimeter_extruder", "fill_density", "infill_extruder", "top_solid_layers", @@ -2482,15 +2482,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ model.convert_from_meters(true); }; if (answer_convert_from_meters == wxOK_DEFAULT) { - MessageWithCheckDialog dlg(q, format_wxstr(_L_PLURAL( + RichMessageDialog dlg(q, format_wxstr(_L_PLURAL( "The dimensions of the object from file %s seem to be defined in meters.\n" "The internal unit of PrusaSlicer are millimeters. Do you want to recalculate the dimensions of the object?", "The dimensions of some objects from file %s seem to be defined in meters.\n" "The internal unit of PrusaSlicer are millimeters. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) + "\n", - _L("Apply to all the remaining small objects being loaded."), _L("The object is too small"), wxICON_WARNING | wxYES | wxNO); + dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded.")); int answer = dlg.ShowModal(); - if (dlg.GetCheckVal()) + if (dlg.IsCheckBoxChecked()) answer_convert_from_meters = answer; else convert_model_if(model, answer == wxID_YES); @@ -2504,15 +2504,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ convert_from_imperial_units(model, true); }; if (answer_convert_from_imperial_units == wxOK_DEFAULT) { - MessageWithCheckDialog dlg(q, format_wxstr(_L_PLURAL( + RichMessageDialog dlg(q, format_wxstr(_L_PLURAL( "The dimensions of the object from file %s seem to be defined in inches.\n" "The internal unit of PrusaSlicer are millimeters. Do you want to recalculate the dimensions of the object?", "The dimensions of some objects from file %s seem to be defined in inches.\n" "The internal unit of PrusaSlicer are millimeters. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) + "\n", - _L("Apply to all the remaining small objects being loaded."), _L("The object is too small"), wxICON_WARNING | wxYES | wxNO); + dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded.")); int answer = dlg.ShowModal(); - if (dlg.GetCheckVal()) + if (dlg.IsCheckBoxChecked()) answer_convert_from_imperial_units = answer; else convert_model_if(model, answer == wxID_YES); @@ -6222,6 +6222,15 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } } + if (opt_key == "material_colour") { + update_scheduled = true; // update should be scheduled (for update 3DScene) + + // update material color in full config + std::vector material_colors = { config.opt_string("material_colour", (unsigned)0) }; + p->config->option("material_colour")->values = material_colors; + continue; + } + p->config->set_key_value(opt_key, config.option(opt_key)->clone()); if (opt_key == "printer_technology") { this->set_printer_technology(config.opt_enum(opt_key)); diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index 3f2dc5a44..4fd45f380 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -101,6 +101,8 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_pr EndDialog(wxID_OK); }); + wxGetApp().UpdateDlgDarkUI(this); + Fit(); CenterOnParent(); @@ -331,6 +333,14 @@ void PrintHostQueueDialog::on_dpi_changed(const wxRect &suggested_rect) save_user_data(UDT_SIZE | UDT_POSITION | UDT_COLS); } +void PrintHostQueueDialog::on_sys_color_changed() +{ +#ifdef _WIN32 + wxGetApp().UpdateDlgDarkUI(this); + wxGetApp().UpdateDVCDarkUI(job_list); +#endif +} + PrintHostQueueDialog::JobState PrintHostQueueDialog::get_state(int idx) { wxCHECK_MSG(idx >= 0 && idx < job_list->GetItemCount(), ST_ERROR, "Out of bounds access to job list"); diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index 294593bd1..eb5661159 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -72,6 +72,7 @@ public: } protected: void on_dpi_changed(const wxRect &suggested_rect) override; + void on_sys_color_changed() override; private: enum Column { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1a96f4baf..4880a3b7a 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4157,6 +4157,7 @@ void TabSLAMaterial::build() auto page = add_options_page(L("Material"), "resin"); auto optgroup = page->new_optgroup(L("Material")); + optgroup->append_single_option_line("material_colour"); optgroup->append_single_option_line("bottle_cost"); optgroup->append_single_option_line("bottle_volume"); optgroup->append_single_option_line("bottle_weight"); @@ -4164,6 +4165,12 @@ void TabSLAMaterial::build() optgroup->m_on_change = [this, optgroup](t_config_option_key opt_key, boost::any value) { + if (opt_key == "material_colour") { + update_dirty(); + on_value_change(opt_key, value); + return; + } + DynamicPrintConfig new_conf = *m_config; if (opt_key == "bottle_volume") {