mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-19 12:47:50 -06:00
Merge branch 'main' into feature/multitool
This commit is contained in:
commit
5e2f145c34
210 changed files with 2326 additions and 1605 deletions
114
src/libslic3r/GCode/AdaptivePAInterpolator.cpp
Normal file
114
src/libslic3r/GCode/AdaptivePAInterpolator.cpp
Normal file
|
@ -0,0 +1,114 @@
|
|||
// AdaptivePAInterpolator.cpp
|
||||
// OrcaSlicer
|
||||
//
|
||||
// Implementation file for the AdaptivePAInterpolator class, providing methods to parse data and perform PA interpolation.
|
||||
|
||||
#include "AdaptivePAInterpolator.hpp"
|
||||
#include <stdexcept>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
/**
|
||||
* @brief Parses the input data and sets up the interpolators.
|
||||
* @param data A string containing the data in CSV format (PA, flow rate, acceleration).
|
||||
* @return 0 on success, -1 on error.
|
||||
*/
|
||||
int AdaptivePAInterpolator::parseAndSetData(const std::string& data) {
|
||||
flow_interpolators_.clear();
|
||||
accelerations_.clear();
|
||||
|
||||
try {
|
||||
std::istringstream ss(data);
|
||||
std::string line;
|
||||
std::map<double, std::vector<std::pair<double, double>>> acc_to_flow_pa;
|
||||
|
||||
while (std::getline(ss, line)) {
|
||||
std::istringstream lineStream(line);
|
||||
std::string value;
|
||||
double paValue, flowRate, acceleration;
|
||||
paValue = flowRate = acceleration = 0.f; // initialize all to zero.
|
||||
|
||||
// Parse PA value
|
||||
if (std::getline(lineStream, value, ',')) {
|
||||
paValue = std::stod(value);
|
||||
}
|
||||
|
||||
// Parse flow rate value
|
||||
if (std::getline(lineStream, value, ',')) {
|
||||
flowRate = std::stod(value);
|
||||
}
|
||||
|
||||
// Parse acceleration value
|
||||
if (std::getline(lineStream, value, ',')) {
|
||||
acceleration = std::stod(value);
|
||||
}
|
||||
|
||||
// Store the parsed values in a map with acceleration as the key
|
||||
acc_to_flow_pa[acceleration].emplace_back(flowRate, paValue);
|
||||
}
|
||||
|
||||
// Iterate through the map to set up the interpolators
|
||||
for (const auto& kv : acc_to_flow_pa) {
|
||||
double acceleration = kv.first;
|
||||
const auto& data = kv.second;
|
||||
|
||||
std::vector<double> flowRates;
|
||||
std::vector<double> paValues;
|
||||
|
||||
for (const auto& pair : data) {
|
||||
flowRates.push_back(pair.first);
|
||||
paValues.push_back(pair.second);
|
||||
}
|
||||
|
||||
// Only set up the interpolator if there are enough data points
|
||||
if (flowRates.size() > 1) {
|
||||
PchipInterpolatorHelper interpolator(flowRates, paValues);
|
||||
flow_interpolators_[acceleration] = interpolator;
|
||||
accelerations_.push_back(acceleration);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
m_isInitialised = false;
|
||||
return -1; // Error: Exception during parsing
|
||||
}
|
||||
m_isInitialised = true;
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Interpolates the PA value for the given flow rate and acceleration.
|
||||
* @param flow_rate The flow rate at which to interpolate.
|
||||
* @param acceleration The acceleration at which to interpolate.
|
||||
* @return The interpolated PA value, or -1 if interpolation fails.
|
||||
*/
|
||||
double AdaptivePAInterpolator::operator()(double flow_rate, double acceleration) {
|
||||
std::vector<double> pa_values;
|
||||
std::vector<double> acc_values;
|
||||
|
||||
// Estimate PA value for every flow to PA model for the given flow rate
|
||||
for (const auto& kv : flow_interpolators_) {
|
||||
double pa_value = kv.second.interpolate(flow_rate);
|
||||
|
||||
// Check if the interpolated PA value is valid
|
||||
if (pa_value != -1) {
|
||||
pa_values.push_back(pa_value);
|
||||
acc_values.push_back(kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are enough acceleration values for interpolation
|
||||
if (acc_values.size() < 2) {
|
||||
// Special case: Only one acceleration value
|
||||
if (acc_values.size() == 1) {
|
||||
return std::round(pa_values[0] * 1000.0) / 1000.0; // Rounded to 3 decimal places
|
||||
}
|
||||
return -1; // Error: Not enough data points for interpolation
|
||||
}
|
||||
|
||||
// Create a new PchipInterpolatorHelper for PA-acceleration interpolation
|
||||
// Use the estimated PA values from the for loop above and their corresponding accelerations to
|
||||
// generate the new PCHIP model. Then run this model to interpolate the PA value for the given acceleration value.
|
||||
PchipInterpolatorHelper pa_accel_interpolator(acc_values, pa_values);
|
||||
return std::round(pa_accel_interpolator.interpolate(acceleration) * 1000.0) / 1000.0; // Rounded to 3 decimal places
|
||||
}
|
54
src/libslic3r/GCode/AdaptivePAInterpolator.hpp
Normal file
54
src/libslic3r/GCode/AdaptivePAInterpolator.hpp
Normal file
|
@ -0,0 +1,54 @@
|
|||
// AdaptivePAInterpolator.hpp
|
||||
// OrcaSlicer
|
||||
//
|
||||
// Header file for the AdaptivePAInterpolator class, responsible for interpolating pressure advance (PA) values based on flow rate and acceleration using PCHIP interpolation.
|
||||
|
||||
#ifndef ADAPTIVEPAINTERPOLATOR_HPP
|
||||
#define ADAPTIVEPAINTERPOLATOR_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include "PchipInterpolatorHelper.hpp"
|
||||
|
||||
/**
|
||||
* @class AdaptivePAInterpolator
|
||||
* @brief A class to interpolate pressure advance (PA) values based on flow rate and acceleration using Piecewise Cubic Hermite Interpolating Polynomial (PCHIP) interpolation.
|
||||
*/
|
||||
class AdaptivePAInterpolator {
|
||||
public:
|
||||
/**
|
||||
* @brief Default constructor.
|
||||
*/
|
||||
AdaptivePAInterpolator() : m_isInitialised(false) {}
|
||||
|
||||
/**
|
||||
* @brief Parses the input data and sets up the interpolators.
|
||||
* @param data A string containing the data in CSV format (PA, flow rate, acceleration).
|
||||
* @return 0 on success, -1 on error.
|
||||
*/
|
||||
int parseAndSetData(const std::string& data);
|
||||
|
||||
/**
|
||||
* @brief Interpolates the PA value for the given flow rate and acceleration.
|
||||
* @param flow_rate The flow rate at which to interpolate.
|
||||
* @param acceleration The acceleration at which to interpolate.
|
||||
* @return The interpolated PA value, or -1 if interpolation fails.
|
||||
*/
|
||||
double operator()(double flow_rate, double acceleration);
|
||||
|
||||
/**
|
||||
* @brief Returns the initialization status.
|
||||
* @return The value of m_isInitialised.
|
||||
*/
|
||||
bool isInitialised() const {
|
||||
return m_isInitialised;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<double, PchipInterpolatorHelper> flow_interpolators_; ///< Map each acceleration to a flow-rate-to-PA interpolator.
|
||||
std::vector<double> accelerations_; ///< Store unique accelerations.
|
||||
bool m_isInitialised;
|
||||
};
|
||||
|
||||
#endif // ADAPTIVEPAINTERPOLATOR_HPP
|
285
src/libslic3r/GCode/AdaptivePAProcessor.cpp
Normal file
285
src/libslic3r/GCode/AdaptivePAProcessor.cpp
Normal file
|
@ -0,0 +1,285 @@
|
|||
// AdaptivePAProcessor.cpp
|
||||
// OrcaSlicer
|
||||
//
|
||||
// Implementation of the AdaptivePAProcessor class, responsible for processing G-code layers with adaptive pressure advance.
|
||||
|
||||
#include "../GCode.hpp"
|
||||
#include "AdaptivePAProcessor.hpp"
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
/**
|
||||
* @brief Constructor for AdaptivePAProcessor.
|
||||
*
|
||||
* This constructor initializes the AdaptivePAProcessor with a reference to a GCode object.
|
||||
* It also initializes the configuration reference, pressure advance interpolation object,
|
||||
* and regular expression patterns used for processing the G-code.
|
||||
*
|
||||
* @param gcodegen A reference to the GCode object that generates the G-code.
|
||||
*/
|
||||
AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen, const std::vector<unsigned int> &tools_used)
|
||||
: m_gcodegen(gcodegen),
|
||||
m_config(gcodegen.config()),
|
||||
m_last_predicted_pa(0.0),
|
||||
m_max_next_feedrate(0.0),
|
||||
m_next_feedrate(0.0),
|
||||
m_current_feedrate(0.0),
|
||||
m_last_extruder_id(-1),
|
||||
m_pa_change_pattern(R"(; PA_CHANGE:T(\d+) MM3MM:([0-9]*\.[0-9]+) ACCEL:(\d+) BR:(\d+) RC:(\d+) OV:(\d+))"),
|
||||
m_g1_f_pattern(R"(G1 F([0-9]+))")
|
||||
{
|
||||
// Constructor body can be used for further initialization if necessary
|
||||
for (unsigned int tool : tools_used) {
|
||||
// Only enable model for the tool if both PA and adaptive PA options are enabled
|
||||
if(m_config.adaptive_pressure_advance.get_at(tool) && m_config.enable_pressure_advance.get_at(tool)){
|
||||
auto interpolator = std::make_unique<AdaptivePAInterpolator>();
|
||||
// Get calibration values from extruder
|
||||
std::string pa_calibration_values = m_config.adaptive_pressure_advance_model.get_at(tool);
|
||||
// Setup the model and store it in the tool-interpolation model map
|
||||
interpolator->parseAndSetData(pa_calibration_values);
|
||||
m_AdaptivePAInterpolators[tool] = std::move(interpolator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the interpolator for a specific tool ID
|
||||
AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int tool_id) {
|
||||
auto it = m_AdaptivePAInterpolators.find(tool_id);
|
||||
if (it != m_AdaptivePAInterpolators.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
return nullptr; // Handle the case where the tool_id is not found
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Processes a layer of G-code and applies adaptive pressure advance.
|
||||
*
|
||||
* This method processes the G-code for a single layer, identifying the appropriate
|
||||
* pressure advance settings and applying them based on the current state and configurations.
|
||||
*
|
||||
* @param gcode A string containing the G-code for the layer.
|
||||
* @return A string containing the processed G-code with adaptive pressure advance applied.
|
||||
*/
|
||||
std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
|
||||
std::istringstream stream(gcode);
|
||||
std::string line;
|
||||
std::ostringstream output;
|
||||
double mm3mm_value = 0.0;
|
||||
unsigned int accel_value = 0;
|
||||
std::string pa_change_line;
|
||||
bool wipe_command = false;
|
||||
|
||||
// Iterate through each line of the layer G-code
|
||||
while (std::getline(stream, line)) {
|
||||
|
||||
// If a wipe start command is found, ignore all speed changes till the wipe end part is found
|
||||
if (line.find("WIPE_START") != std::string::npos) {
|
||||
wipe_command = true;
|
||||
}
|
||||
|
||||
// Update current feed rate (this is preceding an extrude or wipe command only). Ignore any speed changes that are emitted during a wipe move.
|
||||
// Travel feedrate is output as part of a G1 X Y (Z) F command
|
||||
if ( (line.find("G1 F") == 0) && (!wipe_command) ) { // prune lines quickly before running pattern matching
|
||||
std::size_t pos = line.find('F');
|
||||
if (pos != std::string::npos){
|
||||
m_current_feedrate = std::stod(line.substr(pos + 1)) / 60.0; // Convert from mm/min to mm/s
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe end found, continue searching for current feed rate.
|
||||
if (line.find("WIPE_END") != std::string::npos) {
|
||||
wipe_command = false;
|
||||
}
|
||||
|
||||
// Reset next feedrate to zero enable searching for the first encountered
|
||||
// feedrate change command after the PA change tag.
|
||||
m_next_feedrate = 0;
|
||||
|
||||
// Check for PA_CHANGE pattern in the line
|
||||
// We will only find this pattern for extruders where adaptive PA is enabled.
|
||||
// If there is mixed extruders in the layer (i.e. with adaptive PA on and off
|
||||
// this will only update the extruders where the adaptive PA is enabled
|
||||
// as these are the only ones where the PA pattern is output
|
||||
// For a mixed extruder layer with both adaptive PA enabled and disabled when the new tool is selected
|
||||
// the PA for that material is set. As no tag below will be found for this extruder, the original PA is retained.
|
||||
if (line.find("; PA_CHANGE") == 0) { // prune lines quickly before running regex check as regex is more expensive to run
|
||||
if (std::regex_search(line, m_match, m_pa_change_pattern)) {
|
||||
int extruder_id = std::stoi(m_match[1].str());
|
||||
mm3mm_value = std::stod(m_match[2].str());
|
||||
accel_value = std::stod(m_match[3].str());
|
||||
int isBridge = std::stoi(m_match[4].str());
|
||||
int roleChange = std::stoi(m_match[5].str());
|
||||
int isOverhang = std::stoi(m_match[6].str());
|
||||
|
||||
// Check if the extruder ID has changed
|
||||
bool extruder_changed = (extruder_id != m_last_extruder_id);
|
||||
m_last_extruder_id = extruder_id;
|
||||
|
||||
// Save the PA_CHANGE line to output later after finding feedrate
|
||||
pa_change_line = line;
|
||||
|
||||
// Look ahead for feedrate before any line containing both G and E commands
|
||||
std::streampos current_pos = stream.tellg();
|
||||
std::string next_line;
|
||||
double temp_feed_rate = 0;
|
||||
bool extrude_move_found = false;
|
||||
int line_counter = 0;
|
||||
|
||||
// Carry on searching on the layer gcode lines to find the print speed
|
||||
// If a G1 Fxxxx pattern is found, the new speed is identified
|
||||
// Carry on searching for feedrates to find the maximum print speed
|
||||
// until a feature change pattern or a wipe command is detected
|
||||
while (std::getline(stream, next_line)) {
|
||||
line_counter++;
|
||||
// Found an extrude move, set extrude move found flag and move to the next line
|
||||
if ((!extrude_move_found) && next_line.find("G1 ") == 0 &&
|
||||
next_line.find('X') != std::string::npos &&
|
||||
next_line.find('Y') != std::string::npos &&
|
||||
next_line.find('E') != std::string::npos) {
|
||||
// Pattern matched, break the loop
|
||||
extrude_move_found = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a travel move after we've found at least one extrude move
|
||||
// We now need to stop searching for speeds as we're done printing this island
|
||||
if (next_line.find("G1 ") == 0 &&
|
||||
next_line.find('X') != std::string::npos && // X is present
|
||||
next_line.find('Y') != std::string::npos && // Y is present
|
||||
next_line.find('E') == std::string::npos && // no "E" present
|
||||
extrude_move_found) { // An extrude move has happened already
|
||||
// First travel move after extrude move found. Stop searching
|
||||
break;
|
||||
}
|
||||
|
||||
// Found a WIPE command
|
||||
// If we have a wipe command, usually the wipe speed is different (larger) than the max print speed
|
||||
// for that feature. So stop searching if a wipe command is found because we do not want to overwrite the
|
||||
// speed used for PA calculation by the Wipe speed.
|
||||
if (next_line.find("WIPE") != std::string::npos) {
|
||||
break; // Stop searching if wipe command is found
|
||||
}
|
||||
|
||||
// Found another PA_CHANGE pattern
|
||||
// If RC = 1, it means we have a role change, so stop trying to find the max speed for the feature.
|
||||
// This is possibly redundant as a new feature would always have a travel move preceding it
|
||||
// but check anyway. However check last so to not invoke it without reason...
|
||||
if (next_line.find("; PA_CHANGE") == 0) { // prune lines quickly before running pattern matching
|
||||
std::size_t rc_pos = next_line.rfind("RC:");
|
||||
if (rc_pos != std::string::npos) {
|
||||
int rc_value = std::stoi(next_line.substr(rc_pos + 3));
|
||||
if (rc_value == 1) {
|
||||
break; // Role change found, stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Found a Feedrate change command
|
||||
// If the new feedrate is greater than any feedrate encountered so far after the PA change command, use that to calculate the PA value
|
||||
// Also if this is the first feedrate we encounter, store it as the next feedrate.
|
||||
if (next_line.find("G1 F") == 0) { // prune lines quickly before running pattern matching
|
||||
std::size_t pos = next_line.find('F');
|
||||
if (pos != std::string::npos) {
|
||||
double feedrate = std::stod(next_line.substr(pos + 1)) / 60.0; // Convert from mm/min to mm/s
|
||||
if(line_counter==1){ // this is the first command after the PA change pattern, and hence before any extrusion has happened. Reset
|
||||
// the current speed to this one
|
||||
m_current_feedrate = feedrate;
|
||||
}
|
||||
if (temp_feed_rate < feedrate) {
|
||||
temp_feed_rate = feedrate;
|
||||
}
|
||||
if(m_next_feedrate < EPSILON){ // This the first feedrate found after the PA Change command
|
||||
m_next_feedrate = feedrate;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a new maximum feedrate after the PA change command, use it
|
||||
if (temp_feed_rate > 0) {
|
||||
m_max_next_feedrate = temp_feed_rate;
|
||||
} else // If we didnt find a new feedrate at all after the PA change command, use the current feedrate.
|
||||
m_max_next_feedrate = m_current_feedrate;
|
||||
|
||||
// Restore stream position
|
||||
stream.clear();
|
||||
stream.seekg(current_pos);
|
||||
|
||||
// Calculate the predicted PA using the upcomming feature maximum feedrate
|
||||
// Get the interpolator for the active tool
|
||||
AdaptivePAInterpolator* interpolator = getInterpolator(m_last_extruder_id);
|
||||
|
||||
double predicted_pa = 0;
|
||||
double adaptive_PA_speed = 0;
|
||||
|
||||
if(!interpolator){ // Tool not found in the interpolator map
|
||||
// Tool not found in the PA interpolator to tool map
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0;
|
||||
if(m_config.gcode_comments) output << "; APA: Tool doesnt have APA enabled\n";
|
||||
} else if (!interpolator->isInitialised() || (!m_config.adaptive_pressure_advance.get_at(m_last_extruder_id)) )
|
||||
// Check if the model is not initialised by the constructor for the active extruder
|
||||
// Also check that adaptive PA is enabled for that extruder. This should not be needed
|
||||
// as the PA change flag should not be set upstream (in the GCode.cpp file) if adaptive PA is disabled
|
||||
// however check for robustness sake.
|
||||
{
|
||||
// Model failed or adaptive pressure advance not enabled - use default value from m_config
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0;
|
||||
if(m_config.gcode_comments) output << "; APA: Interpolator setup failed, using default pressure advance\n";
|
||||
} else { // Model setup succeeded
|
||||
// Proceed to identify the print speed to use to calculate the adaptive PA value
|
||||
if(isOverhang > 0){ // If we are in an overhang area, use the minimum between current print speed
|
||||
// and any speed immediately after
|
||||
// In most cases the current speed is the minimum one;
|
||||
// however if slowdown for layer cooling is enabled, the overhang
|
||||
// may be slowed down more than the current speed.
|
||||
adaptive_PA_speed = (m_current_feedrate == 0 || m_next_feedrate == 0) ?
|
||||
std::max(m_current_feedrate, m_next_feedrate) :
|
||||
std::min(m_current_feedrate, m_next_feedrate);
|
||||
}else{ // If this is not an overhang area, use the maximum speed from the current and
|
||||
// upcomming speeds for the island.
|
||||
adaptive_PA_speed = std::max(m_max_next_feedrate,m_current_feedrate);
|
||||
}
|
||||
|
||||
// Calculate the adaptive PA value
|
||||
predicted_pa = (*interpolator)(mm3mm_value * adaptive_PA_speed, accel_value);
|
||||
|
||||
// This is a bridge, use the dedicated PA setting.
|
||||
if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id) > EPSILON)
|
||||
predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id);
|
||||
|
||||
if (predicted_pa < 0) { // If extrapolation fails, fall back to the default PA for the extruder.
|
||||
predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0;
|
||||
if(m_config.gcode_comments) output << "; APA: Interpolation failed, using fallback pressure advance value\n";
|
||||
}
|
||||
}
|
||||
if(m_config.gcode_comments) {
|
||||
// Output debug GCode comments
|
||||
output << pa_change_line << '\n'; // Output PA change command tag
|
||||
if(isBridge && m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id) > EPSILON)
|
||||
output << "; APA Model Override (bridge)\n";
|
||||
output << "; APA Current Speed: " << std::to_string(m_current_feedrate) << "\n";
|
||||
output << "; APA Next Speed: " << std::to_string(m_next_feedrate) << "\n";
|
||||
output << "; APA Max Next Speed: " << std::to_string(m_max_next_feedrate) << "\n";
|
||||
output << "; APA Speed Used: " << std::to_string(adaptive_PA_speed) << "\n";
|
||||
output << "; APA Flow rate: " << std::to_string(mm3mm_value * m_max_next_feedrate) << "\n";
|
||||
output << "; APA Prev PA: " << std::to_string(m_last_predicted_pa) << " New PA: " << std::to_string(predicted_pa) << "\n";
|
||||
}
|
||||
if (extruder_changed || std::fabs(predicted_pa - m_last_predicted_pa) > EPSILON) {
|
||||
output << m_gcodegen.writer().set_pressure_advance(predicted_pa); // Use m_writer to set pressure advance
|
||||
m_last_predicted_pa = predicted_pa; // Update the last predicted PA value
|
||||
}
|
||||
}
|
||||
}else {
|
||||
// Output the current line as this isn't a PA change tag
|
||||
output << line << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return output.str();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
85
src/libslic3r/GCode/AdaptivePAProcessor.hpp
Normal file
85
src/libslic3r/GCode/AdaptivePAProcessor.hpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
// AdaptivePAProcessor.hpp
|
||||
// OrcaSlicer
|
||||
//
|
||||
// Header file for the AdaptivePAProcessor class, responsible for processing G-code layers for the purposes of applying adaptive pressure advance.
|
||||
|
||||
#ifndef ADAPTIVEPAPROCESSOR_H
|
||||
#define ADAPTIVEPAPROCESSOR_H
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "AdaptivePAInterpolator.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Forward declaration of GCode class
|
||||
class GCode;
|
||||
|
||||
/**
|
||||
* @brief Class for processing G-code layers with adaptive pressure advance.
|
||||
*/
|
||||
class AdaptivePAProcessor {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor for AdaptivePAProcessor.
|
||||
*
|
||||
* This constructor initializes the AdaptivePAProcessor with a reference to a GCode object.
|
||||
* It also initializes the configuration reference, pressure advance interpolation object,
|
||||
* and regular expression patterns used for processing the G-code.
|
||||
*
|
||||
* @param gcodegen A reference to the GCode object that generates the G-code.
|
||||
*/
|
||||
AdaptivePAProcessor(GCode &gcodegen, const std::vector<unsigned int> &tools_used);
|
||||
|
||||
/**
|
||||
* @brief Processes a layer of G-code and applies adaptive pressure advance.
|
||||
*
|
||||
* This method processes the G-code for a single layer, identifying the appropriate
|
||||
* pressure advance settings and applying them based on the current state and configurations.
|
||||
*
|
||||
* @param gcode A string containing the G-code for the layer.
|
||||
* @return A string containing the processed G-code with adaptive pressure advance applied.
|
||||
*/
|
||||
std::string process_layer(std::string &&gcode);
|
||||
|
||||
/**
|
||||
* @brief Manually sets adaptive PA internal value.
|
||||
*
|
||||
* This method manually sets the adaptive PA internally held value.
|
||||
* Call this when changing tools or in any other case where the internally assumed last PA value may be incorrect
|
||||
*/
|
||||
void resetPreviousPA(double PA){ m_last_predicted_pa = PA; };
|
||||
|
||||
private:
|
||||
GCode &m_gcodegen; ///< Reference to the GCode object.
|
||||
std::unordered_map<unsigned int, std::unique_ptr<AdaptivePAInterpolator>> m_AdaptivePAInterpolators; ///< Map between Interpolator objects and tool ID's
|
||||
const PrintConfig &m_config; ///< Reference to the print configuration.
|
||||
double m_last_predicted_pa; ///< Last predicted pressure advance value.
|
||||
double m_max_next_feedrate; ///< Maximum feed rate (speed) for the upcomming island. If no speed is found, the previous island speed is used.
|
||||
double m_next_feedrate; ///< First feed rate (speed) for the upcomming island.
|
||||
double m_current_feedrate; ///< Current, latest feedrate.
|
||||
int m_last_extruder_id; ///< Last used extruder ID.
|
||||
|
||||
std::regex m_pa_change_pattern; ///< Regular expression to detect PA_CHANGE pattern.
|
||||
std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern.
|
||||
std::smatch m_match; ///< Match results for regular expressions.
|
||||
|
||||
/**
|
||||
* @brief Get the PA interpolator attached to the specified tool ID.
|
||||
*
|
||||
* This method manually sets the adaptive PA internally held value.
|
||||
* Call this when changing tools or in any other case where the internally assumed last PA value may be incorrect
|
||||
*
|
||||
* @param An integer with the tool ID for which the PA interpolation model is to be returned.
|
||||
* @return The Adaptive PA Interpolator object corresponding to that tool.
|
||||
*/
|
||||
AdaptivePAInterpolator* getInterpolator(unsigned int tool_id);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // ADAPTIVEPAPROCESSOR_H
|
|
@ -28,7 +28,6 @@ inline Grids line_rasterization(const Line &line, int64_t xdist = scale_(1), int
|
|||
Point rayStart = line.a;
|
||||
Point rayEnd = line.b;
|
||||
IndexPair currentVoxel = point_map_grid_index(rayStart, xdist, ydist);
|
||||
IndexPair firstVoxel = currentVoxel;
|
||||
IndexPair lastVoxel = point_map_grid_index(rayEnd, xdist, ydist);
|
||||
|
||||
Point ray = rayEnd - rayStart;
|
||||
|
|
|
@ -70,7 +70,8 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
|
|||
" MANUAL_TOOL_CHANGE ",
|
||||
"_DURING_PRINT_EXHAUST_FAN",
|
||||
" WIPE_TOWER_START",
|
||||
" WIPE_TOWER_END"
|
||||
" WIPE_TOWER_END",
|
||||
" PA_CHANGE:"
|
||||
};
|
||||
|
||||
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
||||
|
@ -90,7 +91,8 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
|
|||
" MANUAL_TOOL_CHANGE ",
|
||||
"_DURING_PRINT_EXHAUST_FAN",
|
||||
" WIPE_TOWER_START",
|
||||
" WIPE_TOWER_END"
|
||||
" WIPE_TOWER_END",
|
||||
" PA_CHANGE:"
|
||||
};
|
||||
|
||||
|
||||
|
@ -695,7 +697,9 @@ void GCodeProcessor::TimeProcessor::post_process(const std::string& filename, st
|
|||
if (!disable_m73 && !processed &&!is_temporary_decoration(gcode_line) &&
|
||||
(GCodeReader::GCodeLine::cmd_is(gcode_line, "G1") ||
|
||||
GCodeReader::GCodeLine::cmd_is(gcode_line, "G2") ||
|
||||
GCodeReader::GCodeLine::cmd_is(gcode_line, "G3"))) {
|
||||
GCodeReader::GCodeLine::cmd_is(gcode_line, "G3") ||
|
||||
GCodeReader::GCodeLine::cmd_is(gcode_line, "G10")||
|
||||
GCodeReader::GCodeLine::cmd_is(gcode_line, "G11"))) {
|
||||
// remove temporary lines, add lines M73 where needed
|
||||
unsigned int extra_lines_count = process_line_move(g1_lines_counter ++);
|
||||
if (extra_lines_count > 0)
|
||||
|
@ -3477,7 +3481,6 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line)
|
|||
arc_length = ((int)line.p()) * 2 * PI * (start_point - m_arc_center).norm();
|
||||
//BBS: Attention! arc_onterpolation does not support P mode while P is not 1.
|
||||
arc_interpolation(start_point, end_point, m_arc_center, (m_move_path_type == EMovePathType::Arc_move_ccw));
|
||||
float radian = ArcSegment::calc_arc_radian(start_point, end_point, m_arc_center, (m_move_path_type == EMovePathType::Arc_move_ccw));
|
||||
Vec3f start_dir = Circle::calc_tangential_vector(start_point, m_arc_center, (m_move_path_type == EMovePathType::Arc_move_ccw));
|
||||
Vec3f end_dir = Circle::calc_tangential_vector(end_point, m_arc_center, (m_move_path_type == EMovePathType::Arc_move_ccw));
|
||||
|
||||
|
@ -3838,14 +3841,18 @@ void GCodeProcessor::process_G29(const GCodeReader::GCodeLine& line)
|
|||
|
||||
void GCodeProcessor::process_G10(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
// stores retract move
|
||||
store_move_vertex(EMoveType::Retract);
|
||||
GCodeReader::GCodeLine g10;
|
||||
g10.set(Axis::E, -this->m_parser.config().retraction_length.get_at(m_extruder_id));
|
||||
g10.set(Axis::F, this->m_parser.config().retraction_speed.get_at(m_extruder_id) * 60);
|
||||
process_G1(g10);
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
// stores unretract move
|
||||
store_move_vertex(EMoveType::Unretract);
|
||||
GCodeReader::GCodeLine g11;
|
||||
g11.set(Axis::E, this->m_parser.config().retraction_length.get_at(m_extruder_id) + this->m_parser.config().retract_restart_extra.get_at(m_extruder_id));
|
||||
g11.set(Axis::F, this->m_parser.config().deretraction_speed.get_at(m_extruder_id) * 60);
|
||||
process_G1(g11);
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_G20(const GCodeReader::GCodeLine& line)
|
||||
|
|
|
@ -294,6 +294,7 @@ class Print;
|
|||
During_Print_Exhaust_Fan,
|
||||
Wipe_Tower_Start,
|
||||
Wipe_Tower_End,
|
||||
PA_Change,
|
||||
};
|
||||
|
||||
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
|
||||
|
|
100
src/libslic3r/GCode/PchipInterpolatorHelper.cpp
Normal file
100
src/libslic3r/GCode/PchipInterpolatorHelper.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
// PchipInterpolatorHelper.cpp
|
||||
// OrcaSlicer
|
||||
//
|
||||
// Implementation file for the PchipInterpolatorHelper class
|
||||
|
||||
#include "PchipInterpolatorHelper.hpp"
|
||||
#include <stdexcept>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* @brief Constructs the PCHIP interpolator with given data points.
|
||||
* @param x The x-coordinates of the data points.
|
||||
* @param y The y-coordinates of the data points.
|
||||
*/
|
||||
PchipInterpolatorHelper::PchipInterpolatorHelper(const std::vector<double>& x, const std::vector<double>& y) {
|
||||
setData(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the data points for the interpolator.
|
||||
* @param x The x-coordinates of the data points.
|
||||
* @param y The y-coordinates of the data points.
|
||||
* @throw std::invalid_argument if x and y have different sizes or if they contain fewer than two points.
|
||||
*/
|
||||
void PchipInterpolatorHelper::setData(const std::vector<double>& x, const std::vector<double>& y) {
|
||||
if (x.size() != y.size() || x.size() < 2) {
|
||||
throw std::invalid_argument("Input vectors must have the same size and contain at least two points.");
|
||||
}
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
sortData();
|
||||
computePCHIP();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sorts the data points by x-coordinate.
|
||||
*/
|
||||
void PchipInterpolatorHelper::sortData() {
|
||||
std::vector<std::pair<double, double>> data;
|
||||
for (size_t i = 0; i < x_.size(); ++i) {
|
||||
data.emplace_back(x_[i], y_[i]);
|
||||
}
|
||||
std::sort(data.begin(), data.end());
|
||||
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
x_[i] = data[i].first;
|
||||
y_[i] = data[i].second;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the PCHIP coefficients.
|
||||
*/
|
||||
void PchipInterpolatorHelper::computePCHIP() {
|
||||
size_t n = x_.size() - 1;
|
||||
h_.resize(n);
|
||||
delta_.resize(n);
|
||||
d_.resize(n+1);
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
h_[i] = h(i);
|
||||
delta_[i] = delta(i);
|
||||
}
|
||||
|
||||
d_[0] = delta_[0];
|
||||
d_[n] = delta_[n-1];
|
||||
for (size_t i = 1; i < n; ++i) {
|
||||
if (delta_[i-1] * delta_[i] > 0) {
|
||||
double w1 = 2 * h_[i] + h_[i-1];
|
||||
double w2 = h_[i] + 2 * h_[i-1];
|
||||
d_[i] = (w1 + w2) / (w1 / delta_[i-1] + w2 / delta_[i]);
|
||||
} else {
|
||||
d_[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Interpolates the value at a given point.
|
||||
*/
|
||||
double PchipInterpolatorHelper::interpolate(double xi) const {
|
||||
if (xi <= x_.front()) return y_.front();
|
||||
if (xi >= x_.back()) return y_.back();
|
||||
|
||||
auto it = std::lower_bound(x_.begin(), x_.end(), xi);
|
||||
size_t i = std::distance(x_.begin(), it) - 1;
|
||||
|
||||
double h_i = h_[i];
|
||||
double t = (xi - x_[i]) / h_i;
|
||||
double t2 = t * t;
|
||||
double t3 = t2 * t;
|
||||
|
||||
double h00 = 2 * t3 - 3 * t2 + 1;
|
||||
double h10 = t3 - 2 * t2 + t;
|
||||
double h01 = -2 * t3 + 3 * t2;
|
||||
double h11 = t3 - t2;
|
||||
|
||||
return h00 * y_[i] + h10 * h_i * d_[i] + h01 * y_[i+1] + h11 * h_i * d_[i+1];
|
||||
}
|
76
src/libslic3r/GCode/PchipInterpolatorHelper.hpp
Normal file
76
src/libslic3r/GCode/PchipInterpolatorHelper.hpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
// PchipInterpolatorHelper.hpp
|
||||
// OrcaSlicer
|
||||
//
|
||||
// Header file for the PchipInterpolatorHelper class, responsible for performing Piecewise Cubic Hermite Interpolating Polynomial (PCHIP) interpolation on given data points.
|
||||
|
||||
#ifndef PCHIPINTERPOLATORHELPER_HPP
|
||||
#define PCHIPINTERPOLATORHELPER_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @class PchipInterpolatorHelper
|
||||
* @brief A helper class to perform Piecewise Cubic Hermite Interpolating Polynomial (PCHIP) interpolation.
|
||||
*/
|
||||
class PchipInterpolatorHelper {
|
||||
public:
|
||||
/**
|
||||
* @brief Default constructor.
|
||||
*/
|
||||
PchipInterpolatorHelper() = default;
|
||||
|
||||
/**
|
||||
* @brief Constructs the PCHIP interpolator with given data points.
|
||||
* @param x The x-coordinates of the data points.
|
||||
* @param y The y-coordinates of the data points.
|
||||
*/
|
||||
PchipInterpolatorHelper(const std::vector<double>& x, const std::vector<double>& y);
|
||||
|
||||
/**
|
||||
* @brief Sets the data points for the interpolator.
|
||||
* @param x The x-coordinates of the data points.
|
||||
* @param y The y-coordinates of the data points.
|
||||
* @throw std::invalid_argument if x and y have different sizes or if they contain fewer than two points.
|
||||
*/
|
||||
void setData(const std::vector<double>& x, const std::vector<double>& y);
|
||||
|
||||
/**
|
||||
* @brief Interpolates the value at a given point.
|
||||
* @param xi The x-coordinate at which to interpolate.
|
||||
* @return The interpolated y-coordinate.
|
||||
*/
|
||||
double interpolate(double xi) const;
|
||||
|
||||
private:
|
||||
std::vector<double> x_; ///< The x-coordinates of the data points.
|
||||
std::vector<double> y_; ///< The y-coordinates of the data points.
|
||||
std::vector<double> h_; ///< The differences between successive x-coordinates.
|
||||
std::vector<double> delta_; ///< The slopes of the segments between successive data points.
|
||||
std::vector<double> d_; ///< The derivatives at the data points.
|
||||
|
||||
/**
|
||||
* @brief Computes the PCHIP coefficients.
|
||||
*/
|
||||
void computePCHIP();
|
||||
|
||||
/**
|
||||
* @brief Sorts the data points by x-coordinate.
|
||||
*/
|
||||
void sortData();
|
||||
|
||||
/**
|
||||
* @brief Computes the difference between successive x-coordinates.
|
||||
* @param i The index of the x-coordinate.
|
||||
* @return The difference between x_[i+1] and x_[i].
|
||||
*/
|
||||
double h(int i) const { return x_[i+1] - x_[i]; }
|
||||
|
||||
/**
|
||||
* @brief Computes the slope of the segment between successive data points.
|
||||
* @param i The index of the segment.
|
||||
* @return The slope of the segment between y_[i] and y_[i+1].
|
||||
*/
|
||||
double delta(int i) const { return (y_[i+1] - y_[i]) / h(i); }
|
||||
};
|
||||
|
||||
#endif // PCHIPINTERPOLATORHELPER_HPP
|
|
@ -1,6 +1,4 @@
|
|||
#include "../ClipperUtils.hpp"
|
||||
#include "../Layer.hpp"
|
||||
#include "../Polyline.hpp"
|
||||
|
||||
#include "RetractWhenCrossingPerimeters.hpp"
|
||||
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
|
||||
namespace Slic3r {
|
||||
|
||||
#ifndef _WIN32
|
||||
// Currently on Linux/macOS, this class spits out large amounts of subobject linkage
|
||||
// warnings because of the flowModel field. tk::spline is in an anonymous namespace which
|
||||
// causes this issue. Until the issue can be solved, this is a temporary solution.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wsubobject-linkage"
|
||||
#endif
|
||||
|
||||
class SmallAreaInfillFlowCompensator
|
||||
{
|
||||
public:
|
||||
|
@ -31,6 +39,10 @@ private:
|
|||
double max_modified_length() { return eLengths.back(); }
|
||||
};
|
||||
|
||||
#ifndef _WIN32
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_GCode_SmallAreaInfillFlowCompensator_hpp_ */
|
||||
|
|
|
@ -131,7 +131,7 @@ std::string SpiralVase::process_layer(const std::string &gcode, bool last_layer)
|
|||
if (line.has_z() && !line.retracting(reader)) {
|
||||
// If this is the initial Z move of the layer, replace it with a
|
||||
// (redundant) move to the last Z of previous layer.
|
||||
line.set(reader, Z, z);
|
||||
line.set(Z, z);
|
||||
new_gcode += line.raw() + '\n';
|
||||
return;
|
||||
} else {
|
||||
|
@ -142,17 +142,17 @@ std::string SpiralVase::process_layer(const std::string &gcode, bool last_layer)
|
|||
float factor = len / total_layer_length;
|
||||
if (transition_in)
|
||||
// Transition layer, interpolate the amount of extrusion from zero to the final value.
|
||||
line.set(reader, E, line.e() * factor, 5 /*decimal_digits*/);
|
||||
line.set(E, line.e() * factor, 5 /*decimal_digits*/);
|
||||
else if (transition_out) {
|
||||
// We want the last layer to ramp down extrusion, but without changing z height!
|
||||
// So clone the line before we mess with its Z and duplicate it into a new layer that ramps down E
|
||||
// We add this new layer at the very end
|
||||
GCodeReader::GCodeLine transitionLine(line);
|
||||
transitionLine.set(reader, E, line.e() * (1 - factor), 5 /*decimal_digits*/);
|
||||
transitionLine.set(E, line.e() * (1 - factor), 5 /*decimal_digits*/);
|
||||
transition_gcode += transitionLine.raw() + '\n';
|
||||
}
|
||||
// This line is the core of Spiral Vase mode, ramp up the Z smoothly
|
||||
line.set(reader, Z, z + factor * layer_height);
|
||||
line.set(Z, z + factor * layer_height);
|
||||
if (smooth_spiral) {
|
||||
// Now we also need to try to interpolate X and Y
|
||||
SpiralVase::SpiralPoint p(line.x(), line.y()); // Get current x/y coordinates
|
||||
|
@ -171,10 +171,10 @@ std::string SpiralVase::process_layer(const std::string &gcode, bool last_layer)
|
|||
if (modified_dist_XY < 0.001)
|
||||
line.clear();
|
||||
else {
|
||||
line.set(reader, X, target.x);
|
||||
line.set(reader, Y, target.y);
|
||||
line.set(X, target.x);
|
||||
line.set(Y, target.y);
|
||||
// Scale the extrusion amount according to change in length
|
||||
line.set(reader, E, line.e() * modified_dist_XY / dist_XY, 5 /*decimal_digits*/);
|
||||
line.set(E, line.e() * modified_dist_XY / dist_XY, 5 /*decimal_digits*/);
|
||||
last_point = target;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -307,7 +307,6 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
|
|||
std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const Print& print)
|
||||
{
|
||||
std::vector<unsigned int> tool_order;
|
||||
int initial_extruder_id = -1;
|
||||
std::map<int, double> min_areas_per_extruder;
|
||||
|
||||
for (auto object : print.objects()) {
|
||||
|
@ -336,7 +335,6 @@ std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const Pr
|
|||
}
|
||||
}
|
||||
|
||||
double max_minimal_area = 0.;
|
||||
for (auto ape : min_areas_per_extruder) {
|
||||
auto iter = tool_order.begin();
|
||||
for (; iter != tool_order.end(); iter++) {
|
||||
|
@ -369,7 +367,6 @@ std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const Pr
|
|||
std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const PrintObject& object)
|
||||
{
|
||||
std::vector<unsigned int> tool_order;
|
||||
int initial_extruder_id = -1;
|
||||
std::map<int, double> min_areas_per_extruder;
|
||||
auto first_layer = object.get_layer(0);
|
||||
for (auto layerm : first_layer->regions()) {
|
||||
|
@ -394,7 +391,6 @@ std::vector<unsigned int> ToolOrdering::generate_first_layer_tool_order(const Pr
|
|||
}
|
||||
}
|
||||
|
||||
double max_minimal_area = 0.;
|
||||
for (auto ape : min_areas_per_extruder) {
|
||||
auto iter = tool_order.begin();
|
||||
for (; iter != tool_order.end(); iter++) {
|
||||
|
|
|
@ -1072,8 +1072,6 @@ void WipeTower::toolchange_Wipe(
|
|||
const float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f;
|
||||
float wipe_speed = 0.33f * target_speed;
|
||||
|
||||
float start_y = writer.y();
|
||||
|
||||
#if 0
|
||||
// if there is less than 2.5*m_perimeter_width to the edge, advance straightaway (there is likely a blob anyway)
|
||||
if ((m_left_to_right ? xr-writer.x() : writer.x()-xl) < 2.5f*m_perimeter_width) {
|
||||
|
@ -1132,8 +1130,6 @@ void WipeTower::toolchange_Wipe(
|
|||
m_left_to_right = !m_left_to_right;
|
||||
}
|
||||
|
||||
float end_y = writer.y();
|
||||
|
||||
// We may be going back to the model - wipe the nozzle. If this is followed
|
||||
// by finish_layer, this wipe path will be overwritten.
|
||||
//writer.add_wipe_point(writer.x(), writer.y())
|
||||
|
@ -1422,7 +1418,6 @@ void WipeTower::plan_tower()
|
|||
// If wipe tower height is between the current and next member, set the min_depth as linear interpolation between them
|
||||
auto next_height_to_depth = *iter;
|
||||
if (next_height_to_depth.first > m_wipe_tower_height) {
|
||||
float height_base = curr_height_to_depth.first;
|
||||
float height_diff = next_height_to_depth.first - curr_height_to_depth.first;
|
||||
float min_depth_base = curr_height_to_depth.second;
|
||||
float depth_diff = next_height_to_depth.second - curr_height_to_depth.second;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue