diff --git a/CMakeLists.txt b/CMakeLists.txt index b8c38a6d41..bb556fcc83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,8 @@ set(engine_SRCS # Except main.cpp. src/utils/ExtrusionLine.cpp src/utils/ExtrusionSegment.cpp src/utils/gettime.cpp + src/utils/IdFieldInfo.cpp + src/utils/LabelMaker.cpp src/utils/linearAlg2D.cpp src/utils/ListPolyIt.cpp src/utils/Matrix4x3D.cpp diff --git a/include/ExtruderPlan.h b/include/ExtruderPlan.h index e4d301969a..f7126b42e2 100644 --- a/include/ExtruderPlan.h +++ b/include/ExtruderPlan.h @@ -25,6 +25,7 @@ namespace cura { +class Image; class LayerPlanBuffer; class LayerPlan; /*! @@ -124,6 +125,10 @@ class ExtruderPlan */ void applyBackPressureCompensation(const Ratio back_pressure_compensation); + /*! + */ + void applyIdLabel(const Image& slice_id_texture, const coord_t current_z); + /*! * Gets the mesh being printed first on this plan */ diff --git a/include/FffGcodeWriter.h b/include/FffGcodeWriter.h index 4591e5c754..48cfbc0f7b 100644 --- a/include/FffGcodeWriter.h +++ b/include/FffGcodeWriter.h @@ -20,6 +20,7 @@ namespace cura { class AngleDegrees; +class Image; class Shape; class SkinPart; class SliceDataStorage; @@ -229,7 +230,7 @@ class FffGcodeWriter : public NoCopy * \param total_layers The total number of layers. * \return The layer plans */ - ProcessLayerResult processLayer(const SliceDataStorage& storage, LayerIndex layer_nr, const size_t total_layers) const; + ProcessLayerResult processLayer(const SliceDataStorage& storage, LayerIndex layer_nr, const size_t total_layers, const std::optional& slice_id_texture) const; /*! * This function checks whether prime blob should happen for any extruder on the first layer. diff --git a/include/LayerPlan.h b/include/LayerPlan.h index 81f7271cfc..a941b926ce 100644 --- a/include/LayerPlan.h +++ b/include/LayerPlan.h @@ -835,6 +835,10 @@ class LayerPlan : public NoCopy */ void applyGradualFlow(); + /*! + */ + void applyIdLabel(const Image& slice_id_texture); + /*! * Gets the mesh being printed first on this layer */ @@ -917,6 +921,7 @@ class LayerPlan : public NoCopy const coord_t path_z_offset, double extrusion_mm3_per_mm, PrintFeatureType feature, + const std::optional& inline_comment = std::nullopt, bool update_extrusion_offset = false); /*! diff --git a/include/SlicedUVCoordinates.h b/include/SlicedUVCoordinates.h index f9c9467c5c..85ec801601 100644 --- a/include/SlicedUVCoordinates.h +++ b/include/SlicedUVCoordinates.h @@ -5,10 +5,12 @@ #define SLICEDUVCOORDINATES_H #include +#include +#include #include "geometry/Point2LL.h" #include "utils/Point2F.h" -#include "utils/SparsePointGridInclusive.h" +#include "utils/SparseLineGrid.h" namespace cura { @@ -23,17 +25,34 @@ class SlicedUVCoordinates std::optional getClosestUVCoordinates(const Point2LL& position) const; + /*! /!\ WARNINGS /!\ + * - Currently assumes straight spans in UV-space will be over at most 2 faces, but this isn't _universally_ true. + * - Currently returns the UV-coords of the _entire_ segment from and to are part of, + * even if from and to repressent some points other than begin and end (though it should handle reversed). + */ + std::optional> getUVCoordsLineSegment(const Point2LL& from, const Point2LL& to) const; + private: struct Segment { Point2LL start, end; Point2F uv_start, uv_end; }; + struct SegmentLocator + { + public: + std::pair operator()(Segment* const& seg) + { + return { seg->start, seg->end }; + } + }; static constexpr coord_t cell_size{ 1000 }; static constexpr coord_t search_radius{ 1000 }; - SparsePointGridInclusive located_uv_coordinates_; + std::vector segments_; + SparseLineGrid located_uv_coords_segs_; + std::unordered_multimap segs_by_point_; }; } // namespace cura diff --git a/include/TextureDataMapping.h b/include/TextureDataMapping.h index 09b89e49fd..42b202aaed 100644 --- a/include/TextureDataMapping.h +++ b/include/TextureDataMapping.h @@ -7,6 +7,10 @@ #include #include +#include + +#include "utils/Point2F.h" + namespace cura { @@ -32,5 +36,26 @@ enum class TextureArea Avoid = 2, // Area is to be avoided }; +using Texel = std::pair; + } // namespace cura + +namespace fmt +{ +template<> +struct formatter +{ + constexpr auto parse(format_parse_context& ctx) + { + return ctx.end(); + } + + template + auto format(const cura::TextureBitField& tbf, FormatContext& ctx) const + { + return format_to(ctx.out(), "[{} -- {}]", tbf.bit_range_start_index, tbf.bit_range_end_index); + } +}; +} // namespace fmt + #endif // MESH_H diff --git a/include/TextureDataProvider.h b/include/TextureDataProvider.h index 843583108b..6ecb915d95 100644 --- a/include/TextureDataProvider.h +++ b/include/TextureDataProvider.h @@ -28,6 +28,8 @@ class TextureDataProvider std::optional getAreaPreference(const Point2LL& position, const std::string& feature) const; + bool getTexelsForSpan(const Point2LL& a, const Point2LL& b, const std::string& feature, std::vector& res) const; + private: std::shared_ptr uv_coordinates_; std::shared_ptr texture_; diff --git a/include/gcodeExport.h b/include/gcodeExport.h index 5a67ea3fc3..b69b5a6ded 100644 --- a/include/gcodeExport.h +++ b/include/gcodeExport.h @@ -372,7 +372,13 @@ class GCodeExport : public NoCopy * \param feature the feature that's currently printing * \param update_extrusion_offset whether to update the extrusion offset to match the current flow rate */ - void writeExtrusion(const Point2LL& p, const Velocity& speed, double extrusion_mm3_per_mm, PrintFeatureType feature, bool update_extrusion_offset = false); + void writeExtrusion( + const Point2LL& p, + const Velocity& speed, + double extrusion_mm3_per_mm, + PrintFeatureType feature, + const std::optional& inline_comment = std::nullopt, + bool update_extrusion_offset = false); /*! * Go to a X/Y location with the z-hopped Z value @@ -396,7 +402,13 @@ class GCodeExport : public NoCopy * \param feature the feature that's currently printing * \param update_extrusion_offset whether to update the extrusion offset to match the current flow rate */ - void writeExtrusion(const Point3LL& p, const Velocity& speed, double extrusion_mm3_per_mm, PrintFeatureType feature, bool update_extrusion_offset = false); + void writeExtrusion( + const Point3LL& p, + const Velocity& speed, + double extrusion_mm3_per_mm, + PrintFeatureType feature, + const std::optional& inline_comment = std::nullopt, + bool update_extrusion_offset = false); /*! * Initialize the extruder trains. @@ -471,6 +483,7 @@ class GCodeExport : public NoCopy const Velocity& speed, const double extrusion_mm3_per_mm, const PrintFeatureType& feature, + const std::optional& inline_comment = std::nullopt, const bool update_extrusion_offset = false); /*! @@ -490,6 +503,7 @@ class GCodeExport : public NoCopy const coord_t z, const double e, const PrintFeatureType& feature, + const std::optional& inline_comment = std::nullopt, const std::optional& retraction_amounts = std::nullopt); /*! diff --git a/include/geometry/Point3LL.h b/include/geometry/Point3LL.h index 97fb00167e..039460fe42 100644 --- a/include/geometry/Point3LL.h +++ b/include/geometry/Point3LL.h @@ -49,12 +49,19 @@ class Point3LL Point3LL operator*(const Point3LL& p) const; //!< Element-wise multiplication. For dot product, use .dot()! Point3LL operator/(const Point3LL& p) const; - template + template Point3LL operator*(const T& i) const { return { std::llround(static_cast(x_) * i), std::llround(static_cast(y_) * i), std::llround(static_cast(z_) * i) }; } + template + Point3LL operator*(const T& i) const + { + const coord_t ii = static_cast(i); + return { x_ * ii, y_ * ii, z_ * ii }; + } + template Point3LL operator/(const T& i) const { diff --git a/include/mesh.h b/include/mesh.h index 51c5c58a9b..ffb4ecea45 100644 --- a/include/mesh.h +++ b/include/mesh.h @@ -9,6 +9,7 @@ #include "TextureDataMapping.h" #include "settings/Settings.h" #include "utils/AABB3D.h" +#include "utils/IDFieldInfo.h" #include "utils/Matrix4x3D.h" #include "utils/Point2F.h" @@ -100,6 +101,8 @@ class Image return getPixel(static_cast(uv_coordinates.x_ * width_), static_cast(uv_coordinates.y_ * height_)); } + void visitSpanPerPixel(const Point2F& a, const Point2F& b, const std::function& func) const; + private: std::vector data_; // The raw pixels, data size_t width_{ 0 }; // The image width @@ -115,6 +118,7 @@ See MeshFace for the specifics of how/when faces are connected. */ class Mesh { +private: //! The vertex_hash_map stores a index reference of each vertex for the hash of that location. Allows for quick retrieval of points with the same location. std::unordered_map> vertex_hash_map_; AABB3D aabb_; diff --git a/include/pathPlanning/GCodePath.h b/include/pathPlanning/GCodePath.h index 6c7c0df21a..cc4fc44284 100644 --- a/include/pathPlanning/GCodePath.h +++ b/include/pathPlanning/GCodePath.h @@ -13,6 +13,7 @@ #include "geometry/Point2LL.h" #include "settings/types/Ratio.h" #include "sliceDataStorage.h" +#include "utils/Point2F.h" namespace cura { @@ -46,6 +47,8 @@ struct GCodePath bool perform_z_hop{ false }; //!< Whether to perform a z_hop in this path, which is assumed to be a travel path. bool perform_prime{ false }; //!< Whether this path is preceded by a prime (blob) std::vector points{}; //!< The points constituting this path. The Z coordinate is an offset relative to the actual layer height, added to the global z_offset. + std::optional> idlabel_uv_per_point + = std::nullopt; //!< If this path is part of an ID-label, contains for each path-point an UV-point into the label-texture. bool done{ false }; //!< Path is finished, no more moves should be added, and a new path should be started instead of any appending done to this one. double fan_speed{ GCodePathConfig::FAN_SPEED_DEFAULT }; //!< fan speed override for this path, value should be within range 0-100 (inclusive) and ignored otherwise TimeMaterialEstimates estimates{}; //!< Naive time and material estimates diff --git a/include/sliceDataStorage.h b/include/sliceDataStorage.h index 75c977a598..147aa895d3 100644 --- a/include/sliceDataStorage.h +++ b/include/sliceDataStorage.h @@ -23,6 +23,7 @@ #include "settings/types/LayerIndex.h" #include "utils/AABB.h" #include "utils/AABB3D.h" +#include "utils/IDFieldInfo.h" #include "utils/NoCopy.h" namespace cura @@ -307,6 +308,7 @@ class SliceMeshStorage Settings& settings; std::vector layers; std::string mesh_name; + std::optional id_field_info; LayerIndex layer_nr_max_filled_layer; //!< the layer number of the uppermost layer with content (modified while infill meshes are processed) @@ -337,6 +339,10 @@ class SliceMeshStorage */ SliceMeshStorage(Mesh* mesh, const size_t slice_layer_count); + /*! + */ + void setIdFieldInfo(const std::vector& label_pt_cloud); + /*! * \param extruder_nr The extruder for which to check * \return whether a particular extruder is used by this mesh diff --git a/include/slicer.h b/include/slicer.h index e3aa9cf260..9086b2e6b0 100644 --- a/include/slicer.h +++ b/include/slicer.h @@ -77,6 +77,10 @@ class SlicerLayer */ void makePolygons(const Mesh* mesh); + /*! + */ + void clearSegments(); + protected: /*! * Connect the segments into loops which correctly form polygons (don't perform stitching here) diff --git a/include/utils/AABB3D.h b/include/utils/AABB3D.h index e7d4b9eda5..4a41df1a44 100644 --- a/include/utils/AABB3D.h +++ b/include/utils/AABB3D.h @@ -42,6 +42,14 @@ struct AABB3D */ AABB flatten() const; + /*! + * Whether or not this represents an actual box in 'real' space, not a _negative_ volume. + * Note that the volume might still be empty (== 0.0), which can happen if it represents a(n axis aligned) plane, a line, or a point. + * + * \return Whether or not the space the box envelops is at least 0. + */ + bool exists() const; + /*! * Check whether this aabb overlaps with another. * diff --git a/include/utils/IDFieldInfo.h b/include/utils/IDFieldInfo.h new file mode 100644 index 0000000000..29cc575d12 --- /dev/null +++ b/include/utils/IDFieldInfo.h @@ -0,0 +1,39 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#ifndef ID_FIELD_INFO_H +#define ID_FIELD_INFO_H + +#include + +#include "geometry/Point3LL.h" +#include "utils/AABB.h" +#include "utils/Point2F.h" +#include "utils/Point3D.h" + +namespace cura +{ +const double CLOSE_1D = std::nextafter(1.0, 0.0); +const float CLOSE_1F = std::nextafter(1.0f, 0.0f); +// NOTE: nextafter isn't constexpr yet in c++20, replace with constexpr when we do C++23 + +struct IdFieldInfo +{ + typedef std::pair span_t; + + Point3D primary_axis_; + span_t primary_span_; + + Point3D secondary_axis_; + span_t secondary_span_; + + Point3D normal_; + +public: + static std::optional fromPointCloud(const std::vector& points); + + Point2F worldPointToLabelUv(const Point3LL& pt) const; +}; +} // namespace cura + +#endif // ID_FIELD_INFO_H diff --git a/include/utils/LabelMaker.h b/include/utils/LabelMaker.h new file mode 100644 index 0000000000..36f216a749 --- /dev/null +++ b/include/utils/LabelMaker.h @@ -0,0 +1,16 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#ifndef LABEL_MAKER_H +#define LABEL_MAKER_H + +#include +#include +#include + +namespace cura +{ +void paintStringToBuffer(const std::string_view& str, const size_t buffer_width, const size_t buffer_height, std::vector& buffer); +} + +#endif // LABEL_MAKER_H diff --git a/include/utils/Point2F.h b/include/utils/Point2F.h index 07f10c1849..c3f85a317c 100644 --- a/include/utils/Point2F.h +++ b/include/utils/Point2F.h @@ -4,6 +4,8 @@ #ifndef POINT2F_H #define POINT2F_H +#include + namespace cura { @@ -37,6 +39,11 @@ class Point2F return *this / vSize(); } + Point2F operator*(const float scale) const + { + return Point2F(x_ * scale, y_ * scale); + } + Point2F operator/(const double scale) const { return Point2F(x_ / scale, y_ / scale); @@ -45,6 +52,16 @@ class Point2F auto operator<=>(const Point2F&) const = default; }; +static Point2F operator+(const Point2F& a, const Point2F& b) +{ + return Point2F(a.x_ + b.x_, a.y_ + b.y_); +} + +static Point2F operator-(const Point2F& a, const Point2F& b) +{ + return Point2F(a.x_ - b.x_, a.y_ - b.y_); +} + static Point2F lerp(const Point2F& a, const Point2F& b, const float t) { return Point2F(std::lerp(a.x_, b.x_, t), std::lerp(a.y_, b.y_, t)); diff --git a/src/ExtruderPlan.cpp b/src/ExtruderPlan.cpp index 58a6bb3f86..a1bcab3757 100644 --- a/src/ExtruderPlan.cpp +++ b/src/ExtruderPlan.cpp @@ -3,6 +3,9 @@ #include "ExtruderPlan.h" +#include "TextureDataProvider.h" +#include "mesh.h" // For 'Image' class. + namespace cura { ExtruderPlan::ExtruderPlan( @@ -71,6 +74,88 @@ void ExtruderPlan::applyBackPressureCompensation(const Ratio back_pressure_compe } } +void ExtruderPlan::applyIdLabel(const Image& slice_id_texture, const coord_t current_z) +{ + // FIXME: properly deal with (mostly or wholly) top or bottom (normal = Z-axis) labels + + // TODO?: message (format) should be a (string) setting, like 'ID: \H:\M:\S' or something + + constexpr coord_t inset_dist = 40; // TODO?: make this configurable as well? + for (auto& path : paths_) + { + if (path.points.empty() || path.mesh == nullptr || path.mesh->layers[layer_nr_].texture_data_provider_ == nullptr + || (! path.mesh->id_field_info) + //|| (path.mesh->id_field_info.value().normal_ != IdFieldInfo::Axis::Z && path.config.type != PrintFeatureType::OuterWall) + //|| (path.mesh->id_field_info.value().normal_ == IdFieldInfo::Axis::Z && path.config.type != PrintFeatureType::Skin)) + || (path.config.type != PrintFeatureType::OuterWall && path.config.type != PrintFeatureType::Skin)) + { + continue; + } + + const auto zero_pt = Point3LL(0, 0, 0); + const auto offset_pt = ((path.points.front() + path.points.back()) / 2 - path.mesh->bounding_box.getMiddle()).resized(inset_dist); + const auto offset_z = Point3LL(0, 0, current_z + path.z_offset); + const auto signal_no_uv = Point2F(std::numeric_limits::signaling_NaN(), std::numeric_limits::signaling_NaN()); + + const auto& id_field_info = path.mesh->id_field_info.value(); + + // FIMXE!: The sliding here is only going over 3 sides of the polygon, which means it's not 'closed' at this moment in time. + // Attempts to fix this have so far not been successful (but tired r.n. will look at it when less so later). + + std::vector new_points; + std::vector idlabel_uvs; + new_points.push_back(path.points.front()); + idlabel_uvs.push_back(signal_no_uv); + for (const auto& window : path.points | ranges::views::sliding(2)) + { + const auto& a = window[0]; + const auto& b = window[1]; + + std::vector span_pixels; + if (path.mesh->layers[layer_nr_].texture_data_provider_->getTexelsForSpan(a.toPoint2LL(), b.toPoint2LL(), "label", span_pixels)) + { + const auto pixel_span_3d = (b - a) / static_cast(span_pixels.size()); + auto last_pixel = TextureArea::Normal; + auto last_pt = a; + for (const auto& [idx, texel] : span_pixels | ranges::views::enumerate) + { + const auto raw_pt = a + (idx * pixel_span_3d) + (pixel_span_3d / 2); + const bool preferred = (texel.first == TextureArea::Preferred); + if (preferred || last_pixel != texel.first) + { + if (last_pixel != TextureArea::Preferred) + { + new_points.push_back(last_pt); + idlabel_uvs.push_back(signal_no_uv); + } + + const auto label_uv = id_field_info.worldPointToLabelUv(raw_pt + offset_z); + if (std::clamp(label_uv.x_, 0.0f, CLOSE_1F) == label_uv.x_ && std::clamp(label_uv.y_, 0.0f, CLOSE_1F) == label_uv.y_) + { + const bool raw_val = slice_id_texture.getPixel(label_uv) > 0b0; + const bool val = preferred && raw_val; + + new_points.push_back(raw_pt + (val ? offset_pt : zero_pt)); + idlabel_uvs.push_back(label_uv); + } + } + last_pixel = texel.first; + last_pt = raw_pt; + } + } + + new_points.push_back(b); + idlabel_uvs.push_back(signal_no_uv); + } + + if (new_points.size() != path.points.size()) + { + path.points = new_points; + path.idlabel_uv_per_point = idlabel_uvs; + } + } +} + std::shared_ptr ExtruderPlan::findFirstPrintedMesh() const { for (const GCodePath& path : paths_) diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index 12dc4b1ea3..d87ce6a4fd 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -33,6 +33,7 @@ #include "infill.h" #include "progress/Progress.h" #include "raft.h" +#include "utils/LabelMaker.h" #include "utils/Simplify.h" //Removing micro-segments created by offsetting. #include "utils/ThreadPool.h" #include "utils/linearAlg2D.h" @@ -84,8 +85,32 @@ bool FffGcodeWriter::setTargetFile(const char* filename) return false; } +std::string getTimeStamp() +{ + std::time_t time = std::time({}); + char str[std::size(" yyyy-mm-dd hh:mm:ss")]; + std::strftime(std::data(str), std::size(str), " %F %T", std::gmtime(&time)); + return std::string(str); +} + void FffGcodeWriter::writeGCode(SliceDataStorage& storage, TimeKeeper& time_keeper) { + std::optional slice_id_texture = std::nullopt; + if (std::any_of( + storage.meshes.begin(), + storage.meshes.end(), + [](std::shared_ptr mesh) + { + return mesh->id_field_info.has_value(); + })) + { + constexpr size_t label_width = 128; + constexpr size_t label_height = 128; + std::vector buffer(label_width * label_height, 0); + paintStringToBuffer(getTimeStamp(), label_width, label_height, buffer); + slice_id_texture = std::make_optional(Image(label_width, label_height, 1, std::move(buffer))); + } + const size_t start_extruder_nr = getStartExtruder(storage); gcode.preSetup(start_extruder_nr); gcode.setSliceUUID(slice_uuid); @@ -186,9 +211,9 @@ void FffGcodeWriter::writeGCode(SliceDataStorage& storage, TimeKeeper& time_keep run_multiple_producers_ordered_consumer( process_layer_starting_layer_nr, total_layers, - [&storage, total_layers, this](int layer_nr) + [&storage, total_layers, &slice_id_texture, this](int layer_nr) { - return std::make_optional(processLayer(storage, layer_nr, total_layers)); + return std::make_optional(processLayer(storage, layer_nr, total_layers, slice_id_texture)); }, [this, total_layers](std::optional result_opt) { @@ -1151,7 +1176,8 @@ void FffGcodeWriter::endRaftLayer(const SliceDataStorage& storage, LayerPlan& gc } } -FffGcodeWriter::ProcessLayerResult FffGcodeWriter::processLayer(const SliceDataStorage& storage, LayerIndex layer_nr, const size_t total_layers) const +FffGcodeWriter::ProcessLayerResult + FffGcodeWriter::processLayer(const SliceDataStorage& storage, LayerIndex layer_nr, const size_t total_layers, const std::optional& slice_id_texture) const { spdlog::debug("GcodeWriter processing layer {} of {}", layer_nr, total_layers); TimeKeeper time_keeper; @@ -1310,6 +1336,11 @@ FffGcodeWriter::ProcessLayerResult FffGcodeWriter::processLayer(const SliceDataS gcode_layer.applyBackPressureCompensation(); time_keeper.registerTime("Back pressure comp."); + if (slice_id_texture.has_value()) + { + gcode_layer.applyIdLabel(slice_id_texture.value()); + } + return { &gcode_layer, timer_total.elapsed().count(), time_keeper.getRegisteredTimes() }; } diff --git a/src/FffPolygonGenerator.cpp b/src/FffPolygonGenerator.cpp index e7d8083a24..498efff1a6 100644 --- a/src/FffPolygonGenerator.cpp +++ b/src/FffPolygonGenerator.cpp @@ -291,15 +291,26 @@ bool FffPolygonGenerator::sliceModel(MeshGroup* meshgroup, TimeKeeper& timeKeepe // check one if raft offset is needed const bool has_raft = mesh_group_settings.get("adhesion_type") == EPlatformAdhesion::RAFT; + // init variable(s) & logging for the support 'painting' type operations + if (mesh.texture_ && mesh.texture_data_mapping_) + { + spdlog::info("Mesh `{}` painting-operation info:", static_cast(&mesh)); + for (auto [paint_type, tex_data] : *mesh.texture_data_mapping_) + { + spdlog::info("Texture-data for type '{}', mapped to pixel-bits: {}", paint_type, tex_data); + } + } + else + { + spdlog::info("No painting-operation data specified for mesh `{}`.", static_cast(&mesh)); + } + std::vector label_pt_cloud; + // calculate the height at which each layer is actually printed (printZ) for (LayerIndex layer_nr = 0; layer_nr < meshStorage.layers.size(); layer_nr++) { SliceLayer& layer = meshStorage.layers[layer_nr]; - const SlicerLayer& slicer_layer = slicer->layers[layer_nr]; - if (slicer_layer.sliced_uv_coordinates_ && mesh.texture_ && mesh.texture_data_mapping_) - { - layer.texture_data_provider_ = std::make_shared(slicer_layer.sliced_uv_coordinates_, mesh.texture_, mesh.texture_data_mapping_); - } + SlicerLayer& slicer_layer = slicer->layers[layer_nr]; if (use_variable_layer_heights) { @@ -332,8 +343,48 @@ bool FffPolygonGenerator::sliceModel(MeshGroup* meshgroup, TimeKeeper& timeKeepe layer.printZ += train.settings_.get("layer_0_z_overlap"); // undo shifting down of first layer } } + + // support for 'painting' type operations + if (slicer_layer.sliced_uv_coordinates_ && mesh.texture_ && mesh.texture_data_mapping_) + { + layer.texture_data_provider_ = std::make_shared(slicer_layer.sliced_uv_coordinates_, mesh.texture_, mesh.texture_data_mapping_); + + // slice/print-ID label specific: (update/)calculate bounding-box + if (mesh.texture_data_mapping_->contains("label")) + { + const auto match_pixel = static_cast(TextureArea::Preferred) << mesh.texture_data_mapping_->at("label").bit_range_start_index; + const auto& height = layer.printZ; + for (const auto& segment : slicer_layer.segments_) + // TODO: Deal with the fact that we _actually_ don't have the segments in that place anymore (I temporarily disabled the clear). + // (We've still got all we need in the slicer_layer.sliced_uv_coordinates_.segments, but that's all private at the moment! + { + if (segment.uv_start.has_value() && segment.uv_end.has_value()) + { + const auto& uv_a = segment.uv_start.value(); + const auto& uv_b = segment.uv_end.value(); + const auto& a = segment.start; + const auto& b = segment.end; + mesh.texture_->visitSpanPerPixel( + uv_a, + uv_b, + [&label_pt_cloud, &match_pixel, &uv_a, &uv_b, &a, &b, &height](const int32_t& pixel, const Point2F& uv) + { + if ((pixel & match_pixel) != 0b0) + { + const auto param = (uv - uv_a).vSize() / (uv_b - uv_a).vSize(); + label_pt_cloud.emplace_back(a + param * (b - a), height); + } + }); + } + } + } + } + + slicer_layer.clearSegments(); } + meshStorage.setIdFieldInfo(label_pt_cloud); + delete slicerList[meshIdx]; Progress::messageProgress(Progress::Stage::PARTS, meshIdx + 1, slicerList.size()); diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index dcaff54d00..b6b43e51ed 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -29,6 +29,7 @@ #include "plugins/slots.h" #include "raft.h" // getTotalExtraLayers #include "range/v3/view/chunk_by.hpp" +#include "range/v3/view/enumerate.hpp" #include "settings/types/Ratio.h" #include "sliceDataStorage.h" #include "utils/Simplify.h" @@ -2472,9 +2473,10 @@ void LayerPlan::writeExtrusionRelativeZ( const coord_t path_z_offset, double extrusion_mm3_per_mm, PrintFeatureType feature, + const std::optional& inline_comment, bool update_extrusion_offset) { - gcode.writeExtrusion(position + Point3LL(0, 0, z_ + path_z_offset), speed, extrusion_mm3_per_mm, feature, update_extrusion_offset); + gcode.writeExtrusion(position + Point3LL(0, 0, z_ + path_z_offset), speed, extrusion_mm3_per_mm, feature, inline_comment, update_extrusion_offset); } void LayerPlan::addLinesMonotonic( @@ -2974,6 +2976,16 @@ void LayerPlan::processFanSpeedAndMinimalLayerTime(Point2LL starting_position) last_extruder_plan.processFanSpeedForMinimalLayerTime(maximum_cool_min_layer_time, other_extr_plan_time); } +std::optional getIdLabelUvComment(const std::optional>& idlabel_uvs, const size_t idx) +{ + if (! idlabel_uvs.has_value()) + { + return std::nullopt; + } + const auto uv_pt = idlabel_uvs.value()[idx]; + return (std::isnan(uv_pt.x_) || std::isnan(uv_pt.y_)) ? std::nullopt : std::make_optional(fmt::format("UV: {0:.4f} {1:.4f}", uv_pt.x_, uv_pt.y_)); +} + void LayerPlan::writeGCode(GCodeExport& gcode) { auto communication = Application::getInstance().communication_; @@ -3376,13 +3388,22 @@ void LayerPlan::writeGCode(GCodeExport& gcode) if (! coasting) // not same as 'else', cause we might have changed [coasting] in the line above... { // normal path to gcode algorithm Point3LL prev_point = gcode.getPosition(); - for (const auto& pt : path.points) + for (const auto& [idx, pt] : path.points | ranges::views::enumerate) { const auto [_, time] = extruder_plan.getPointToPointTime(prev_point, pt, path); insertTempOnTime(time, path_idx); const double extrude_speed = speed * path.speed_back_pressure_factor; - writeExtrusionRelativeZ(gcode, pt, extrude_speed, path.z_offset, path.getExtrusionMM3perMM(), path.config.type, update_extrusion_offset); + + writeExtrusionRelativeZ( + gcode, + pt, + extrude_speed, + path.z_offset, + path.getExtrusionMM3perMM(), + path.config.type, + getIdLabelUvComment(path.idlabel_uv_per_point, idx), + update_extrusion_offset); sendLineTo(path, pt, extrude_speed); prev_point = pt; @@ -3424,6 +3445,7 @@ void LayerPlan::writeGCode(GCodeExport& gcode) path.z_offset + z_offset, spiral_path.getExtrusionMM3perMM(), spiral_path.config.type, + std::nullopt, // FIXME?: ID should probably work for spiralize as well. update_extrusion_offset); sendLineTo(spiral_path, Point3LL(p1.x_, p1.y_, z_offset), extrude_speed, layer_thickness_); } @@ -3556,7 +3578,14 @@ bool LayerPlan::writePathWithCoasting( auto [_, time] = extruder_plan.getPointToPointTime(previous_position, path.points[point_idx], path); insertTempOnTime(time, path_idx); - writeExtrusionRelativeZ(gcode, path.points[point_idx], extrude_speed, path.z_offset, path.getExtrusionMM3perMM(), path.config.type); + writeExtrusionRelativeZ( + gcode, + path.points[point_idx], + extrude_speed, + path.z_offset, + path.getExtrusionMM3perMM(), + path.config.type, + getIdLabelUvComment(path.idlabel_uv_per_point, path_idx)); sendLineTo(path, path.points[point_idx], extrude_speed); previous_position = path.points[point_idx]; @@ -3682,6 +3711,15 @@ void LayerPlan::applyGradualFlow() } } +void LayerPlan::applyIdLabel(const Image& slice_id_texture) +{ + for (ExtruderPlan& extruder_plan : extruder_plans_) + { + // TODO: opt-out sure-to-be unaffected layers + extruder_plan.applyIdLabel(slice_id_texture, z_); + } +} + std::shared_ptr LayerPlan::findFirstPrintedMesh() const { for (const ExtruderPlan& extruder_plan : extruder_plans_) diff --git a/src/SlicedUVCoordinates.cpp b/src/SlicedUVCoordinates.cpp index f7137adc98..5c14835c8f 100644 --- a/src/SlicedUVCoordinates.cpp +++ b/src/SlicedUVCoordinates.cpp @@ -3,78 +3,120 @@ #include "SlicedUVCoordinates.h" +#include + #include "slicer.h" +#include "utils/linearAlg2D.h" namespace cura { SlicedUVCoordinates::SlicedUVCoordinates(const std::vector& segments) - : located_uv_coordinates_(cell_size) + : located_uv_coords_segs_(cell_size) { segments_.reserve(segments.size()); - for (const SlicerSegment& segment : segments) { if (segment.uv_start.has_value() && segment.uv_end.has_value()) { - located_uv_coordinates_.insert(segment.start, segment.uv_start.value()); - located_uv_coordinates_.insert(segment.end, segment.uv_end.value()); + segments_.emplace_back(segment.start, segment.end, segment.uv_start.value(), segment.uv_end.value()); + Segment* seg = &segments_.back(); + + located_uv_coords_segs_.insert(seg); - segments_.push_back(Segment{ segment.start, segment.end, segment.uv_start.value(), segment.uv_end.value() }); + segs_by_point_.insert({ seg->start, seg }); + segs_by_point_.insert({ seg->end, seg }); } } } std::optional SlicedUVCoordinates::getClosestUVCoordinates(const Point2LL& position) const { - // First try the quick method, which will work in 99% cases - SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem nearest_uv_coordinates; - if (located_uv_coordinates_.getNearest(position, search_radius, nearest_uv_coordinates)) + float closest_dist2 = std::numeric_limits::infinity(); + const Segment* res = nullptr; + float res_param = 0.0f; + const auto find_closest = [&position, &closest_dist2, &res, &res_param](Segment* const& seg) { - return nearest_uv_coordinates.val; - } + const auto on_line = LinearAlg2D::getClosestOnLineSegment(position, seg->start, seg->end); + const float err2 = vSize2(on_line - position); + if (err2 < closest_dist2) + { + closest_dist2 = err2; + res = seg; + res_param = vSize(on_line - seg->start) / static_cast(vSize(seg->end - seg->start)); + } + return true; // Don't stop searching at any point during this loop. + }; + located_uv_coords_segs_.processNearby(position, search_radius, find_closest); + return closest_dist2 < std::numeric_limits::infinity() ? std::make_optional(res->uv_start + (res->uv_end - res->uv_start) * res_param) : std::nullopt; +} + +std::optional> SlicedUVCoordinates::getUVCoordsLineSegment(const Point2LL& a, const Point2LL& b) const +{ + // TODO/FIXME: This is currently not optimized. + // (First tried to write the optimized version but it didn't work out... will have to try again later when more clearheaded.) + // _ALSO!_ this'll only work reliably for quite simple UV-meshes, so it really _needs_ to change. - // We couldn't find a close point with UV coordinates, so try to find the closest segment and project the point to it - double closest_distance = std::numeric_limits::max(); - std::optional closest_uv_coordinates; + typedef std::tuple seg_dist2_t; - for (const Segment& segment : segments_) + const Point2LL* ptr_pt; + std::vector* ptr_dist2_list; + const auto gather_segments = [&ptr_pt, &ptr_dist2_list](Segment* const& seg) { - const double segment_length = vSizef(segment.end - segment.start); - if (std::abs(segment_length) < 0.001) - { - continue; - } + const auto on_line = LinearAlg2D::getClosestOnLineSegment(*ptr_pt, seg->start, seg->end); + const float err2 = vSize2(on_line - *ptr_pt); + const float param = vSize(on_line - seg->start) / static_cast(vSize(seg->end - seg->start)); + ptr_dist2_list->emplace_back(seg, param < 0.5f, err2); + return true; + }; + const auto sort_by_dist2 = [](const seg_dist2_t& q, const seg_dist2_t& r) + { + return std::get<2>(q) < std::get<2>(r); + }; - const double dot_product = dot((position - segment.start), (segment.end - segment.start)) / segment_length; - double distance_to_segment; - double interpolate_factor; + std::vector closest_to_a; + ptr_pt = &a; + ptr_dist2_list = &closest_to_a; + located_uv_coords_segs_.processNearby(a, search_radius, gather_segments); + std::stable_sort(closest_to_a.begin(), closest_to_a.end(), sort_by_dist2); - if (dot_product > segment_length) - { - interpolate_factor = 1.0; - distance_to_segment = vSizef(position - segment.end); - } - else if (dot_product < 0.0) - { - interpolate_factor = 0.0; - distance_to_segment = vSizef(position - segment.start); - } - else - { - interpolate_factor = dot_product / segment_length; - const Point2LL projected_position = cura::lerp(segment.start, segment.end, interpolate_factor); - distance_to_segment = vSizef(position - projected_position); - } + std::vector closest_to_b; + ptr_pt = &b; + ptr_dist2_list = &closest_to_b; + located_uv_coords_segs_.processNearby(b, search_radius, gather_segments); + std::stable_sort(closest_to_b.begin(), closest_to_b.end(), sort_by_dist2); - if (distance_to_segment < closest_distance) + float closest_uv_dist2 = std::numeric_limits::infinity(); + std::pair pts; + + for (const auto& a_seg_info : closest_to_a) + { + for (const auto& b_seg_info : closest_to_b) { - closest_distance = distance_to_segment; - closest_uv_coordinates = cura::lerp(segment.uv_start, segment.uv_end, static_cast(interpolate_factor)); + Segment* a_seg = std::get<0>(a_seg_info); + Segment* b_seg = std::get<0>(b_seg_info); + + const bool a_side = std::get<1>(a_seg_info); + const bool b_side = std::get<1>(b_seg_info); + + const auto& a_uv = a_side ? a_seg->uv_start : a_seg->uv_end; + const auto& b_uv = b_side ? b_seg->uv_start : b_seg->uv_end; + // assert(a_uv != b_uv); + if (a_uv == b_uv) + { + return std::nullopt; + } + + const float uv_dist2 = (b_uv - a_uv).vSize2(); + if (uv_dist2 < closest_uv_dist2) + { + closest_uv_dist2 = uv_dist2; + pts = { a_uv, b_uv }; + } } } - return closest_uv_coordinates; + return closest_uv_dist2 < std::numeric_limits::infinity() ? std::make_optional(pts) : std::nullopt; } } // namespace cura diff --git a/src/TextureDataProvider.cpp b/src/TextureDataProvider.cpp index c2df61f485..43be98fc0f 100644 --- a/src/TextureDataProvider.cpp +++ b/src/TextureDataProvider.cpp @@ -51,4 +51,34 @@ std::optional TextureDataProvider::getAreaPreference(const Point2LL return std::nullopt; } +bool TextureDataProvider::getTexelsForSpan(const Point2LL& a, const Point2LL& b, const std::string& feature, std::vector& res) const +{ + auto data_mapping_iterator = texture_data_mapping_->find(feature); + if (data_mapping_iterator == texture_data_mapping_->end()) + { + return false; + } + + const TextureBitField& bit_field = data_mapping_iterator->second; + const std::optional> points_uv = uv_coordinates_->getUVCoordsLineSegment(a, b); + if (! points_uv.has_value()) + { + return false; + } + + bool has_any = false; + texture_->visitSpanPerPixel( + points_uv.value().first, + points_uv.value().second, + [&bit_field, &res, &has_any](const int32_t pixel_data, const Point2F& uv_pt) + { + res.push_back({ static_cast( + // FIXME: The method to shift left & right here is the same as above, should be an inline method? + (pixel_data << (32 - 1 - bit_field.bit_range_end_index)) >> (32 - 1 - (bit_field.bit_range_end_index - bit_field.bit_range_start_index))), + uv_pt }); + has_any |= (res.back().first == TextureArea::Preferred); + }); + return has_any; +} + } // namespace cura diff --git a/src/gcodeExport.cpp b/src/gcodeExport.cpp index efaf31d39e..3dbfb8fad6 100644 --- a/src/gcodeExport.cpp +++ b/src/gcodeExport.cpp @@ -925,9 +925,15 @@ void GCodeExport::writeTravel(const Point2LL& p, const Velocity& speed) { writeTravel(Point3LL(p.X, p.Y, current_layer_z_), speed); } -void GCodeExport::writeExtrusion(const Point2LL& p, const Velocity& speed, double extrusion_mm3_per_mm, PrintFeatureType feature, bool update_extrusion_offset) +void GCodeExport::writeExtrusion( + const Point2LL& p, + const Velocity& speed, + double extrusion_mm3_per_mm, + PrintFeatureType feature, + const std::optional& inline_comment, + bool update_extrusion_offset) { - writeExtrusion(Point3LL(p.X, p.Y, current_layer_z_), speed, extrusion_mm3_per_mm, feature, update_extrusion_offset); + writeExtrusion(Point3LL(p.X, p.Y, current_layer_z_), speed, extrusion_mm3_per_mm, feature, inline_comment, update_extrusion_offset); } void GCodeExport::writeTravel(const Point3LL& p, const Velocity& speed, const std::optional retract_distance) @@ -940,14 +946,20 @@ void GCodeExport::writeTravel(const Point3LL& p, const Velocity& speed, const st writeTravel(p.x_, p.y_, p.z_ + is_z_hopped_, speed, retract_distance); } -void GCodeExport::writeExtrusion(const Point3LL& p, const Velocity& speed, double extrusion_mm3_per_mm, PrintFeatureType feature, bool update_extrusion_offset) +void GCodeExport::writeExtrusion( + const Point3LL& p, + const Velocity& speed, + double extrusion_mm3_per_mm, + PrintFeatureType feature, + const std::optional& inline_comment, + bool update_extrusion_offset) { if (flavor_ == EGCodeFlavor::BFB) { writeMoveBFB(p.x_, p.y_, p.z_, speed, extrusion_mm3_per_mm, feature); return; } - writeExtrusion(p.x_, p.y_, p.z_, speed, extrusion_mm3_per_mm, feature, update_extrusion_offset); + writeExtrusion(p.x_, p.y_, p.z_, speed, extrusion_mm3_per_mm, feature, inline_comment, update_extrusion_offset); } void GCodeExport::writeMoveBFB(const int x, const int y, const int z, const Velocity& speed, double extrusion_mm3_per_mm, PrintFeatureType feature) @@ -1039,7 +1051,7 @@ void GCodeExport::writeTravel(const coord_t x, const coord_t y, const coord_t z, const PrintFeatureType travel_move_type = sendTravel(Point3LL(x, y, z), speed, extruder_attr, retraction_amounts); *output_stream_ << "G0"; - writeFXYZE(speed, x, y, z, current_e_value_, travel_move_type, retraction_amounts); + writeFXYZE(speed, x, y, z, current_e_value_, travel_move_type, std::nullopt, retraction_amounts); } void GCodeExport::writeExtrusion( @@ -1049,6 +1061,7 @@ void GCodeExport::writeExtrusion( const Velocity& speed, const double extrusion_mm3_per_mm, const PrintFeatureType& feature, + const std::optional& inline_comment, const bool update_extrusion_offset) { if (current_position_.x_ == x && current_position_.y_ == y && current_position_.z_ == z) @@ -1117,7 +1130,7 @@ void GCodeExport::writeExtrusion( const double new_e_value = current_e_value_ + extrusion_per_mm * diff_length; *output_stream_ << "G1"; - writeFXYZE(speed, x, y, z, new_e_value, feature); + writeFXYZE(speed, x, y, z, new_e_value, feature, inline_comment); } void GCodeExport::writeFXYZE( @@ -1127,6 +1140,7 @@ void GCodeExport::writeFXYZE( const coord_t z, const double e, const PrintFeatureType& feature, + const std::optional& inline_comment, const std::optional& retraction_amounts) { if (current_speed_ != speed) @@ -1155,6 +1169,10 @@ void GCodeExport::writeFXYZE( { const double output_e = (relative_extrusion_) ? e + current_e_offset_ - current_e_value_ : e + current_e_offset_; *output_stream_ << " " << extruder_attr_[current_extruder_].extruder_character_ << PrecisionedDouble{ 5, output_e }; + if (inline_comment.has_value()) + { + *output_stream_ << " ;" << inline_comment.value(); + } current_e_value_ = e; } *output_stream_ << new_line_; diff --git a/src/mesh.cpp b/src/mesh.cpp index 9e60d45454..9dfb484610 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -12,6 +12,38 @@ namespace cura { +void Image::visitSpanPerPixel(const Point2F& a, const Point2F& b, const std::function& func) const +{ + constexpr auto func_major_stepper = [](const int64_t& da, const int64_t& abs_db, const int64_t& i) + { + return da < 0 ? -i : i; + }; + constexpr auto func_minor_stepper = [](const int64_t& da, const int64_t& abs_db, const int64_t& i) + { + return (i * da) / abs_db; + }; + + const auto x0 = static_cast(a.x_ * width_); + const auto y0 = static_cast(a.y_ * height_); + const auto x1 = static_cast(b.x_ * width_); + const auto y1 = static_cast(b.y_ * height_); + + const auto dx = x1 - x0; + const auto dy = y1 - y0; + const auto abs_dx = std::llabs(dx); + const auto abs_dy = std::llabs(dy); + const auto max_span = std::max(abs_dx, abs_dy); + + const auto func_x = abs_dy <= abs_dx ? func_major_stepper : func_minor_stepper; + const auto func_y = abs_dx <= abs_dy ? func_major_stepper : func_minor_stepper; + for (size_t i_pix = 0; i_pix < max_span; ++i_pix) + { + const auto xi = x0 + func_x(dx, abs_dy, i_pix); + const auto yi = y0 + func_y(dy, abs_dx, i_pix); + func(getPixel(xi, yi), Point2F(static_cast(xi) / width_, static_cast(yi) / height_)); + } +} + const int vertex_meld_distance = MM2INT(0.03); /*! * returns a hash for the location, but first divides by the vertex_meld_distance, diff --git a/src/sliceDataStorage.cpp b/src/sliceDataStorage.cpp index d26e2aaa8a..1c8c0b7fa7 100644 --- a/src/sliceDataStorage.cpp +++ b/src/sliceDataStorage.cpp @@ -97,6 +97,7 @@ void SliceLayer::getOutlines(Shape& result, bool external_polys_only) const SliceMeshStorage::SliceMeshStorage(Mesh* mesh, const size_t slice_layer_count) : settings(mesh->settings_) , mesh_name(mesh->mesh_name_) + , id_field_info(std::nullopt) , layer_nr_max_filled_layer(0) , bounding_box(mesh->getAABB()) , base_subdiv_cube(nullptr) @@ -106,6 +107,10 @@ SliceMeshStorage::SliceMeshStorage(Mesh* mesh, const size_t slice_layer_count) layers.resize(slice_layer_count); } +void SliceMeshStorage::setIdFieldInfo(const std::vector& label_pt_cloud) +{ + id_field_info = IdFieldInfo::fromPointCloud(label_pt_cloud); +} bool SliceMeshStorage::getExtruderIsUsed(const size_t extruder_nr) const { diff --git a/src/slicer.cpp b/src/slicer.cpp index 6151cf15e2..a1a4f03486 100644 --- a/src/slicer.cpp +++ b/src/slicer.cpp @@ -799,7 +799,10 @@ void SlicerLayer::makePolygons(const Mesh* mesh) open_polylines_.removeDegenerateVerts(); sliced_uv_coordinates_ = std::make_shared(segments_); +} +void SlicerLayer::clearSegments() +{ // Clear the segment list to save memory, it is no longer needed after this point. segments_.clear(); } diff --git a/src/utils/AABB3D.cpp b/src/utils/AABB3D.cpp index 698c4745b7..fb22b55f41 100644 --- a/src/utils/AABB3D.cpp +++ b/src/utils/AABB3D.cpp @@ -42,6 +42,11 @@ bool AABB3D::hit(const AABB3D& other) const return true; } +bool AABB3D::exists() const +{ + return min_.x_ <= max_.x_ && min_.y_ <= max_.y_ && min_.z_ <= max_.z_; +} + AABB3D AABB3D::include(Point3LL p) { min_.x_ = std::min(min_.x_, p.x_); diff --git a/src/utils/IdFieldInfo.cpp b/src/utils/IdFieldInfo.cpp new file mode 100644 index 0000000000..7ab548c174 --- /dev/null +++ b/src/utils/IdFieldInfo.cpp @@ -0,0 +1,118 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#include "utils/IDFieldInfo.h" + +#include +#include +#include + +#include +#include +#include + +#include "utils/AABB3D.h" + +using namespace cura; + +double proj(const Point3D& b, const Point3LL& p) +{ + return (Point3D(p) * b) / (b * b); +} + +// FIXME: something doesn't quite work right yet -- the normal is OK (I think?) but the primary and secondary axii don't do what I want yet... + +std::optional IdFieldInfo::fromPointCloud(const std::vector& points) +{ + if (points.size() < 3) + { + return std::nullopt; + } + + // instead of doing full PCA (which is more efficient probably), for now just try to find the 'flattest' of 13 orientations (full grid: 9 * 3 = 27, then: (27 - 1) / 2 = 13 to + // get rid of the origin and mirrored directions) + constexpr double D2 = 0.70710678118654752440084436210485; + constexpr double D3 = 0.57735026918962576450914878050196; + static std::vector normal_candidates = std::vector({ + Point3D(0, 0, 1), + Point3D(0, 1, 0), + Point3D(1, 0, 0), + Point3D(0, D2, D2), + Point3D(0, D2, -D2), + Point3D(D2, 0, D2), + Point3D(D2, 0, -D2), + Point3D(D2, D2, 0), + Point3D(D2, -D2, 0), + Point3D(D3, D3, D3), + Point3D(D3, D3, -D3), + Point3D(D3, -D3, D3), + Point3D(D3, -D3, -D3), + }); + + std::unordered_map remapped_spans; + for (const auto& [idx, norm] : normal_candidates | ranges::views::enumerate) + { + auto& [remapped_min, remapped_max] = (remapped_spans[idx] = { std::numeric_limits::infinity(), -std::numeric_limits::infinity() }); + for (const auto& pt : points) + { + const auto val = proj(norm, pt); + remapped_min = std::min(remapped_min, val); + remapped_max = std::max(remapped_max, val); + } + } + + auto span_sizes = remapped_spans + | ranges::view::transform( + [](const auto& key_value) + { + return std::make_pair(key_value.second.second - key_value.second.first, key_value.first); + }) + | ranges::to>>(); + std::stable_sort( + span_sizes.begin(), + span_sizes.end(), + [](const auto& a, const auto& b) + { + return a.first < b.first; + }); + + const auto& normal = normal_candidates[span_sizes.front().second]; + const auto& secondary = std::find_if( + normal_candidates.begin(), + normal_candidates.end(), + [&normal](const auto& ax) + { + return normal.cross(ax).vSize() > CLOSE_1D; + }); + if (secondary == normal_candidates.end() || *secondary == normal) + { + return std::nullopt; + } + const auto& primary = std::find_if( + normal_candidates.begin(), + normal_candidates.end(), + [&normal, &secondary](const auto& ax) + { + return (*secondary).cross(ax).vSize() > CLOSE_1D && normal.cross(ax).vSize() > CLOSE_1D; + }); + if (primary == normal_candidates.end() || *primary == normal || primary == secondary) + { + return std::nullopt; + } + const int primary_idx = std::distance(normal_candidates.begin(), primary); + const int secondary_idx = std::distance(normal_candidates.begin(), secondary); + return std::make_optional(IdFieldInfo{ .primary_axis_ = *primary, + .primary_span_ = remapped_spans[primary_idx], + .secondary_axis_ = *secondary, + .secondary_span_ = remapped_spans[secondary_idx], + .normal_ = normal }); +} + +Point2F IdFieldInfo::worldPointToLabelUv(const Point3LL& pt) const +{ + // TODO: Get the '''furthest''' points in the '''diagonal''' directions of the chosen primary and secondary axii, should approximate well enough to 'inscribed' bounding rect. + + return Point2F( + 1.0f - (proj(primary_axis_, pt) - primary_span_.first) / (primary_span_.second - primary_span_.first), + 1.0f - (proj(secondary_axis_, pt) - secondary_span_.first) / (secondary_span_.second - secondary_span_.first)); +} diff --git a/src/utils/LabelMaker.cpp b/src/utils/LabelMaker.cpp new file mode 100644 index 0000000000..0d6cb6480a --- /dev/null +++ b/src/utils/LabelMaker.cpp @@ -0,0 +1,246 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#include "utils/LabelMaker.h" + +#include + +#include + +// TODO: put 'font8x8' into config file instead +constexpr std::array font8x8 = { + R"( +..####.. +.######. +##...### +##..#.## +##.#..## +###...## +.######. +..####.. +)", + R"( +..####.. +.#####.. +....##.. +....##.. +....##.. +....##.. +.######. +.######. +)", + R"( +.######. +######## +##....## +.....##. +....##.. +..###... +.####### +######## +)", + R"( +.######. +######## +##.. ### +...###.. +..####.. +.... ### +######## +.######. +)", + R"( +....###. +...##### +..##..## +.##...## +######## +.####### +......## +......## +)", + R"( +.####### +######## +##...... +###..... +.######. +......## +.####### +######.. +)", + R"( +..###### +.####### +##...... +######.. +#######. +##....## +######## +.#####.. +)", + R"( +#######. +######## +......## +.....##. +....##.. +...##... +####.... +####.... +)", + R"( +..####.. +.######. +##....## +.######. +.######. +##....## +.######. +..####.. +)", + R"( +..#####. +######## +##....## +.####### +..###### +......## +#######. +######.. +)", + R"( +...##... +..####.. +.######. +##....## +######## +######## +##....## +##....## +)", + R"( +######.. +######## +##....## +#######. +#######. +##....## +######## +######.. +)", + R"( +..###### +######## +##...... +##...... +##...... +##...... +######## +..###### +)", + R"( +######.. +######## +##....## +##....## +##....## +##....## +######## +######.. +)", + R"( +.####### +######## +##...... +######## +######## +##...... +######## +.####### +)", + R"( +.####### +######## +##...... +######## +######## +##...... +##...... +##...... +)", +}; + +std::vector> makeFont(const std::array& font) +{ + std::vector> res; + for (const std::string_view& glyph : font) + { + res.emplace_back(); + for (const char c : glyph) + { + switch (c) + { + case '.': + res.back().push_back(false); + break; + case '#': + res.back().push_back(true); + break; + } + } + } + return res; +} +const auto proc_font = makeFont(font8x8); + +void cura::paintStringToBuffer(const std::string_view& str, const size_t buffer_width, const size_t buffer_height, std::vector& buffer) +{ + constexpr size_t base_offset_x = 3; + size_t offset_x = base_offset_x; + size_t offset_y = 3; + for (char cs : str) + { + int c = cs; + int offset = 0; + if (c >= 'a' && c <= 'z') + { + offset = 'a' - 0xA; + } + else if (c >= 'A' && c <= 'Z') + { + offset = 'A' - 0xA; + } + else if (c >= '0' && c <= '9') + { + offset = '0'; + } + else + { + c = -1; + } + c -= offset; + + if (c >= 0 && c < proc_font.size()) + { + const auto& glyph = proc_font[c]; + for (const auto& [idx, pix] : glyph | ranges::views::enumerate) + { + const size_t x = offset_x + idx % 8; + const size_t y = offset_y + idx / 8; + buffer[y * buffer_width + x] = pix; + } + } + + offset_x += 10; + if (offset_x + 1 >= buffer_width) + { + offset_x = base_offset_x; + offset_y += 10; + } + if (offset_y + 1 >= buffer_height) + { + return; + } + } +}