mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 04:11:13 +00:00
Merge branch 'main' into hanif/wizard-loading-signal
This commit is contained in:
+16
-1
@@ -144,6 +144,7 @@
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/GCode/ToolOrdering.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -207,6 +208,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
|
||||
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
|
||||
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
|
||||
{CLI_EXPORT_STATISTICS_ERROR, "Failed exporting the G-code statistics."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
|
||||
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
|
||||
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
|
||||
@@ -1539,6 +1541,9 @@ int CLI::run(int argc, char **argv)
|
||||
if (allow_mix_temp_option)
|
||||
allow_mix_temp = allow_mix_temp_option->value;
|
||||
|
||||
const ConfigOptionBool* export_statistics_option = m_config.option<ConfigOptionBool>("export_statistics");
|
||||
const bool export_statistics = export_statistics_option != nullptr && export_statistics_option->value;
|
||||
|
||||
ConfigOptionBool* avoid_extrusion_cali_region_option = m_config.option<ConfigOptionBool>("avoid_extrusion_cali_region");
|
||||
if (avoid_extrusion_cali_region_option)
|
||||
avoid_extrusion_cali_region = avoid_extrusion_cali_region_option->value;
|
||||
@@ -7081,6 +7086,16 @@ int CLI::run(int argc, char **argv)
|
||||
record_exit_reson(outfile_dir, CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, index + 1, cli_errors[CLI_GCODE_PATH_IN_UNPRINTABLE_AREA], sliced_info);
|
||||
flush_and_exit(CLI_GCODE_PATH_IN_UNPRINTABLE_AREA);
|
||||
}
|
||||
// Without an output directory the G-code is a temporary file, kept only in an exported 3MF.
|
||||
if (export_statistics && !outfile_dir.empty()) {
|
||||
try {
|
||||
export_gcode_statistics(*gcode_result, gcode_statistics_path(outfile));
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate " << index + 1 << ": " << ex.what();
|
||||
record_exit_reson(outfile_dir, CLI_EXPORT_STATISTICS_ERROR, index + 1, cli_errors[CLI_EXPORT_STATISTICS_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_EXPORT_STATISTICS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//outfile_final = (dynamic_cast<Print*>(print))->print_statistics().finalize_output_path(outfile);
|
||||
@@ -7209,7 +7224,7 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<ThumbnailData *> thumbnails, no_light_thumbnails, top_thumbnails, pick_thumbnails;
|
||||
std::vector<PlateBBoxData*> plate_bboxes;
|
||||
PlateDataPtrs plate_data_list;
|
||||
partplate_list.store_to_3mf_structure(plate_data_list);
|
||||
partplate_list.store_to_3mf_structure(plate_data_list, true, -1, export_statistics);
|
||||
|
||||
if (sliced_plate == -1) {
|
||||
for (int i = 0; i < plate_data_list.size(); i++) {
|
||||
|
||||
@@ -399,6 +399,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_plate_gridlines").empty())
|
||||
set_bool("show_plate_gridlines", true);
|
||||
|
||||
if (get("focus_mode").empty())
|
||||
set_bool("focus_mode", false);
|
||||
|
||||
if (get("show_outline").empty())
|
||||
set_bool("show_outline", false);
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ set(lisbslic3r_sources
|
||||
GCode/FanMover.hpp
|
||||
GCode/GCodeProcessor.cpp
|
||||
GCode/GCodeProcessor.hpp
|
||||
GCode/GCodeStatistics.cpp
|
||||
GCode/GCodeStatistics.hpp
|
||||
GCode/ElegooGCodeProcessorHelper.cpp
|
||||
GCode.hpp
|
||||
GCode/PchipInterpolatorHelper.cpp
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include <boost/spirit/home/karma.hpp>
|
||||
#include <deque>
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
#include <boost/thread/lock_guard.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_duration.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_types.hpp>
|
||||
@@ -6541,7 +6542,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(",add no light thumbnail %1%'s data into 3mf") % (index + 1);
|
||||
thumbnail_status[index] = true;
|
||||
no_light_thumbnail_status[index] = true;
|
||||
}
|
||||
}
|
||||
// Adds the file Metadata/top_i.png and Metadata/pick_i.png
|
||||
@@ -9052,6 +9053,16 @@ bool _BBS_3MF_Exporter::_add_gcode_file_to_archive(mz_zip_archive& archive, cons
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", store %1% to 3mf %2%\n") % src_gcode_file % gcode_in_3mf;
|
||||
}
|
||||
});
|
||||
for (const PlateData *plate_data : plate_data_list2) {
|
||||
if (plate_data->statistics.empty())
|
||||
continue;
|
||||
const std::string statistics_in_3mf = (boost::format(STATISTICS_FILE_FORMAT) % (plate_data->plate_index + 1)).str();
|
||||
if (!mz_zip_writer_add_mem(&archive, statistics_in_3mf.c_str(), plate_data->statistics.data(), plate_data->statistics.size(),
|
||||
MZ_DEFAULT_COMPRESSION)) {
|
||||
add_error("Unable to add statistics file to archive");
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9776,13 +9787,13 @@ bool has_restore_data(std::string & path, std::string& origin)
|
||||
origin = "<lock>";
|
||||
return false;
|
||||
}
|
||||
// ORCA: non-throwing probes; ec is deliberately unread, a backup path that cannot be
|
||||
// probed is treated as having no restore data rather than aborting the application.
|
||||
// ORCA: the caller removes this directory unless origin is "<lock>", so every way of
|
||||
// failing to establish the backup's state has to claim the lock instead.
|
||||
boost::system::error_code ec;
|
||||
if (boost::filesystem::exists(path + "/lock.txt", ec)) {
|
||||
std::string pid;
|
||||
load_string_file(path + "/lock.txt", pid);
|
||||
try {
|
||||
load_string_file(path + "/lock.txt", pid);
|
||||
if (get_process_name(boost::lexical_cast<int>(pid)) ==
|
||||
get_process_name(0)) {
|
||||
origin = "<lock>";
|
||||
@@ -9790,12 +9801,20 @@ bool has_restore_data(std::string & path, std::string& origin)
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
origin = "<lock>";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::string file3mf = path + "/.3mf";
|
||||
if (!boost::filesystem::exists(file3mf, ec))
|
||||
if (!boost::filesystem::exists(file3mf, ec)) {
|
||||
// ORCA: a .3mf that is simply absent is a spent backup and stays collectable. A probe
|
||||
// that could not look must not be: ec reports ENOENT/ENOTDIR for "not there" as
|
||||
// well, so only a different error means the answer is unknown.
|
||||
if (ec && ec != boost::system::errc::no_such_file_or_directory &&
|
||||
ec != boost::system::errc::not_a_directory)
|
||||
origin = "<lock>";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (boost::filesystem::exists(path + "/origin.txt", ec))
|
||||
load_string_file(path + "/origin.txt", origin);
|
||||
|
||||
@@ -32,6 +32,7 @@ struct ThumbnailData;
|
||||
#define PLATE_THUMBNAIL_SMALL_HEIGHT 128
|
||||
|
||||
#define GCODE_FILE_FORMAT "Metadata/plate_%1%.gcode"
|
||||
#define STATISTICS_FILE_FORMAT "Metadata/plate_%1%.statistics.json"
|
||||
#define THUMBNAIL_FILE_FORMAT "Metadata/plate_%1%.png"
|
||||
#define NO_LIGHT_THUMBNAIL_FILE_FORMAT "Metadata/plate_no_light_%1%.png"
|
||||
#define TOP_FILE_FORMAT "Metadata/top_%1%.png"
|
||||
@@ -100,6 +101,8 @@ struct PlateData
|
||||
std::string nozzle_volume_types;
|
||||
std::string gcode_file;
|
||||
std::string gcode_file_md5;
|
||||
// The G-code's statistics as JSON, stored with the G-code when not empty.
|
||||
std::string statistics;
|
||||
std::string thumbnail_file;
|
||||
std::string no_light_thumbnail_file;
|
||||
ThumbnailData plate_thumbnail;
|
||||
@@ -174,6 +177,8 @@ enum class SaveStrategy
|
||||
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
|
||||
// production-extension value.
|
||||
MinimalPublished = 1 << 13,
|
||||
// With WithGcode, also the G-code's statistics.
|
||||
WithStatistics = 1 << 14,
|
||||
|
||||
SplitModel = 0x1000 | ProductionExt,
|
||||
Encrypted = SecureContentExt | SplitModel,
|
||||
|
||||
+40
-20
@@ -2625,8 +2625,15 @@ static void set_mass_locator(GCodeProcessor &processor, const Print &print)
|
||||
}
|
||||
Object &o = objects.emplace_back(Object{ object, int(object_masses.size()), count, bodies_total, {}, std::move(bodies), {}, {} });
|
||||
object_masses.resize(object_masses.size() + object->instances().size());
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance)
|
||||
object_masses[o.first_instance + instance].assembly = assembly;
|
||||
const ModelInstancePtrs &model_instances = object->model_object()->instances;
|
||||
for (size_t instance = 0; instance < object->instances().size(); ++instance) {
|
||||
GCodeProcessorResult::ObjectMass &mass = object_masses[o.first_instance + instance];
|
||||
mass.assembly = assembly;
|
||||
mass.name = object->model_object()->name;
|
||||
// Copies of another rotation or scale slice as objects of their own, so number them in the model object.
|
||||
mass.instance = int(std::find(model_instances.begin(), model_instances.end(), object->instances()[instance].model_instance) -
|
||||
model_instances.begin());
|
||||
}
|
||||
bodies_total += int(count * object->instances().size());
|
||||
for (const Layer *layer : layers) {
|
||||
o.print_zs.emplace_back(layer->print_z);
|
||||
@@ -3563,6 +3570,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
print.throw_if_canceled();
|
||||
|
||||
// Taken before the object loop, which reassigns initial_extruder_id when printing by object.
|
||||
const int compat_first_layer_bed_temp = m_config.bed_temperature_formula == BedTempFormula::btfHighestTemp ?
|
||||
get_highest_bed_temperature(true, print) :
|
||||
get_bed_temperature(initial_extruder_id, true, print.config().curr_bed_type);
|
||||
const int compat_first_layer_nozzle_temp =
|
||||
print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int) initial_extruder_id));
|
||||
|
||||
int extruder_id = get_extruder_id(initial_extruder_id);
|
||||
|
||||
m_cooling_buffer = make_unique<CoolingBuffer>(*this);
|
||||
@@ -4153,12 +4167,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple image
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n",
|
||||
first_layer_bed_temperature);
|
||||
file.write_format(
|
||||
"; first_layer_temperature = %d\n",
|
||||
print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
} else if (thumbnail_cb != nullptr) {
|
||||
// generate the thumbnails
|
||||
@@ -4201,6 +4211,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
|
||||
file.write_format("; EXECUTABLE_BLOCK_START\n");
|
||||
|
||||
// Set for every printer, as the object label comments use these ids too.
|
||||
assign_object_label_ids(print);
|
||||
// SoftFever
|
||||
if( m_enable_exclude_object)
|
||||
file.write(set_object_info(&print));
|
||||
@@ -4741,10 +4753,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
@@ -11055,15 +11066,30 @@ inline std::string polygon_to_string(const Polygon &polygon, Print *print, bool
|
||||
gcode << "]";
|
||||
return gcode.str();
|
||||
}
|
||||
// this function iterator PrintObject and assign a seqential id to each object.
|
||||
// this id is used to generate unique object id for each object.
|
||||
// Numbers the objects, and their instances both within each object (id) and across the print
|
||||
// (unique_id). The "; printing object" comments and the exclude-object commands label objects with these ids.
|
||||
void GCode::assign_object_label_ids(Print &print)
|
||||
{
|
||||
m_instance_names.clear();
|
||||
size_t object_id = 0;
|
||||
size_t unique_id = 0;
|
||||
for (PrintObject *object : print.objects()) {
|
||||
object->set_id(object_id++);
|
||||
size_t inst_id = 0;
|
||||
for (PrintInstance &inst : object->instances()) {
|
||||
inst.id = inst_id++;
|
||||
inst.unique_id = unique_id++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Writes the exclude-object definitions for the ids assign_object_label_ids() set.
|
||||
std::string GCode::set_object_info(Print *print) {
|
||||
const auto gflavor = print->config().gcode_flavor.value;
|
||||
if (print->is_BBL_printer() ||
|
||||
(gflavor != gcfKlipper && gflavor != gcfMarlinLegacy && gflavor != gcfMarlinFirmware && gflavor != gcfRepRapFirmware))
|
||||
return "";
|
||||
std::ostringstream gcode;
|
||||
size_t object_id = 0;
|
||||
// Orca: check if we are in pa calib mode
|
||||
if (print->calib_mode() == CalibMode::Calib_PA_Pattern) {
|
||||
BoundingBoxf bbox_bed(print->config().printable_area.values);
|
||||
@@ -11079,14 +11105,8 @@ std::string GCode::set_object_info(Print *print) {
|
||||
} else if (print->calib_mode() == CalibMode::Calib_PA_Line) {
|
||||
// PA_Line has only one object, no EXCLUDE_OBJECT_DEFINE needed
|
||||
} else {
|
||||
size_t unique_id = 0;
|
||||
m_instance_names.clear();
|
||||
for (PrintObject* object : print->objects()) {
|
||||
object->set_id(object_id++);
|
||||
size_t inst_id = 0;
|
||||
for (PrintInstance& inst : object->instances()) {
|
||||
inst.unique_id = unique_id++;
|
||||
inst.id = inst_id++;
|
||||
// Outlines are in plate coordinates. On a belt printer that is the frame after
|
||||
// the slicing rotation has been undone and before the G-code axis remap and
|
||||
// machine-frame shear: where the object stands on the belt, which is what an
|
||||
|
||||
@@ -319,6 +319,7 @@ public:
|
||||
bool is_BBL_Printer();
|
||||
WipeTowerType wipe_tower_type();
|
||||
|
||||
void assign_object_label_ids(Print &print);
|
||||
// SoftFever
|
||||
std::string set_object_info(Print* print);
|
||||
|
||||
@@ -794,7 +795,7 @@ protected:
|
||||
|
||||
bool m_enable_exclude_object;
|
||||
std::vector<size_t> m_label_objects_ids;
|
||||
// Object label names by instance, built on first use from the ids set_object_info() assigns.
|
||||
// Object label names by instance, built on first use from the ids assign_object_label_ids() assigns.
|
||||
std::unordered_map<const PrintInstance*, std::string> m_instance_names;
|
||||
const std::string& instance_name(const PrintInstance &instance);
|
||||
std::string _encode_label_ids_to_base64(std::vector<size_t> ids);
|
||||
|
||||
@@ -298,6 +298,9 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
BoundingBoxf3 box;
|
||||
// Of an object, whether it is an assembly.
|
||||
bool assembly{ false };
|
||||
// Of an object instance, its model object's name and its index among that object's instances.
|
||||
std::string name;
|
||||
int instance{ -1 };
|
||||
|
||||
Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); }
|
||||
void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "GCodeStatistics.hpp"
|
||||
|
||||
#include "GCodeProcessor.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <ios>
|
||||
#include <map>
|
||||
#include <math.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::ordered_json;
|
||||
|
||||
double rounded(double value, int decimals)
|
||||
{
|
||||
const double scale = std::pow(10., decimals);
|
||||
return std::round(value * scale) / scale;
|
||||
}
|
||||
|
||||
// As the object masses weigh it, at the default density for a filament without one.
|
||||
double weight_of(const GCodeProcessorResult &result, size_t filament, double volume)
|
||||
{
|
||||
const bool has_density = filament < result.filament_densities.size() && result.filament_densities[filament] > 0.f;
|
||||
// mm³ at g/cm³ weigh mg.
|
||||
return 0.001 * volume * (has_density ? result.filament_densities[filament] : DEFAULT_FILAMENT_DENSITY);
|
||||
}
|
||||
|
||||
struct Usage
|
||||
{
|
||||
double time{ 0. };
|
||||
double volume{ 0. };
|
||||
double weight{ 0. };
|
||||
|
||||
void add(const Usage &other)
|
||||
{
|
||||
time += other.time;
|
||||
volume += other.volume;
|
||||
weight += other.weight;
|
||||
}
|
||||
|
||||
json to_json() const { return json{ { "time", rounded(time, 3) }, { "volume", rounded(volume, 3) }, { "weight", rounded(weight, 4) } }; }
|
||||
};
|
||||
|
||||
json features_json(const std::map<ExtrusionRole, Usage> &features)
|
||||
{
|
||||
json out = json::object();
|
||||
for (const auto &[role, usage] : features)
|
||||
out[ExtrusionEntity::role_to_string(role)] = usage.to_json();
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result)
|
||||
{
|
||||
constexpr size_t normal = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
|
||||
struct Layer
|
||||
{
|
||||
float z{ 0.f };
|
||||
double travel_time{ 0. };
|
||||
Usage total;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
};
|
||||
std::vector<Layer> layers;
|
||||
std::map<ExtrusionRole, Usage> features;
|
||||
double travel_time = 0.;
|
||||
|
||||
// As the preview sums them: a layer takes the time of all its moves, a feature that of its extrusions.
|
||||
for (const GCodeProcessorResult::MoveVertex &move : result.moves) {
|
||||
if (layers.size() <= move.layer_id)
|
||||
layers.resize(move.layer_id + 1);
|
||||
Layer &layer = layers[move.layer_id];
|
||||
const double time = move.time[normal];
|
||||
layer.total.time += time;
|
||||
if (move.type == EMoveType::Travel) {
|
||||
layer.travel_time += time;
|
||||
travel_time += time;
|
||||
}
|
||||
// A vertex added inside a move to show its acceleration repeats the move's extrusion.
|
||||
if (move.type != EMoveType::Extrude || move.internal_only)
|
||||
continue;
|
||||
if (move.extrusion_role != erCustom)
|
||||
layer.z = move.position.z();
|
||||
Usage usage;
|
||||
usage.time = time;
|
||||
usage.volume = double(move.delta_extruder) * M_PI * sqr(0.5 * double(result.filament_diameters[move.extruder_id]));
|
||||
usage.weight = weight_of(result, move.extruder_id, usage.volume);
|
||||
layer.total.volume += usage.volume;
|
||||
layer.total.weight += usage.weight;
|
||||
layer.features[move.extrusion_role].add(usage);
|
||||
features[move.extrusion_role].add(usage);
|
||||
}
|
||||
|
||||
// As the preview's filament table, which counts the flush too.
|
||||
const PrintEstimatedStatistics &stats = result.print_statistics;
|
||||
double volume = 0., weight = 0., cost = 0.;
|
||||
json filaments_json = json::array();
|
||||
for (const auto &entry : stats.total_volumes_per_extruder) {
|
||||
const size_t filament = entry.first;
|
||||
const auto use = [&result, filament](const std::map<size_t, double> &volumes) {
|
||||
const auto it = volumes.find(filament);
|
||||
const double used = it == volumes.end() ? 0. : it->second;
|
||||
return json{ { "volume", rounded(used, 3) }, { "weight", rounded(weight_of(result, filament, used), 4) } };
|
||||
};
|
||||
const double filament_weight = weight_of(result, filament, entry.second);
|
||||
// Priced per kilogram.
|
||||
const double filament_cost = filament < result.filament_costs.size() ? 0.001 * filament_weight * result.filament_costs[filament] : 0.;
|
||||
volume += entry.second;
|
||||
weight += filament_weight;
|
||||
cost += filament_cost;
|
||||
filaments_json.push_back(json{ { "id", filament + 1 },
|
||||
{ "length", rounded(entry.second / (M_PI * sqr(0.5 * double(result.filament_diameters[filament]))), 2) },
|
||||
{ "volume", rounded(entry.second, 3) },
|
||||
{ "weight", rounded(filament_weight, 4) },
|
||||
{ "cost", rounded(filament_cost, 2) },
|
||||
{ "model", use(stats.model_volumes_per_extruder) },
|
||||
{ "support", use(stats.support_volumes_per_extruder) },
|
||||
{ "prime_tower", use(stats.wipe_tower_volumes_per_extruder) },
|
||||
{ "flush", use(stats.flush_per_filament) } });
|
||||
}
|
||||
|
||||
json summary = json{ { "time", rounded(stats.modes[normal].time, 3) },
|
||||
{ "prepare_time", rounded(stats.modes[normal].prepare_time, 3) },
|
||||
{ "travel_time", rounded(travel_time, 3) },
|
||||
{ "travel_distance", rounded(stats.total_travel_distance, 2) },
|
||||
{ "layer_count", layers.size() },
|
||||
{ "volume", rounded(volume, 3) },
|
||||
{ "weight", rounded(weight, 4) },
|
||||
{ "filament_cost", rounded(cost, 2) },
|
||||
{ "filament_changes", stats.total_filament_changes },
|
||||
{ "filaments", filaments_json },
|
||||
{ "features", features_json(features) } };
|
||||
|
||||
json layers_json = json::array();
|
||||
for (const Layer &layer : layers)
|
||||
layers_json.push_back(json{ { "z", rounded(layer.z, 3) },
|
||||
{ "time", rounded(layer.total.time, 3) },
|
||||
{ "travel_time", rounded(layer.travel_time, 3) },
|
||||
{ "volume", rounded(layer.total.volume, 3) },
|
||||
{ "weight", rounded(layer.total.weight, 4) },
|
||||
{ "features", features_json(layer.features) } });
|
||||
|
||||
// The supports of an instance are numbered as the instance.
|
||||
json objects_json = json::array();
|
||||
for (size_t i = 0; i < result.object_masses.size(); ++i) {
|
||||
const GCodeProcessorResult::ObjectMass &object = result.object_masses[i];
|
||||
const GCodeProcessorResult::ObjectMass::Sum part = object.total();
|
||||
const GCodeProcessorResult::ObjectMass::Sum support = i < result.support_masses.size() ? result.support_masses[i].total() :
|
||||
GCodeProcessorResult::ObjectMass::Sum{};
|
||||
// In mg, as volumes in mm³ at g/cm³.
|
||||
objects_json.push_back(json{ { "name", object.name },
|
||||
{ "instance", object.instance },
|
||||
{ "volume", rounded(part.volume, 3) },
|
||||
{ "weight", rounded(0.001 * part.mass, 4) },
|
||||
{ "support_volume", rounded(support.volume, 3) },
|
||||
{ "support_weight", rounded(0.001 * support.mass, 4) } });
|
||||
}
|
||||
|
||||
// The version goes up only when a key is removed, renamed or changes meaning.
|
||||
const json out = json{ { "version", 1 },
|
||||
{ "generator", std::string(SLIC3R_APP_NAME) + " " + SoftFever_VERSION },
|
||||
{ "units", json{ { "time", "s" }, { "length", "mm" }, { "volume", "mm3" }, { "weight", "g" } } },
|
||||
{ "summary", summary },
|
||||
{ "layers", layers_json },
|
||||
{ "objects", objects_json } };
|
||||
return out.dump(2);
|
||||
}
|
||||
|
||||
std::string gcode_statistics_path(const std::string &gcode_path)
|
||||
{
|
||||
return boost::filesystem::path(gcode_path).replace_extension(".statistics.json").string();
|
||||
}
|
||||
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path)
|
||||
{
|
||||
// Binary, so the file reads the same on every platform and inside a 3MF.
|
||||
boost::nowide::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc);
|
||||
file << gcode_statistics_json(result);
|
||||
file.close();
|
||||
if (file.fail())
|
||||
throw ExportError(format(_u8L("Failed to write the G-code statistics to %1%."), path));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct GCodeProcessorResult;
|
||||
|
||||
// The statistics of a plate's G-code as JSON, in seconds, millimeters and grams: totals, per layer and per object instance.
|
||||
std::string gcode_statistics_json(const GCodeProcessorResult &result);
|
||||
// Where the statistics of the G-code at gcode_path are written.
|
||||
std::string gcode_statistics_path(const std::string &gcode_path);
|
||||
// Throws ExportError when the file cannot be written.
|
||||
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1196,7 +1196,6 @@ static void cut_segmented_layers(const std::vector<ExPolygons> &input_exp
|
||||
const std::function<void()> &throw_on_cancel_callback)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - cutting segmented layers in parallel - begin";
|
||||
const float interlocking_cut_width = interlocking_depth > 0.f ? std::max(cut_width - interlocking_depth, 0.f) : 0.f;
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, segmented_regions.size()),
|
||||
[&segmented_regions, &input_expolygons, &cut_width, &interlocking_depth, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
|
||||
|
||||
+71
-87
@@ -149,6 +149,8 @@ void Print::clear()
|
||||
m_print_regions.clear();
|
||||
m_model.clear_objects();
|
||||
m_statistics_by_extruder_count.clear();
|
||||
m_same_shrinkage_compensations = false;
|
||||
m_shrinkage_compensation = Vec3d::Ones();
|
||||
}
|
||||
|
||||
bool Print::has_tpu_filament() const
|
||||
@@ -584,32 +586,36 @@ std::vector<unsigned int> Print::object_extruders() const
|
||||
for (const PrintRegion ®ion : object->all_regions())
|
||||
region.collect_object_printing_extruders(*this, extruders);
|
||||
|
||||
for (const PrintObject* object : m_objects) {
|
||||
const ModelObject* mo = object->model_object();
|
||||
for (const ModelVolume* mv : mo->volumes) {
|
||||
std::vector<int> volume_extruders = mv->get_extruders();
|
||||
for (int extruder : volume_extruders) {
|
||||
assert(extruder > 0);
|
||||
extruders.push_back(extruder - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// layer range
|
||||
for (auto layer_range : mo->layer_config_ranges) {
|
||||
if (layer_range.second.has("extruder")) {
|
||||
//BBS: actually when user doesn't change filament by height range(value is default 0), height range should not save key "extruder".
|
||||
//Don't know why height range always save key "extruder" because of no change(should only save difference)...
|
||||
//Add protection here to avoid overflow
|
||||
auto value = layer_range.second.option("extruder")->getInt();
|
||||
if (value > 0)
|
||||
extruders.push_back(value - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const PrintObject* object : m_objects)
|
||||
append_model_object_extruders(*object->model_object(), extruders);
|
||||
sort_remove_duplicates(extruders);
|
||||
return extruders;
|
||||
}
|
||||
|
||||
// Appends 0-based indices of the filaments assigned to the volumes of model_object (painting included) and to its height ranges.
|
||||
void Print::append_model_object_extruders(const ModelObject &mo, std::vector<unsigned int> &extruders)
|
||||
{
|
||||
for (const ModelVolume* mv : mo.volumes) {
|
||||
std::vector<int> volume_extruders = mv->get_extruders();
|
||||
for (int extruder : volume_extruders) {
|
||||
assert(extruder > 0);
|
||||
extruders.push_back(extruder - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// layer range
|
||||
for (auto layer_range : mo.layer_config_ranges) {
|
||||
if (layer_range.second.has("extruder")) {
|
||||
//BBS: actually when user doesn't change filament by height range(value is default 0), height range should not save key "extruder".
|
||||
//Don't know why height range always save key "extruder" because of no change(should only save difference)...
|
||||
//Add protection here to avoid overflow
|
||||
auto value = layer_range.second.option("extruder")->getInt();
|
||||
if (value > 0)
|
||||
extruders.push_back(value - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns 0-based indices of used extruders
|
||||
std::vector<unsigned int> Print::support_material_extruders() const
|
||||
{
|
||||
@@ -619,22 +625,8 @@ std::vector<unsigned int> Print::support_material_extruders() const
|
||||
auto num_extruders = (unsigned int)m_config.filament_diameter.size();
|
||||
|
||||
for (PrintObject *object : m_objects) {
|
||||
if (object->has_support_material()) {
|
||||
assert(object->config().support_filament >= 0);
|
||||
if (object->config().support_filament == 0)
|
||||
support_uses_current_extruder = true;
|
||||
else {
|
||||
unsigned int i = (unsigned int)object->config().support_filament - 1;
|
||||
extruders.emplace_back((i >= num_extruders) ? 0 : i);
|
||||
}
|
||||
assert(object->config().support_interface_filament >= 0);
|
||||
if (object->config().support_interface_filament == 0)
|
||||
support_uses_current_extruder = true;
|
||||
else {
|
||||
unsigned int i = (unsigned int)object->config().support_interface_filament - 1;
|
||||
extruders.emplace_back((i >= num_extruders) ? 0 : i);
|
||||
}
|
||||
}
|
||||
if (object->has_support_material() && append_support_extruders(object->config(), num_extruders, extruders))
|
||||
support_uses_current_extruder = true;
|
||||
}
|
||||
|
||||
if (support_uses_current_extruder)
|
||||
@@ -645,31 +637,61 @@ std::vector<unsigned int> Print::support_material_extruders() const
|
||||
return extruders;
|
||||
}
|
||||
|
||||
// Appends 0-based indices of the support and support interface filaments of an object printing support material.
|
||||
// Returns true if support or support interface is printed with the filament of the object.
|
||||
bool Print::append_support_extruders(const PrintObjectConfig &config, unsigned int num_extruders, std::vector<unsigned int> &extruders)
|
||||
{
|
||||
bool uses_current_extruder = false;
|
||||
assert(config.support_filament >= 0);
|
||||
if (config.support_filament == 0)
|
||||
uses_current_extruder = true;
|
||||
else {
|
||||
unsigned int i = (unsigned int)config.support_filament - 1;
|
||||
extruders.emplace_back((i >= num_extruders) ? 0 : i);
|
||||
}
|
||||
assert(config.support_interface_filament >= 0);
|
||||
if (config.support_interface_filament == 0)
|
||||
uses_current_extruder = true;
|
||||
else {
|
||||
unsigned int i = (unsigned int)config.support_interface_filament - 1;
|
||||
extruders.emplace_back((i >= num_extruders) ? 0 : i);
|
||||
}
|
||||
return uses_current_extruder;
|
||||
}
|
||||
|
||||
// returns 0-based indices of used extruders
|
||||
std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const
|
||||
{
|
||||
std::vector<unsigned int> extruders = this->object_extruders();
|
||||
append(extruders, this->support_material_extruders());
|
||||
|
||||
if (conside_custom_gcode) {
|
||||
//BBS
|
||||
int num_extruders = m_config.filament_colour.size();
|
||||
if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) {
|
||||
for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) {
|
||||
if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders)
|
||||
extruders.push_back((unsigned int)(item.extruder - 1));
|
||||
}
|
||||
if (conside_custom_gcode)
|
||||
this->append_tool_change_extruders(extruders);
|
||||
this->append_wipe_tower_extruder(extruders);
|
||||
sort_remove_duplicates(extruders);
|
||||
return extruders;
|
||||
}
|
||||
|
||||
// Appends 0-based indices of the filaments the tool changes of the current plate switch to.
|
||||
void Print::append_tool_change_extruders(std::vector<unsigned int> &extruders) const
|
||||
{
|
||||
//BBS
|
||||
int num_extruders = m_config.filament_colour.size();
|
||||
if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) {
|
||||
for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) {
|
||||
if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders)
|
||||
extruders.push_back((unsigned int)(item.extruder - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Print::append_wipe_tower_extruder(std::vector<unsigned int> &extruders) const
|
||||
{
|
||||
// If a wipe tower filament is explicitly set, ensure it participates in tool ordering.
|
||||
if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) {
|
||||
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size()));
|
||||
extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based
|
||||
}
|
||||
|
||||
sort_remove_duplicates(extruders);
|
||||
return extruders;
|
||||
}
|
||||
|
||||
unsigned int Print::num_object_instances() const
|
||||
@@ -5652,44 +5674,6 @@ std::string PrintStatistics::finalize_output_path(const std::string &path_in) co
|
||||
return final_path;
|
||||
}
|
||||
|
||||
// Orca: Implement prusa's filament shrink compensation approach
|
||||
// Returns if all used filaments have same shrinkage compensations.
|
||||
bool Print::has_same_shrinkage_compensations() const {
|
||||
const std::vector<unsigned int> extruders = this->extruders();
|
||||
if (extruders.empty())
|
||||
return false;
|
||||
|
||||
const double filament_shrinkage_compensation_xy = m_config.filament_shrink.get_at(extruders.front());
|
||||
const double filament_shrinkage_compensation_z = m_config.filament_shrinkage_compensation_z.get_at(extruders.front());
|
||||
|
||||
for (unsigned int extruder : extruders) {
|
||||
if (filament_shrinkage_compensation_xy != m_config.filament_shrink.get_at(extruder) ||
|
||||
filament_shrinkage_compensation_z != m_config.filament_shrinkage_compensation_z.get_at(extruder)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Orca: Implement prusa's filament shrink compensation approach, but amended so 100% from the user is the equivalent to 0 in orca.
|
||||
// Returns scaling for each axis representing shrinkage compensations in each axis.
|
||||
Vec3d Print::shrinkage_compensation() const
|
||||
{
|
||||
if (!this->has_same_shrinkage_compensations())
|
||||
return Vec3d::Ones();
|
||||
|
||||
const unsigned int first_extruder = this->extruders().front();
|
||||
|
||||
const double xy_shrinkage_percent = m_config.filament_shrink.get_at(first_extruder);
|
||||
const double z_shrinkage_percent = m_config.filament_shrinkage_compensation_z.get_at(first_extruder);
|
||||
|
||||
const double xy_compensation = 100.0 / xy_shrinkage_percent;
|
||||
const double z_compensation = 100.0 / z_shrinkage_percent;
|
||||
|
||||
return { xy_compensation, xy_compensation, z_compensation };
|
||||
}
|
||||
|
||||
const std::string PrintStatistics::FilamentUsedG = "filament used [g]";
|
||||
const std::string PrintStatistics::FilamentUsedGMask = "; filament used [g] =";
|
||||
|
||||
|
||||
+20
-8
@@ -249,9 +249,9 @@ struct PrintInstance
|
||||
// SoftFever
|
||||
//
|
||||
// instance id
|
||||
size_t id;
|
||||
size_t id{0};
|
||||
// Orca: unique id used by marlin/rrf cancel object feature
|
||||
size_t unique_id;
|
||||
size_t unique_id{0};
|
||||
|
||||
//BBS: instance_shift is too large because of multi-plate, apply without plate offset.
|
||||
Point shift_without_plate_offset() const;
|
||||
@@ -503,9 +503,12 @@ public:
|
||||
std::vector<std::reference_wrapper<const PrintRegion>> all_regions() const;
|
||||
const PrintObjectRegions* shared_regions() const throw() { return m_shared_regions; }
|
||||
|
||||
bool has_support() const { return m_config.enable_support || m_config.enforce_support_layers > 0; }
|
||||
bool has_raft() const { return m_config.raft_layers > 0; }
|
||||
bool has_support_material() const { return this->has_support() || this->has_raft(); }
|
||||
static bool has_support(const PrintObjectConfig &config) { return config.enable_support || config.enforce_support_layers > 0; }
|
||||
static bool has_raft(const PrintObjectConfig &config) { return config.raft_layers > 0; }
|
||||
static bool has_support_material(const PrintObjectConfig &config) { return has_support(config) || has_raft(config); }
|
||||
bool has_support() const { return has_support(m_config); }
|
||||
bool has_raft() const { return has_raft(m_config); }
|
||||
bool has_support_material() const { return has_support_material(m_config); }
|
||||
// Checks if the model object is painted using the multi-material painting gizmo.
|
||||
bool is_mm_painted() const { return this->model_object()->is_mm_painted(); }
|
||||
// Checks if the model object is painted using the fuzzy skin painting gizmo.
|
||||
@@ -735,7 +738,7 @@ private:
|
||||
// SoftFever
|
||||
//
|
||||
// object id
|
||||
size_t m_id;
|
||||
size_t m_id{0};
|
||||
void apply_conical_overhang();
|
||||
|
||||
public:
|
||||
@@ -1385,10 +1388,11 @@ public:
|
||||
size_t config_index_generation() const { return m_config_index_generation; }
|
||||
|
||||
// Orca: Implement prusa's filament shrink compensation approach
|
||||
// Both are decided by apply() from the filaments the model uses, before the PrintObject transformations are built.
|
||||
// Returns if all used filaments have same shrinkage compensations.
|
||||
bool has_same_shrinkage_compensations() const;
|
||||
bool has_same_shrinkage_compensations() const { return m_same_shrinkage_compensations; }
|
||||
// Returns scaling for each axis representing shrinkage compensations in each axis.
|
||||
Vec3d shrinkage_compensation() const;
|
||||
Vec3d shrinkage_compensation() const { return m_shrinkage_compensation; }
|
||||
|
||||
std::tuple<float, float> object_skirt_offset(double margin_height = 0) const;
|
||||
|
||||
@@ -1484,11 +1488,19 @@ private:
|
||||
s_slicing_pipeline_hook_fn(*this, object, step);
|
||||
}
|
||||
|
||||
static void append_model_object_extruders(const ModelObject &mo, std::vector<unsigned int> &extruders);
|
||||
static bool append_support_extruders(const PrintObjectConfig &config, unsigned int num_extruders, std::vector<unsigned int> &extruders);
|
||||
void append_tool_change_extruders(std::vector<unsigned int> &extruders) const;
|
||||
void append_wipe_tower_extruder(std::vector<unsigned int> &extruders) const;
|
||||
void update_shrinkage_compensation(size_t num_extruders, std::vector<int> &variant_index);
|
||||
|
||||
PrintConfig m_config;
|
||||
PrintObjectConfig m_default_object_config;
|
||||
PrintRegionConfig m_default_region_config;
|
||||
PrintObjectPtrs m_objects;
|
||||
PrintRegionPtrs m_print_regions;
|
||||
bool m_same_shrinkage_compensations { false };
|
||||
Vec3d m_shrinkage_compensation { Vec3d::Ones() };
|
||||
|
||||
//SoftFever
|
||||
bool m_isBBLPrinter = false;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <set>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
@@ -1257,6 +1258,80 @@ static PrintObjectRegions* generate_print_object_regions(
|
||||
return out.release();
|
||||
}
|
||||
|
||||
// Orca: The shrinkage compensation is baked into the PrintObject transformations, so it has to be known before apply()
|
||||
// creates them. Derive it from the filaments the new model prints with instead of from the existing PrintObjects and
|
||||
// their regions, which do not exist yet on the first apply(). Every feature filament of every part, height range and
|
||||
// modifier counts, whether the feature ends up printing or not, so filaments that disagree switch the compensation
|
||||
// off rather than letting a wrong compensation through.
|
||||
void Print::update_shrinkage_compensation(size_t num_extruders, std::vector<int> &variant_index)
|
||||
{
|
||||
const auto set_compensation = [this](unsigned int extruder) {
|
||||
// 100% from the user is the equivalent to 0 in orca.
|
||||
const double xy_compensation = 100.0 / m_config.filament_shrink.get_at(extruder);
|
||||
const double z_compensation = 100.0 / m_config.filament_shrinkage_compensation_z.get_at(extruder);
|
||||
m_shrinkage_compensation = { xy_compensation, xy_compensation, z_compensation };
|
||||
};
|
||||
const auto same_shrinkage = [this](unsigned int a, unsigned int b) {
|
||||
return m_config.filament_shrink.get_at(a) == m_config.filament_shrink.get_at(b) &&
|
||||
m_config.filament_shrinkage_compensation_z.get_at(a) == m_config.filament_shrinkage_compensation_z.get_at(b);
|
||||
};
|
||||
// Usually all filaments share the same shrinkage, then it does not matter which of them print.
|
||||
bool uniform = true;
|
||||
for (unsigned int extruder = 1; extruder < num_extruders && uniform; ++ extruder)
|
||||
uniform = same_shrinkage(0, extruder);
|
||||
if (uniform) {
|
||||
m_same_shrinkage_compensations = true;
|
||||
set_compensation(0);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<unsigned int> extruders;
|
||||
const auto append_feature_extruders = [num_extruders, &extruders](const PrintRegionConfig &config) {
|
||||
for (int id : { config.outer_wall_filament_id.value, config.inner_wall_filament_id.value, config.sparse_infill_filament_id.value,
|
||||
config.internal_solid_filament_id.value, config.top_surface_filament_id.value, config.bottom_surface_filament_id.value }) {
|
||||
const unsigned int i = (unsigned int)std::max(0, id - 1);
|
||||
extruders.emplace_back(i >= num_extruders ? 0 : i);
|
||||
}
|
||||
};
|
||||
for (const ModelObject *model_object : m_model.objects) {
|
||||
if (std::none_of(model_object->instances.begin(), model_object->instances.end(), [](const ModelInstance *mi) { return mi->is_printable(); }))
|
||||
continue;
|
||||
std::vector<const ModelVolume*> modifiers;
|
||||
std::copy_if(model_object->volumes.begin(), model_object->volumes.end(), std::back_inserter(modifiers), [](const ModelVolume *mv) { return mv->is_modifier(); });
|
||||
for (const ModelVolume *part : model_object->volumes) {
|
||||
if (! part->is_model_part())
|
||||
continue;
|
||||
std::vector<PrintRegionConfig> parents { region_config_from_model_volume(m_default_region_config, nullptr, *part, num_extruders, variant_index) };
|
||||
for (const auto &[range, range_config] : model_object->layer_config_ranges)
|
||||
parents.emplace_back(region_config_from_model_volume(m_default_region_config, &range_config.get(), *part, num_extruders, variant_index));
|
||||
for (const PrintRegionConfig &parent : parents) {
|
||||
append_feature_extruders(parent);
|
||||
for (const ModelVolume *modifier : modifiers)
|
||||
append_feature_extruders(region_config_from_model_volume(parent, nullptr, *modifier, num_extruders, variant_index));
|
||||
}
|
||||
}
|
||||
append_model_object_extruders(*model_object, extruders);
|
||||
const PrintObjectConfig object_config = PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders, variant_index);
|
||||
if (PrintObject::has_support_material(object_config))
|
||||
append_support_extruders(object_config, (unsigned int)num_extruders, extruders);
|
||||
}
|
||||
this->append_tool_change_extruders(extruders);
|
||||
// A mixed slot prints with its physical components.
|
||||
const auto expand_and_sort = [this, &extruders]() {
|
||||
extruders = expand_mixed_filaments(extruders, m_config.filament_is_mixed.values, m_config.filament_mixed_components.values);
|
||||
};
|
||||
expand_and_sort();
|
||||
// The wipe tower filament may also be flushed into the objects.
|
||||
this->append_wipe_tower_extruder(extruders);
|
||||
expand_and_sort();
|
||||
|
||||
m_same_shrinkage_compensations = ! extruders.empty() &&
|
||||
std::all_of(extruders.begin(), extruders.end(), [&](unsigned int extruder) { return same_shrinkage(extruders.front(), extruder); });
|
||||
m_shrinkage_compensation = Vec3d::Ones();
|
||||
if (m_same_shrinkage_compensations)
|
||||
set_compensation(extruders.front());
|
||||
}
|
||||
|
||||
Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config, bool extruder_applied)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
@@ -1864,6 +1939,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
|
||||
// 4) Generate PrintObjects from ModelObjects and their instances.
|
||||
{
|
||||
this->update_shrinkage_compensation(num_extruders, print_variant_index);
|
||||
PrintObjectPtrs print_objects_new;
|
||||
print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size()));
|
||||
bool new_objects = false;
|
||||
|
||||
@@ -1656,7 +1656,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->tooltip = L("Line width of the Bridge. If expressed as a %, it will be computed over the nozzle diameter.\n"
|
||||
"Recommended to use with a higher Bridge density or Bridge flow ratio.\n\n"
|
||||
"The maximum value is 100% or the nozzle diameter.\n"
|
||||
"If set to 0, the line width will match the Internal solid infill width.");
|
||||
"If set to 0, the line width will match the Internal solid infill width, or the nozzle diameter for thick bridges.");
|
||||
def->sidetext = L("mm or %");
|
||||
def->ratio_over = "nozzle_diameter";
|
||||
def->min = 0;
|
||||
@@ -1919,7 +1919,7 @@ void PrintConfigDef::init_fff_params()
|
||||
"3. If artifacts still appear, enable Extrusion Rate Smoothing (ERS) to further smooth the flow transitions.\n\n"
|
||||
"Note: When this option is enabled, overhang perimeters are treated like overhangs, meaning the overhang speed is "
|
||||
"applied even if the overhanging perimeter is part of a bridge.\n"
|
||||
"For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 100% overhang speed will be applied.");
|
||||
"For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 75% overhang speed will be applied, limited by the bridge speed.");
|
||||
def->mode = comAdvanced;
|
||||
def->nullable = true;
|
||||
def->set_default_value(new ConfigOptionBoolsNullable{ false });
|
||||
@@ -1980,7 +1980,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("External");
|
||||
def->category = L("Speed");
|
||||
def->tooltip = L("Speed of the externally visible bridge extrusions.\n\n"
|
||||
"In addition, if Slow down for curled perimeters is disabled or Classic overhang mode is enabled, "
|
||||
"In addition, if Slow down for curled perimeters is disabled, "
|
||||
"it will be the print speed of overhang walls that are supported by less than 13%, "
|
||||
"whether they are part of a bridge or an overhang.");
|
||||
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
|
||||
@@ -2650,7 +2650,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->category = L("Speed");
|
||||
def->tooltip = L("This separate setting will affect the speed of perimeters having radius <= small_perimeter_threshold "
|
||||
"(usually holes). If expressed as percentage (for example: 80%) it will be calculated "
|
||||
"on the outer wall speed setting above. Set to zero for auto.");
|
||||
"on the outer wall speed setting above.");
|
||||
def->sidetext = L("mm/s or %");
|
||||
def->ratio_over = "outer_wall_speed";
|
||||
def->min = 1;
|
||||
@@ -2673,10 +2673,9 @@ void PrintConfigDef::init_fff_params()
|
||||
def->category = L("Speed");
|
||||
def->tooltip = L("Same as \"Small perimeters\", but for supports. "
|
||||
"This separate setting will affect the speed of support for areas <= `small_support_perimeter_threshold`. "
|
||||
"If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above. "
|
||||
"Set to zero for auto.");
|
||||
"If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above.");
|
||||
def->sidetext = L("mm/s or %");
|
||||
def->ratio_over = "outer_wall_speed";
|
||||
def->ratio_over = "support_speed";
|
||||
def->min = 1;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloatsOrPercentsNullable{FloatOrPercent(50, true)});
|
||||
@@ -2700,7 +2699,7 @@ void PrintConfigDef::init_fff_params()
|
||||
"However, overhang performance will reduce as there is no internal perimeter to print the external wall against. "
|
||||
"This option requires a minimum of 3 walls to be effective as it prints the internal walls from the 3rd perimeter onwards first, "
|
||||
"then the external perimeter and, finally, the first internal perimeter. "
|
||||
"This option is recommended against the Outer/Inner option in most cases.\n\n"
|
||||
"This option is recommended over the Outer/Inner option in most cases.\n\n"
|
||||
"Use Outer/Inner for the same external wall quality and dimensional accuracy benefits of Inner/Outer/Inner option. "
|
||||
"However, the Z seams will appear less consistent as the first extrusion of a new layer starts on a visible surface.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<WallSequence>::get_enum_values();
|
||||
@@ -4012,7 +4011,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("initial_layer_speed", coFloats);
|
||||
def->label = L("First layer");
|
||||
def->tooltip = L("This is the speed for the first layer except for solid infill sections.");
|
||||
def->tooltip = L("This is the speed for the walls and brim of the first layer.");
|
||||
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
|
||||
def->min = 1;
|
||||
def->mode = comAdvanced;
|
||||
@@ -4021,7 +4020,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("initial_layer_infill_speed", coFloats);
|
||||
def->label = L("First layer infill");
|
||||
def->tooltip = L("This is the speed for solid infill parts of the first layer.");
|
||||
def->tooltip = L("This is the speed for the first layer except for walls and brim, such as infill, gap fill and support.");
|
||||
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
|
||||
def->min = 1;
|
||||
def->mode = comAdvanced;
|
||||
@@ -4935,7 +4934,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Infill/wall overlap");
|
||||
def->category = L("Strength");
|
||||
// xgettext:no-c-format, no-boost-format
|
||||
def->tooltip = L("This allows the infill area to be enlarged slightly to overlap with walls for better bonding. The percentage value is relative to line width of sparse infill. Set this value to ~10-15% to minimize potential over extrusion and accumulation of material resulting in rough top surfaces.");
|
||||
def->tooltip = L("This allows the infill area to be enlarged slightly to overlap with walls for better bonding. The percentage value is relative to the inner wall line width. Set this value to ~10-15% to minimize potential over extrusion and accumulation of material resulting in rough top surfaces.");
|
||||
def->sidetext = "%";
|
||||
def->ratio_over = "inner_wall_line_width";
|
||||
def->mode = comAdvanced;
|
||||
@@ -4948,7 +4947,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->tooltip = L("Top solid infill area is enlarged slightly to overlap with wall for better bonding "
|
||||
"and to minimize the appearance of pinholes where the top infill meets the walls. "
|
||||
"A value of 25-30% is a good starting point, minimizing the appearance of pinholes. "
|
||||
"The percentage value is relative to line width of sparse infill.");
|
||||
"The percentage value is relative to the inner wall line width.");
|
||||
def->sidetext = "%";
|
||||
def->ratio_over = "inner_wall_line_width";
|
||||
def->mode = comAdvanced;
|
||||
@@ -4998,9 +4997,9 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("mmu_segmented_region_interlocking_depth", coFloat);
|
||||
def->label = L("Interlocking depth of a segmented region");
|
||||
def->tooltip = L("Interlocking depth of a segmented region. It will be ignored if "
|
||||
"\"mmu_segmented_region_max_width\" is zero or if \"mmu_segmented_region_interlocking_depth\" "
|
||||
"is bigger than \"mmu_segmented_region_max_width\". Zero disables this feature.");
|
||||
def->tooltip = L("Interlocking depth of a segmented region. On every other layer, the segmented region "
|
||||
"is limited to this depth. It will be ignored if \"interlocking_beam\" is enabled. "
|
||||
"Zero disables this feature.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->min = 0;
|
||||
def->category = L("Advanced");
|
||||
@@ -5168,7 +5167,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Minimize wall height angle");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Reduce the height of top-surface perimeters to match the model edge height.\n"
|
||||
"Affects perimeters with a slope less than this angle (degrees).\n"
|
||||
"Affects perimeters with a slope greater than this angle (degrees).\n"
|
||||
"A reasonable value is 35. Set to 0 to disable.");
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->min = 0;
|
||||
@@ -6560,7 +6559,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("role_based_wipe_speed", coBool);
|
||||
def->label = L("Role base wipe speed");
|
||||
def->label = L("Role based wipe speed");
|
||||
def->category = L("Speed");
|
||||
def->tooltip = L("The wipe speed is determined by the speed of the current extrusion role. "
|
||||
"e.g. if a wipe action is executed immediately following an outer wall extrusion, the speed of the outer wall extrusion will be utilized for the wipe action.");
|
||||
@@ -6664,9 +6663,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Draft shield");
|
||||
def->tooltip = L("A draft shield is useful to protect an ABS or ASA print from warping and detaching from print bed due to wind draft. "
|
||||
"It is usually needed only with open frame printers, i.e. without an enclosure.\n\n"
|
||||
"Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.\n"
|
||||
"Note: With the draft shield active, the skirt will be printed at skirt distance from the object. "
|
||||
"Therefore, if brims are active it may intersect with them. To avoid this, increase the skirt distance value.\n");
|
||||
"Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<DraftShield>::get_enum_values();
|
||||
def->enum_values.push_back("disabled");
|
||||
def->enum_values.push_back("enabled");
|
||||
@@ -8274,10 +8271,10 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Maximum wipe tower print speed");
|
||||
def->tooltip = L("The maximum print speed when purging in the wipe tower and printing the wipe tower sparse layers. "
|
||||
"When purging, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n\n"
|
||||
"When printing the sparse layers, if the internal perimeter speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n\n"
|
||||
"When printing the sparse layers, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n\n"
|
||||
"Increasing this speed may affect the tower's stability as well as increase the force with which the nozzle collides with any blobs that may have formed on the wipe tower.\n\n"
|
||||
"Before increasing this parameter beyond the default of 90 mm/s, make sure your printer can reliably bridge at the increased speeds and that ooze when tool changing is well controlled.\n\n"
|
||||
"For the wipe tower external perimeters the internal perimeter speed is used regardless of this setting.");
|
||||
"For the wipe tower external perimeters the internal perimeter speed is used, also limited by this setting.");
|
||||
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
|
||||
def->mode = comAdvanced;
|
||||
def->min = 10;
|
||||
@@ -8286,9 +8283,9 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("wipe_tower_wall_type", coEnum);
|
||||
def->label = L("Wall type");
|
||||
def->tooltip = L("Wipe tower outer wall type.\n"
|
||||
"1. Rectangle: The default wall type, a rectangle with fixed width and height.\n"
|
||||
"1. Rectangle: A rectangle with fixed width and height.\n"
|
||||
"2. Cone: A cone with a fillet at the bottom to help stabilize the wipe tower.\n"
|
||||
"3. Rib: Adds four ribs to the tower wall for enhanced stability.");
|
||||
"3. Rib: The default wall type, which adds four ribs to the tower wall for enhanced stability.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<WipeTowerWallType>::get_enum_values();
|
||||
def->enum_values.emplace_back("rectangle");
|
||||
def->enum_values.emplace_back("cone");
|
||||
@@ -8419,7 +8416,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("wipe_tower_extra_flow", coPercent);
|
||||
def->label = L("Extra flow for purging");
|
||||
def->tooltip = L("Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker or narrower "
|
||||
def->tooltip = L("Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker "
|
||||
"than they normally would be. The spacing is adjusted automatically.");
|
||||
def->sidetext = "%";
|
||||
def->mode = comAdvanced;
|
||||
@@ -8596,11 +8593,10 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Minimum wall length");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Adjust this value to prevent short, unclosed walls from being printed, which could increase print time. "
|
||||
"Higher values remove more and longer walls.\n\n"
|
||||
"The value is a multiple of the wall width. Higher values remove more and longer walls.\n\n"
|
||||
"NOTE: Bottom and top surfaces will not be affected by this value to prevent visual gaps on the outside of the model. "
|
||||
"Adjust 'One wall threshold' in the Advanced settings below to adjust the sensitivity of what is considered a top-surface. "
|
||||
"Adjust 'One wall threshold' in the 'Walls and surfaces' settings below to adjust the sensitivity of what is considered a top-surface. "
|
||||
"'One wall threshold' is only visible if this setting is set above the default value of 0.5, or if single-wall top surfaces is enabled.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->mode = comAdvanced;
|
||||
def->min = 0.0;
|
||||
def->max = 25.0;
|
||||
@@ -13044,6 +13040,13 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def->cli_params = "dir";
|
||||
def->set_default_value(new ConfigOptionString());
|
||||
|
||||
def = this->add("export_statistics", coBool);
|
||||
def->label = L("Export statistics");
|
||||
def->tooltip = L("Write the statistics of each sliced plate as JSON, as plate_N.statistics.json next to its G-code in the output "
|
||||
"directory and inside the 3MF of --export-3mf: print time, filament and feature totals, and the same per layer "
|
||||
"and per object.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("debug", coInt);
|
||||
def->label = L("Debug level");
|
||||
def->tooltip = L("Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:trace\n");
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
#define CLI_OBJECT_ORIENT_FAILED -22
|
||||
#define CLI_MODIFIED_PARAMS_TO_PRINTER -23
|
||||
#define CLI_FILE_VERSION_NOT_SUPPORTED -24
|
||||
#define CLI_EXPORT_STATISTICS_ERROR -25
|
||||
|
||||
|
||||
#define CLI_NO_SUITABLE_OBJECTS -50
|
||||
|
||||
@@ -20,6 +20,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Widgets/FilamentLoad.hpp
|
||||
GUI/AboutDialog.cpp
|
||||
GUI/AboutDialog.hpp
|
||||
GUI/AnimatedTexture.cpp
|
||||
GUI/AnimatedTexture.hpp
|
||||
GUI/AmsMappingPopup.cpp
|
||||
GUI/AmsMappingPopupUpdate.cpp
|
||||
GUI/AmsMappingPopup.hpp
|
||||
|
||||
@@ -297,6 +297,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
|
||||
, printable(true)
|
||||
, visible(true)
|
||||
, is_active(true)
|
||||
, on_hidden_plate(false)
|
||||
, zoom_to_volumes(true)
|
||||
, shader_outside_printer_detection_enabled(false)
|
||||
, is_outside(false)
|
||||
@@ -824,6 +825,8 @@ bool GLVolume::is_below_printbed() const
|
||||
|
||||
void GLVolume::render_sinking_contours()
|
||||
{
|
||||
if (!is_active)
|
||||
return;
|
||||
m_sinking_contours.render();
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ public:
|
||||
bool visible : 1;
|
||||
// Whether or not this volume is active for rendering
|
||||
bool is_active : 1;
|
||||
// Whether or not is_active was cleared because the other plates are hidden
|
||||
bool on_hidden_plate : 1;
|
||||
// Whether or not to use this volume when applying zoom_to_volumes()
|
||||
bool zoom_to_volumes : 1;
|
||||
// Wheter or not this volume is enabled for outside print volume detection in shader.
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
#include "AnimatedTexture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <wx/animdecod.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/gifdecod.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/log.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<unsigned char> read_file_bytes(const std::string &path)
|
||||
{
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
bool has_gif_signature(const std::vector<unsigned char> &bytes)
|
||||
{
|
||||
return bytes.size() > 6 && bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F';
|
||||
}
|
||||
|
||||
// wxImage keeps colour and alpha in two separate buffers, and a file with no alpha at all has none;
|
||||
// OpenGL wants the four channels interleaved.
|
||||
std::vector<unsigned char> image_to_rgba(const wxImage &image)
|
||||
{
|
||||
const size_t n = size_t(image.GetWidth()) * size_t(image.GetHeight());
|
||||
const unsigned char *rgb = image.GetData();
|
||||
const unsigned char *a = image.HasAlpha() ? image.GetAlpha() : nullptr;
|
||||
|
||||
std::vector<unsigned char> out(n * 4);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
out[i * 4 + 0] = rgb[i * 3 + 0];
|
||||
out[i * 4 + 1] = rgb[i * 3 + 1];
|
||||
out[i * 4 + 2] = rgb[i * 3 + 2];
|
||||
out[i * 4 + 3] = a != nullptr ? a[i] : 255;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// One decoded frame on its way to the GPU: the whole animation's area, already composited.
|
||||
struct DecodedFrame
|
||||
{
|
||||
std::vector<unsigned char> rgba;
|
||||
double delay; // seconds
|
||||
};
|
||||
|
||||
// Every frame of a GIF, composited onto the animation's own canvas.
|
||||
//
|
||||
// A GIF frame is a sub-rectangle that is drawn over what came before it, and the one before says how
|
||||
// to clean up after itself (leave it, erase it, put back what it covered). Without replaying that, an
|
||||
// animation that only re-sends the pixels that changed - which is most of them - draws as a flicker of
|
||||
// disconnected patches. wxGIFDecoder hands over the raw palette indices, which is what makes replaying
|
||||
// it here straightforward: a transparent index means "keep what is underneath", nothing else does.
|
||||
bool decode_gif(const std::vector<unsigned char> &bytes, int &width, int &height, std::vector<DecodedFrame> &out)
|
||||
{
|
||||
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
||||
wxGIFDecoder decoder;
|
||||
|
||||
// A truncated file still decodes everything up to the cut, which is worth showing.
|
||||
const wxGIFErrorCode err = decoder.LoadGIF(stream);
|
||||
if ((err != wxGIF_OK && err != wxGIF_TRUNCATED) || decoder.GetFrameCount() == 0)
|
||||
return false;
|
||||
|
||||
const wxSize size = decoder.GetAnimationSize();
|
||||
width = size.GetWidth();
|
||||
height = size.GetHeight();
|
||||
if (width <= 0 || height <= 0)
|
||||
return false;
|
||||
|
||||
std::vector<unsigned char> canvas(size_t(width) * size_t(height) * 4, 0);
|
||||
std::vector<unsigned char> saved;
|
||||
|
||||
for (unsigned int i = 0; i < decoder.GetFrameCount(); ++i) {
|
||||
const wxSize frame_size = decoder.GetFrameSize(i);
|
||||
const wxPoint frame_pos = decoder.GetFramePosition(i);
|
||||
const wxAnimationDisposal disposal = decoder.GetDisposalMethod(i);
|
||||
const unsigned char *indices = decoder.GetData(i);
|
||||
const unsigned char *palette = decoder.GetPalette(i);
|
||||
const int transparent = decoder.GetTransparentColourIndex(i);
|
||||
|
||||
if (disposal == wxANIM_TOPREVIOUS)
|
||||
saved = canvas;
|
||||
|
||||
if (indices != nullptr && palette != nullptr)
|
||||
for (int y = 0; y < frame_size.GetHeight(); ++y) {
|
||||
const int cy = frame_pos.y + y;
|
||||
if (cy < 0 || cy >= height)
|
||||
continue;
|
||||
for (int x = 0; x < frame_size.GetWidth(); ++x) {
|
||||
const int cx = frame_pos.x + x;
|
||||
if (cx < 0 || cx >= width)
|
||||
continue;
|
||||
const unsigned char index = indices[size_t(y) * size_t(frame_size.GetWidth()) + size_t(x)];
|
||||
if (int(index) == transparent)
|
||||
continue;
|
||||
unsigned char *dst = &canvas[(size_t(cy) * size_t(width) + size_t(cx)) * 4];
|
||||
dst[0] = palette[3 * size_t(index) + 0];
|
||||
dst[1] = palette[3 * size_t(index) + 1];
|
||||
dst[2] = palette[3 * size_t(index) + 2];
|
||||
dst[3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// Browsers treat 0 and 1 hundredths of a second as "as fast as the file can mean", and settle
|
||||
// on 100 ms; an animation authored against that convention plays at the intended speed only if
|
||||
// this does the same.
|
||||
const long delay_ms = decoder.GetDelay(i);
|
||||
out.push_back({ canvas, (delay_ms <= 10 ? 100.0 : double(delay_ms)) / 1000.0 });
|
||||
|
||||
if (disposal == wxANIM_TOBACKGROUND) {
|
||||
for (int y = 0; y < frame_size.GetHeight(); ++y) {
|
||||
const int cy = frame_pos.y + y;
|
||||
if (cy < 0 || cy >= height)
|
||||
continue;
|
||||
const int x0 = std::max(0, frame_pos.x);
|
||||
const int x1 = std::min(width, frame_pos.x + frame_size.GetWidth());
|
||||
if (x1 > x0)
|
||||
std::fill_n(&canvas[(size_t(cy) * size_t(width) + size_t(x0)) * 4], size_t(x1 - x0) * 4, 0);
|
||||
}
|
||||
} else if (disposal == wxANIM_TOPREVIOUS) {
|
||||
canvas = saved;
|
||||
}
|
||||
}
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AnimatedTexture::load_from_file(const std::string &path)
|
||||
{
|
||||
reset();
|
||||
|
||||
boost::system::error_code ec;
|
||||
if (!boost::filesystem::is_regular_file(path, ec))
|
||||
return false;
|
||||
|
||||
const std::vector<unsigned char> bytes = read_file_bytes(path);
|
||||
if (bytes.empty())
|
||||
return false;
|
||||
|
||||
std::vector<DecodedFrame> frames;
|
||||
int width = 0, height = 0;
|
||||
|
||||
if (has_gif_signature(bytes)) {
|
||||
if (!decode_gif(bytes, width, height, frames)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot decode GIF " << path;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
wxLogNull no_log; // a format wxWidgets cannot read is handled here, not in a dialog
|
||||
wxImage image;
|
||||
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
||||
if (!image.LoadFile(stream, wxBITMAP_TYPE_ANY) || !image.IsOk()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot decode image " << path;
|
||||
return false;
|
||||
}
|
||||
width = image.GetWidth();
|
||||
height = image.GetHeight();
|
||||
frames.push_back({ image_to_rgba(image), 0.0 });
|
||||
}
|
||||
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
m_frames.reserve(frames.size());
|
||||
for (DecodedFrame &frame : frames) {
|
||||
auto texture = std::make_unique<GLTexture>();
|
||||
// Mipmapped: a hint is drawn at whatever width the panel has room for, which is usually
|
||||
// smaller than the source, and an unfiltered downscale of a diagram is all aliasing.
|
||||
if (!texture->load_from_raw_data(std::move(frame.rgba), (unsigned int) width, (unsigned int) height, false,
|
||||
/* use_mipmaps */ true)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot upload " << path;
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
m_frames.push_back({ std::move(texture), m_duration, frame.delay });
|
||||
m_duration += frame.delay;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimatedTexture::reset()
|
||||
{
|
||||
m_frames.clear();
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
m_duration = 0.;
|
||||
}
|
||||
|
||||
unsigned int AnimatedTexture::texture_id(double time) const
|
||||
{
|
||||
if (m_frames.empty())
|
||||
return 0;
|
||||
if (m_frames.size() == 1 || m_duration <= 0.)
|
||||
return m_frames.front().texture->get_id();
|
||||
|
||||
const double t = time - m_duration * std::floor(time / m_duration);
|
||||
// Frames are few; a linear walk beats keeping an index around and getting it stale.
|
||||
size_t i = 0;
|
||||
while (i + 1 < m_frames.size() && t >= m_frames[i].start + m_frames[i].delay)
|
||||
++i;
|
||||
return m_frames[i].texture->get_id();
|
||||
}
|
||||
|
||||
const AnimatedTexture *AnimatedTextureCache::get(const std::string &path)
|
||||
{
|
||||
const auto it = m_items.find(path);
|
||||
if (it != m_items.end())
|
||||
return it->second.get();
|
||||
|
||||
auto texture = std::make_unique<AnimatedTexture>();
|
||||
if (!texture->load_from_file(path))
|
||||
texture.reset(); // remembered as a failure, so the file is not re-read every frame
|
||||
|
||||
const AnimatedTexture *result = texture.get();
|
||||
m_items.emplace(path, std::move(texture));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef slic3r_AnimatedTexture_hpp_
|
||||
#define slic3r_AnimatedTexture_hpp_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// An illustration for the UI to draw: one still image, or the frames of an animated GIF together with
|
||||
// the delays the file asks for. Decoded on the CPU once and uploaded as one GL texture per frame.
|
||||
//
|
||||
// A texture per frame rather than an atlas: these are small hints a few hundred pixels across and a
|
||||
// handful of frames long, so the extra textures cost little, while drawing a frame stays a plain
|
||||
// ImGui::Image with no UV arithmetic and no dependence on the maximum texture size.
|
||||
//
|
||||
// load_from_file() uploads to the GPU, so it needs a current GL context - call it from rendering code.
|
||||
class AnimatedTexture
|
||||
{
|
||||
public:
|
||||
AnimatedTexture() = default;
|
||||
AnimatedTexture(AnimatedTexture &&) = default;
|
||||
AnimatedTexture &operator=(AnimatedTexture &&) = default;
|
||||
AnimatedTexture(const AnimatedTexture &) = delete;
|
||||
AnimatedTexture &operator=(const AnimatedTexture &) = delete;
|
||||
|
||||
// PNG, JPG and BMP through wxImage; GIF, animated or not, through wxGIFDecoder. False, and nothing
|
||||
// loaded, if the file is missing or cannot be decoded.
|
||||
bool load_from_file(const std::string &path);
|
||||
void reset();
|
||||
|
||||
bool is_valid() const { return !m_frames.empty(); }
|
||||
bool is_animated() const { return m_frames.size() > 1; }
|
||||
int width() const { return m_width; }
|
||||
int height() const { return m_height; }
|
||||
// Length of one loop, in seconds; 0 for a still image.
|
||||
double duration() const { return m_duration; }
|
||||
|
||||
// The texture to draw `time` seconds into the loop, which wraps. 0 when nothing is loaded.
|
||||
unsigned int texture_id(double time) const;
|
||||
|
||||
private:
|
||||
struct Frame
|
||||
{
|
||||
std::unique_ptr<GLTexture> texture;
|
||||
double start{0.}; // seconds into the loop this frame appears at
|
||||
double delay{0.}; // seconds it stays up
|
||||
};
|
||||
|
||||
std::vector<Frame> m_frames;
|
||||
int m_width{0};
|
||||
int m_height{0};
|
||||
double m_duration{0.};
|
||||
};
|
||||
|
||||
// The illustrations a panel has shown at least once, keyed by absolute path, decoded and uploaded on
|
||||
// first use and kept until clear() - which has to run while the GL context is still current.
|
||||
class AnimatedTextureCache
|
||||
{
|
||||
public:
|
||||
// Null when the file is missing or cannot be decoded. The failure is remembered too, so a tooltip
|
||||
// whose illustration has not been drawn yet costs one lookup, not one decode attempt per frame.
|
||||
const AnimatedTexture *get(const std::string &path);
|
||||
void clear() { m_items.clear(); }
|
||||
|
||||
private:
|
||||
std::map<std::string, std::unique_ptr<AnimatedTexture>> m_items; // null value: tried once, failed
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_AnimatedTexture_hpp_
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -57,6 +58,7 @@
|
||||
#include "libslic3r_version.h"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/Utils/PrintHost.hpp"
|
||||
@@ -781,6 +783,7 @@ void BackgroundSlicingProcess::schedule_export(const std::string& path, bool exp
|
||||
this->invalidate_step(bspsGCodeFinalize);
|
||||
m_export_path = path;
|
||||
m_export_path_on_removable_media = export_path_on_removable_media;
|
||||
m_export_statistics = GUI::wxGetApp().app_config->get_bool("export_statistics");
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::schedule_upload(Slic3r::PrintHostJob upload_job)
|
||||
@@ -906,6 +909,8 @@ void BackgroundSlicingProcess::finalize_gcode()
|
||||
BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int) copy_ret_val << ") durring copy_file() to " << export_path << ".";
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
m_print->set_status(100, GUI::format(_L("G-code file exported to %1%"), export_path));
|
||||
}
|
||||
@@ -953,6 +958,8 @@ void BackgroundSlicingProcess::export_gcode()
|
||||
// throw Slic3r::ExportError(_utf8(L("Unknown error when exporting G-code.")));
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
// BBS
|
||||
auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
|
||||
|
||||
@@ -258,6 +258,8 @@ private:
|
||||
// but once set, it cannot be re-set.
|
||||
std::string m_export_path;
|
||||
bool m_export_path_on_removable_media = false;
|
||||
// Read from the preferences when the export is scheduled, as the background thread may not read them.
|
||||
bool m_export_statistics = false;
|
||||
// Print host upload job to schedule after slicing is complete, used by schedule_upload(),
|
||||
// empty by default (ie. no upload to schedule)
|
||||
PrintHostJob m_upload_job;
|
||||
|
||||
+247
-35
@@ -1679,6 +1679,7 @@ GLCanvas3D::~GLCanvas3D()
|
||||
|
||||
m_sel_plate_toolbar.del_all_item();
|
||||
m_sel_plate_toolbar.del_stats_item();
|
||||
m_plate_switcher_items.clear();
|
||||
}
|
||||
|
||||
void GLCanvas3D::post_event(wxEvent &&event)
|
||||
@@ -2360,6 +2361,8 @@ void GLCanvas3D::zoom_to_gcode()
|
||||
void GLCanvas3D::zoom_to_plate(int plate_idx)
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE && _hides_inactive_plates())
|
||||
plate_idx = REQUIRES_ZOOM_TO_CUR_PLATE;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE) {
|
||||
box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
|
||||
box.min.z() = 0.0;
|
||||
@@ -2416,12 +2419,22 @@ float GLCanvas3D::get_collapse_toolbar_height() const
|
||||
}
|
||||
|
||||
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
|
||||
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar,
|
||||
// get_canvas_toolbar_right and canvas_corner_height, so an overlay kept clear of the corner follows its layout.
|
||||
static constexpr float NAVIGATOR_SIZE = 128.f;
|
||||
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
|
||||
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
|
||||
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
|
||||
static constexpr float CANVAS_TOOLBAR_SPACING = 6.f;
|
||||
|
||||
// What the corner takes at the left edge of the canvas: the 3D navigator, or else the canvas toolbar.
|
||||
static float canvas_corner_height()
|
||||
{
|
||||
if (wxGetApp().show_3d_navigator())
|
||||
return NAVIGATOR_SIZE;
|
||||
const int buttons = wxGetApp().show_canvas_zoom_button() ? 3 : 2;
|
||||
return CANVAS_TOOLBAR_MARGIN + 2.f * CANVAS_TOOLBAR_PADDING + buttons * CANVAS_TOOLBAR_BUTTON + (buttons - 1) * CANVAS_TOOLBAR_SPACING;
|
||||
}
|
||||
|
||||
float GLCanvas3D::get_canvas_toolbar_right() const
|
||||
{
|
||||
@@ -2546,6 +2559,9 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
||||
|
||||
wxGetApp().imgui()->new_frame();
|
||||
|
||||
if (_update_inactive_plates_visibility())
|
||||
scene_dirty = true;
|
||||
|
||||
if (m_picking_enabled && !m_benchmarking) {
|
||||
if (m_rectangle_selection.is_dragging())
|
||||
// picking pass using rectangle selection
|
||||
@@ -4978,7 +4994,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
// Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected
|
||||
// during the scene manipulation.
|
||||
|
||||
if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
|
||||
if (m_picking_enabled && (m_gizmos.is_allow_multi_select_parts_or_objects() || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
|
||||
if (evt.LeftDown() && !m_hover_volume_idxs.empty()) {
|
||||
int volume_idx = get_first_hover_volume_idx();
|
||||
bool already_selected = m_selection.contains_volume(volume_idx);
|
||||
@@ -4988,7 +5004,18 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
|
||||
if (already_selected && ctrl_down)
|
||||
m_selection.remove(volume_idx);
|
||||
else if (alt_down && m_gizmos.is_allow_multi_select_parts_or_objects() && m_selection.get_mode() == Selection::Volume &&
|
||||
m_selection.get_object_idx() == m_volumes.volumes[volume_idx]->object_idx() &&
|
||||
m_selection.get_instance_idx() == m_volumes.volumes[volume_idx]->instance_idx()) {
|
||||
// Alt+click on another part of the selected parts' instance adds it, and on a selected
|
||||
// part removes it, unless it is the only one.
|
||||
if (!already_selected)
|
||||
m_selection.add_volumes(Selection::Volume, {(unsigned int) volume_idx}, false);
|
||||
else if (curr_idxs.size() > 1)
|
||||
m_selection.remove_volumes(Selection::Volume, {(unsigned int) volume_idx});
|
||||
}
|
||||
else if (alt_down) {
|
||||
// Select just the clicked part, also when an object is selected.
|
||||
Selection::EMode mode = Selection::Volume;
|
||||
if (already_selected) {
|
||||
std::vector<unsigned int> volume_idxs;
|
||||
@@ -5588,13 +5615,14 @@ void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_mov
|
||||
// parts moved. Notifying a plate about an instance that stayed put invalidates its slice
|
||||
// result, and notifying instance 0 alone left a moved copy unregistered on its new plate.
|
||||
{
|
||||
const bool parts_moved = force_volume_move || selection_mode == Selection::Volume;
|
||||
std::set<std::pair<int, int>> notified;
|
||||
for (unsigned int i : m_selection.get_volume_idxs()) {
|
||||
const GLVolume* v = m_volumes.volumes[i];
|
||||
const int object_idx = v->object_idx();
|
||||
if (object_idx < 0 || object_idx >= static_cast<int>(m_model->objects.size()))
|
||||
continue;
|
||||
const std::pair<int, int> key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx());
|
||||
const std::pair<int, int> key(object_idx, parts_moved ? -1 : v->instance_idx());
|
||||
if (notified.insert(key).second)
|
||||
m_selection.notify_instance_update(key.first, key.second);
|
||||
}
|
||||
@@ -7657,7 +7685,6 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
|
||||
|
||||
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
{
|
||||
bool result = true;
|
||||
if (!m_sel_plate_toolbar.is_enabled()) {
|
||||
return false;
|
||||
}
|
||||
@@ -7682,19 +7709,13 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
PartPlateList& plate_list = p_plater->get_partplate_list();
|
||||
for (int i = 0; i < plate_list.get_plate_count(); i++) {
|
||||
IMToolbarItem* item = new IMToolbarItem();
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
if (plate && plate->thumbnail_data.is_valid()) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
item->image_data = plate->thumbnail_data.pixels;
|
||||
item->image_width = plate->thumbnail_data.width;
|
||||
item->image_height = plate->thumbnail_data.height;
|
||||
result = item->generate_texture();
|
||||
}
|
||||
if (PartPlate* plate = plate_list.get_plate(i))
|
||||
item->set_thumbnail(plate->thumbnail_data);
|
||||
m_sel_plate_toolbar.m_items.push_back(item);
|
||||
}
|
||||
p_plater->clear_plate_toolbar_image_dirty();
|
||||
m_sel_plate_toolbar.is_display_scrollbar = false;
|
||||
return result;
|
||||
return true;
|
||||
}
|
||||
|
||||
//BBS: GUI refactor
|
||||
@@ -7962,8 +7983,8 @@ void GLCanvas3D::_picking_pass()
|
||||
if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) {
|
||||
const GLVolume* volume = m_volumes.volumes[hit.raycaster_id];
|
||||
if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) {
|
||||
// do not add the volume id if any gizmo is active and CTRL is pressed
|
||||
if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL))
|
||||
// do not add the volume id if CTRL is pressed while a gizmo that does not allow Ctrl+click selection is active
|
||||
if (m_gizmos.is_allow_multi_select_parts_or_objects() || !wxGetKeyState(WXK_CONTROL))
|
||||
m_hover_volume_idxs.emplace_back(hit.raycaster_id);
|
||||
m_gizmos.set_hover_id(-1);
|
||||
}
|
||||
@@ -8889,11 +8910,75 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
const Vec2d bc = m_bed.build_volume().bed_center();
|
||||
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
|
||||
} else {
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
// With the other plates hidden, the axes go to the current plate as in Preview.
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled() && !_hides_inactive_plates());
|
||||
}
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_hides_inactive_plates() const
|
||||
{
|
||||
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_update_inactive_plates_visibility()
|
||||
{
|
||||
const bool hide = _hides_inactive_plates();
|
||||
if (!hide && !m_inactive_plates_hidden)
|
||||
return false;
|
||||
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
PartPlate* plate = hide ? plate_list.get_curr_plate() : nullptr;
|
||||
|
||||
struct Instance
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
bool selected{ false };
|
||||
};
|
||||
std::map<std::pair<int, int>, Instance> instances;
|
||||
if (plate != nullptr)
|
||||
for (const GLVolume* volume : m_volumes.volumes) {
|
||||
Instance& instance = instances[{ volume->object_idx(), volume->instance_idx() }];
|
||||
instance.box.merge(volume->transformed_convex_hull_bounding_box());
|
||||
instance.selected |= volume->selected;
|
||||
}
|
||||
|
||||
// A gizmo that hides the other objects shows them all again when it closes.
|
||||
const bool keep_hidden = m_gizmos.is_hiding_instances();
|
||||
bool changed = false;
|
||||
bool any_hidden = false;
|
||||
std::vector<bool> shown_again(m_volumes.volumes.size(), false);
|
||||
for (size_t i = 0; i < m_volumes.volumes.size(); ++i) {
|
||||
GLVolume* volume = m_volumes.volumes[i];
|
||||
const Instance* instance = plate != nullptr ? &instances[{ volume->object_idx(), volume->instance_idx() }] : nullptr;
|
||||
if (instance != nullptr && !instance->selected && instance->box.defined && !plate->get_bounding_box().overlap(instance->box)) {
|
||||
changed |= volume->is_active;
|
||||
volume->is_active = false;
|
||||
volume->on_hidden_plate = true;
|
||||
} else if (volume->on_hidden_plate && !keep_hidden) {
|
||||
volume->is_active = true;
|
||||
volume->on_hidden_plate = false;
|
||||
shown_again[i] = true;
|
||||
changed = true;
|
||||
}
|
||||
any_hidden |= volume->on_hidden_plate;
|
||||
}
|
||||
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters) {
|
||||
const int i = SceneRaycaster::decode_id(SceneRaycaster::EType::Volume, item->get_id());
|
||||
if (0 <= i && i < int(m_volumes.volumes.size()) && (m_volumes.volumes[i]->on_hidden_plate || shown_again[i]))
|
||||
item->set_active(shown_again[i]);
|
||||
}
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Bed))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters)
|
||||
item->set_active(!hide || SceneRaycaster::decode_id(SceneRaycaster::EType::Bed, item->get_id()) / PartPlate::GRABBER_COUNT ==
|
||||
plate_list.get_curr_plate_index());
|
||||
|
||||
m_inactive_plates_hidden = hide || any_hidden;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
|
||||
{
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
@@ -8902,7 +8987,7 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
|
||||
PartPlate* curr_plate = plate_list.get_curr_plate();
|
||||
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
|
||||
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled, _hides_inactive_plates());
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
|
||||
@@ -9920,6 +10005,7 @@ void GLCanvas3D::_render_overlays()
|
||||
|
||||
//BBS: GUI refactor: GLToolbar
|
||||
_render_imgui_select_plate_toolbar();
|
||||
_render_plate_switcher();
|
||||
_render_return_toolbar();
|
||||
// BBS
|
||||
//_render_view_toolbar();
|
||||
@@ -10188,6 +10274,35 @@ void GLCanvas3D::_render_main_toolbar()
|
||||
m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
|
||||
}
|
||||
|
||||
// The scrollbar of the plate toolbars of Preview and Prepare, in units of the toolbar scale.
|
||||
static constexpr float PLATE_TOOLBAR_SCROLLBAR = 14.f;
|
||||
|
||||
// Pushes 4 colors and 1 style var.
|
||||
static void push_plate_toolbar_scrollbar_style(bool is_dark, float scrollbar_size)
|
||||
{
|
||||
const ImVec4 grab(0.77f, 0.77f, 0.77f, is_dark ? .6f : 1.f);
|
||||
// A background with opacity over the window's would make a second color.
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, grab);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
}
|
||||
|
||||
// The border of a hovered plate in the plate toolbars, when it is not the current one.
|
||||
static ImVec4 plate_toolbar_hover_color(bool is_dark)
|
||||
{
|
||||
return ImVec4(0.67f, 0.67f, 0.67f, is_dark ? .6f : 1.f);
|
||||
}
|
||||
|
||||
// Not IsWindowHovered(), which is false while a button is held down and would drop the bar.
|
||||
static bool plate_toolbar_keeps_scrollbar(bool shown, float scrollbar_size)
|
||||
{
|
||||
const ImVec2 pos = ImGui::GetWindowPos();
|
||||
return ImGui::IsMouseHoveringRect(pos, pos + ImGui::GetWindowSize() + ImVec2(shown ? scrollbar_size : 0.f, 0.f), !shown) ||
|
||||
(ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow());
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
@@ -10318,16 +10433,15 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
int item_count = m_sel_plate_toolbar.m_items.size() + (m_sel_plate_toolbar.show_stats_item ? 1 : 0);
|
||||
float window_height_calc = (item_count * (button_height + (margin_size + button_margin) * 2.0f) + (item_count - 1) * ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().WindowPadding.y * 2.0f);
|
||||
bool show_scroll = m_sel_plate_toolbar.is_display_scrollbar && (window_height_calc > window_height_max);
|
||||
float scrollbar_size = 14.0f * f_scale;
|
||||
float scrollbar_size = PLATE_TOOLBAR_SCROLLBAR * f_scale;
|
||||
float window_height = std::min(window_height_calc, window_height_max);
|
||||
float window_width = button_width + (margin_size + button_margin + ImGui::GetStyle().WindowPadding.x) * 2 + (show_scroll ? scrollbar_size : 0);
|
||||
|
||||
ImVec4 window_bg = m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f);
|
||||
ImVec4 button_active = ImGuiWrapper::COL_ORCA; // ORCA: Use orca color for selected sliced plate border
|
||||
ImVec4 button_hover = ImVec4(0.67f, 0.67f, 0.67, m_is_dark ? .6f : 1.0f);
|
||||
ImVec4 button_hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImVec4 orca_active = m_is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
|
||||
ImVec4 orca_hover = m_is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
|
||||
ImVec4 scroll_col = ImVec4(0.77f, 0.77f, 0.77f, m_is_dark ? .6f : 1.0f);
|
||||
ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
float button_radius = 1.0f * f_scale;
|
||||
@@ -10335,14 +10449,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
//use white text as the background switch to black
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, m_is_dark ? ImVec4(.9f, .9f, .9f, 1) : ImVec4(.3f, .3f, .3f, 1)); // ORCA Plate number text > Add support for dark mode
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, window_bg);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA using background color with opacity creates a second color. This prevents secondary color
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, button_active);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, button_hover);
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar_size);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (6.f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, button_radius + margin_size);
|
||||
@@ -10613,18 +10723,109 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
ImGui::PopStyleColor(8);
|
||||
ImGui::PopStyleVar(7);
|
||||
|
||||
//ORCA ImGui::IsWindowHovered() returns false when left_down events on buttons that causes scrollbar disappears for a short time
|
||||
auto win_pos = ImGui::GetWindowPos();
|
||||
bool is_win_hovered = ImGui::IsMouseHoveringRect(win_pos, win_pos + ImVec2(window_width + (show_scroll ? scrollbar_size : 0), window_height), !show_scroll);
|
||||
|
||||
// Also show scrollbar visible and continue to capture mouse position
|
||||
const bool is_scrollbar_active_drag = GImGui != nullptr && ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow();
|
||||
|
||||
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered || is_scrollbar_active_drag;
|
||||
m_sel_plate_toolbar.is_display_scrollbar = plate_toolbar_keeps_scrollbar(show_scroll, scrollbar_size);
|
||||
|
||||
imgui.end();
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_plate_switcher()
|
||||
{
|
||||
Plater* plater = wxGetApp().plater();
|
||||
PartPlateList& plate_list = plater->get_partplate_list();
|
||||
const int plate_count = plate_list.get_plate_count();
|
||||
if (!_hides_inactive_plates() || plate_count < 2 || m_gizmos.get_current_type() != GLGizmosManager::Undefined)
|
||||
return;
|
||||
|
||||
// The thumbnails are the ones of the Preview plate toolbar.
|
||||
const unsigned version = plater->plate_toolbar_image_version();
|
||||
if (m_plate_switcher_version != version || m_plate_switcher_items.size() != size_t(plate_count)) {
|
||||
// Only the current plate is edited here; the others are redrawn when their thumbnails go invalid.
|
||||
for (int i = 0; i < plate_count && !plater->is_gcode_3mf(); ++i)
|
||||
plater->update_plate_thumbnails(i, i == plate_list.get_curr_plate_index());
|
||||
m_plate_switcher_items.clear();
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
m_plate_switcher_items.push_back(std::make_unique<IMToolbarItem>());
|
||||
m_plate_switcher_items.back()->set_thumbnail(plate_list.get_plate(i)->thumbnail_data);
|
||||
}
|
||||
m_plate_switcher_version = version;
|
||||
}
|
||||
|
||||
float sc = get_scale();
|
||||
#ifdef WIN32
|
||||
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
ImGuiWrapper& imgui = *wxGetApp().imgui();
|
||||
const ImVec2 btn_size = ImVec2(72.f, 72.f) * sc;
|
||||
const ImVec2 padding = ImVec2(8.f, 8.f) * sc;
|
||||
const float spacing = 8.f * sc;
|
||||
const float rounding = 4.f * sc;
|
||||
const float scrollbar = PLATE_TOOLBAR_SCROLLBAR * sc;
|
||||
const float top = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + CANVAS_TOOLBAR_MARGIN * sc;
|
||||
const float content_h = plate_count * (btn_size.y + spacing) - spacing + 2.f * padding.y;
|
||||
const float max_h = std::max(btn_size.y + 2.f * padding.y, float(get_canvas_size().get_height()) - top - (canvas_corner_height() + CANVAS_TOOLBAR_MARGIN) * sc);
|
||||
// As in the Preview plate toolbar, the scrollbar shows only while the mouse is over the column.
|
||||
const bool scroll = m_plate_switcher_scrollbar && content_h > max_h;
|
||||
|
||||
const ImVec4 text_color = m_is_dark ? ImVec4(.9f, .9f, .9f, 1.f) : ImVec4(.3f, .3f, .3f, 1.f);
|
||||
const ImVec4 empty_color = ImVec4(text_color.x, text_color.y, text_color.z, .45f);
|
||||
const ImVec4 hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
|
||||
// Concentric with the thumbnails; tighter around the scrollbar, as in the Preview plate toolbar.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, scroll ? 6.f * sc : rounding + padding.x);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f));
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar);
|
||||
|
||||
imgui.set_next_window_pos(CANVAS_TOOLBAR_MARGIN * sc, top, ImGuiCond_Always, 0.f, 0.f);
|
||||
imgui.set_next_window_size(btn_size.x + 2.f * padding.x + (scroll ? scrollbar : 0.f), std::min(content_h, max_h), ImGuiCond_Always);
|
||||
imgui.begin(std::string("##PlateSwitcher"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoCollapse | (scroll ? 0 : ImGuiWindowFlags_NoScrollbar));
|
||||
|
||||
const int curr_plate = plate_list.get_curr_plate_index();
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
const ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
const ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
const bool current = i == curr_plate;
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushID(i);
|
||||
const bool clicked = ImGui::InvisibleButton("##plate", btn_size);
|
||||
ImGui::PopID();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_bg, rounding);
|
||||
if (ImTextureID texture = m_plate_switcher_items[i]->texture_id)
|
||||
draw_list->AddImageRounded(texture, pos, pos + btn_size, ImVec2(0.f, 1.f), ImVec2(1.f, 0.f), IM_COL32_WHITE, rounding);
|
||||
if (plate->empty())
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_dim, rounding);
|
||||
if (current || hovered) {
|
||||
const float border = 2.f * sc;
|
||||
draw_list->AddRect(pos - ImVec2(border, border) * .5f, pos + btn_size + ImVec2(border, border) * .5f,
|
||||
ImGui::GetColorU32(current ? (hovered ? ImGuiWrapper::COL_ORCA_HOVER : ImGuiWrapper::COL_ORCA) : hover),
|
||||
rounding + border * .5f, 0, border);
|
||||
}
|
||||
draw_list->AddText(pos + ImVec2(4.f, 2.f) * sc, ImGui::GetColorU32(plate->empty() ? empty_color : text_color), std::to_string(i + 1).c_str());
|
||||
|
||||
if (clicked && !current)
|
||||
plater->CallAfter([i]() {
|
||||
wxGetApp().plater()->select_plate(i);
|
||||
wxGetApp().plater()->deselect_all();
|
||||
});
|
||||
if (hovered) {
|
||||
const std::string name = plate->get_plate_name();
|
||||
imgui.tooltip(_u8L("Plate") + " " + std::to_string(i + 1) + (name.empty() ? "" : " (" + name + ")"), ImGui::GetFontSize() * 20.f);
|
||||
}
|
||||
}
|
||||
|
||||
m_plate_switcher_scrollbar = plate_toolbar_keeps_scrollbar(scroll, scrollbar);
|
||||
imgui.end();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(5);
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_assemble_view_toolbar() const
|
||||
@@ -10731,7 +10932,7 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
|
||||
ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
|
||||
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
|
||||
ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
|
||||
ImVec2 spacing = ImVec2(CANVAS_TOOLBAR_SPACING, CANVAS_TOOLBAR_SPACING) * sc;
|
||||
ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
|
||||
Vec2i32 pos = {
|
||||
m_canvas_toolbar_pos[0] + margin.x,
|
||||
@@ -10934,6 +11135,17 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
[]{wxGetApp().toggle_show_plate_gridlines();}
|
||||
);
|
||||
|
||||
create_menu_item( _utf8(L("Focus mode")),
|
||||
m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas, // work only on prepare
|
||||
wxGetApp().focus_mode(),
|
||||
[this]{
|
||||
wxGetApp().toggle_focus_mode();
|
||||
if (wxGetApp().focus_mode())
|
||||
zoom_to_plate(REQUIRES_ZOOM_TO_CUR_PLATE);
|
||||
m_dirty = true;
|
||||
}
|
||||
);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
create_menu_item( _utf8(L("Labels")),
|
||||
|
||||
@@ -660,6 +660,10 @@ private:
|
||||
mutable GLToolbar m_main_toolbar;
|
||||
mutable GLToolbar m_separator_toolbar;
|
||||
mutable IMToolbar m_sel_plate_toolbar;
|
||||
// Thumbnails of the plate switcher (Focus mode), refreshed when the plater's version changes.
|
||||
std::vector<std::unique_ptr<IMToolbarItem>> m_plate_switcher_items;
|
||||
unsigned m_plate_switcher_version{ unsigned(-1) };
|
||||
bool m_plate_switcher_scrollbar{ false };
|
||||
mutable GLToolbar m_assemble_view_toolbar;
|
||||
mutable IMReturnToolbar m_return_toolbar;
|
||||
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
|
||||
@@ -778,6 +782,8 @@ private:
|
||||
//BBS:add plate related logic
|
||||
mutable std::vector<int> m_hover_volume_idxs;
|
||||
std::vector<int> m_hover_plate_idxs;
|
||||
// Some volume or plate raycaster may still be hidden with the inactive plates.
|
||||
bool m_inactive_plates_hidden{ false };
|
||||
// IMEX ghost hover state (plate-owned transparent ghost volumes).
|
||||
int m_hover_ghost_head { -1 }; // physical head index, -1 when not hovering a ghost
|
||||
int m_hover_ghost_plate { -1 }; // plate index for the hovered ghost, -1 when none
|
||||
@@ -1527,6 +1533,9 @@ private:
|
||||
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
//BBS: add part plate related logic
|
||||
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
bool _hides_inactive_plates() const;
|
||||
// Returns whether what the scene shows changed.
|
||||
bool _update_inactive_plates_visibility();
|
||||
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
|
||||
BoundingBoxf3 _current_plate_box() const;
|
||||
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
|
||||
@@ -1565,6 +1574,8 @@ private:
|
||||
void _render_gizmos_overlay();
|
||||
void _render_main_toolbar();
|
||||
void _render_imgui_select_plate_toolbar();
|
||||
// Prepare: picks the plate to show while the inactive ones are hidden.
|
||||
void _render_plate_switcher();
|
||||
void _render_assemble_view_toolbar() const;
|
||||
void _render_return_toolbar() const;
|
||||
void _render_canvas_toolbar();
|
||||
|
||||
+149
-8
@@ -41,6 +41,7 @@
|
||||
#include <boost/filesystem/exception.hpp>
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#include <boost/log/detail/native_typeof.hpp>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
@@ -325,6 +326,13 @@ namespace GUI {
|
||||
|
||||
class MainFrame;
|
||||
|
||||
// The OrcaCloud agent behind the shared NetworkAgent, or null when networking/OrcaCloud is not
|
||||
// available (e.g. stealth mode or the network plugin still loading).
|
||||
static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent* agent)
|
||||
{
|
||||
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
|
||||
}
|
||||
|
||||
void start_ping_test()
|
||||
{
|
||||
return;
|
||||
@@ -2262,7 +2270,9 @@ void GUI_App::init_networking_callbacks()
|
||||
if (return_code == 5) {
|
||||
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: login expired";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: the desktop lost its session on its own. Leave the webview's
|
||||
// own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
if (msg_dlg.ShowModal() == wxOK) {
|
||||
return;
|
||||
@@ -5238,7 +5248,7 @@ void GUI_App::post_logout_to_webview(const std::string& provider)
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/, bool user_initiated)
|
||||
{
|
||||
if (m_agent && m_agent->is_user_login(provider)) {
|
||||
m_agent->user_logout(true, provider);
|
||||
@@ -5251,6 +5261,21 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
|
||||
}
|
||||
|
||||
if (provider == ORCA_CLOUD_PROVIDER) {
|
||||
// Invalidate any handoff still in flight, so a token minted for the signed-out
|
||||
// account is never loaded into the pane.
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
|
||||
// An explicit user sign-out also ends the webview's own cloud session (guide §8).
|
||||
// A session the desktop lost on its own must NOT do this: a 2FA user would otherwise
|
||||
// loop through the code prompt on every desktop re-login.
|
||||
if (user_initiated) {
|
||||
if (auto orca = get_orca_cloud_agent(m_agent)) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->cloud_signed_out(from_u8(orca->build_web_session_signout_url()));
|
||||
}
|
||||
}
|
||||
|
||||
/* delete old user settings */
|
||||
bool transfer_preset_changes = false;
|
||||
wxString header = _L("Some presets are modified.") + "\n" +
|
||||
@@ -5372,7 +5397,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
|
||||
request_user_logout();
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_orca_login_info") == 0) {
|
||||
@@ -5387,7 +5412,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_bambu_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
|
||||
@@ -5396,7 +5421,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_orca_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_modeldepot") == 0) {
|
||||
@@ -5408,6 +5433,20 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_openproject") == 0) {
|
||||
this->request_open_project({});
|
||||
}
|
||||
else if (command_str.compare("homepage_open_cloud") == 0) {
|
||||
// Deferred: set_cloud_mode() may create a second native webview, which must
|
||||
// not run inside this webview's script-message callback.
|
||||
CallAfter([this]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_close_cloud") == 0) {
|
||||
CallAfter([]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(false);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_recent_projects") == 0) {
|
||||
if (mainframe) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
@@ -5680,7 +5719,9 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (!provider.empty() && m_agent->is_user_login(provider)) {
|
||||
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: a 401 means the desktop session was rejected. Leave the
|
||||
// webview's own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
|
||||
if (!m_show_http_error_msgdlg) {
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
@@ -5824,6 +5865,93 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
|
||||
}*/
|
||||
}
|
||||
|
||||
wxString GUI_App::cloud_webview_handoff()
|
||||
{
|
||||
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
|
||||
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
|
||||
// post request_web_session, which is what opens the desktop login dialog.
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return {};
|
||||
return from_u8(orca->build_web_session_start_url());
|
||||
}
|
||||
|
||||
void GUI_App::request_cloud_web_session(const std::string& state)
|
||||
{
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
// Only the newest request counts: a newer state supersedes any mint or login still in flight.
|
||||
m_cloud_handoff_state = state;
|
||||
const uint64_t gen = ++m_cloud_handoff_gen;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca || !orca->is_user_login()) {
|
||||
// Desktop signed out: open the desktop login dialog and stop here. On success
|
||||
// on_user_login_handle reloads the Start URL, which makes the page request again; on
|
||||
// cancel nothing happens (the guide's "on cancel, do nothing"). The newest state is
|
||||
// already stored, so a repeat request while the dialog is open is dropped rather than
|
||||
// stacking another dialog.
|
||||
if (!login_dlg || !login_dlg->IsShown())
|
||||
request_login(true, ORCA_CLOUD_PROVIDER);
|
||||
return;
|
||||
}
|
||||
|
||||
mint_cloud_web_session(state, gen);
|
||||
}
|
||||
|
||||
void GUI_App::mint_cloud_web_session(const std::string& state, uint64_t gen)
|
||||
{
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// The endpoint call blocks on network I/O (and possibly a cross-process token refresh), so
|
||||
// it must not run on the UI thread. Hand off to a worker and marshal the token back.
|
||||
boost::thread([this, orca, state, gen]() {
|
||||
std::string token_hash;
|
||||
std::string error_code;
|
||||
unsigned int http_code = 0;
|
||||
orca->request_web_session_handoff(token_hash, error_code, http_code);
|
||||
|
||||
if (is_closing())
|
||||
return;
|
||||
|
||||
if (token_hash.empty()) {
|
||||
// No token: 401 after retry, 409/429/503 or a transport error. The page owns its own
|
||||
// "Sign-in link expired"/"Try again" UI; the desktop stays silent. Never log the body
|
||||
// (it may echo account data), only the status/error code.
|
||||
BOOST_LOG_TRIVIAL(warning) << "cloud web session handoff not minted - http_code=" << http_code
|
||||
<< " error_code=" << error_code;
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::wxGetApp().CallAfter([this, state, gen, token_hash]() {
|
||||
on_cloud_web_session_minted(state, gen, token_hash);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash)
|
||||
{
|
||||
if (is_closing())
|
||||
return;
|
||||
// Drop a result for a superseded request (a newer request_web_session, login or sign-out).
|
||||
if (gen != m_cloud_handoff_gen || state != m_cloud_handoff_state)
|
||||
return;
|
||||
if (token_hash.empty())
|
||||
return;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// Load the Complete URL in the SAME webview that posted request_web_session, because the
|
||||
// state lives in that webview's session storage (guide §3/§7).
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->load_cloud_url(from_u8(orca->build_web_session_complete_url(token_hash, state)));
|
||||
}
|
||||
|
||||
void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
{
|
||||
if (!m_agent) { return; }
|
||||
@@ -5879,6 +6007,16 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
if (app_config->get("sync_user_preset") == "true") {
|
||||
start_sync_user_preset();
|
||||
}
|
||||
|
||||
// Re-seed the embedded OrcaCloud pane if it is already open, so it picks up the new session
|
||||
// without waiting for the next open. On a fresh login or account switch this invalidates any
|
||||
// in-flight handoff for the previous account and loads the Start URL with the new user_id, so
|
||||
// the page drops the old account and re-requests (guide §8).
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
if (home->cloud_mode_active())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
}
|
||||
|
||||
|
||||
@@ -8012,8 +8150,11 @@ void GUI_App::on_stealth_mode_enter()
|
||||
{
|
||||
stop_sync_user_preset();
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
// Stealth mode forces a desktop logout, but the user did not choose "Sign out": guide §8 ties
|
||||
// ?signout=1 to the explicit desktop sign-out only. Leave the cloud webview's own session
|
||||
// alone, so it is re-checked (and re-seeded with the right account) on the next open.
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
home->SendCloudProvidersInfo();
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ private:
|
||||
bool m_side_popup_status{false};
|
||||
bool m_show_http_error_msgdlg{false};
|
||||
std::chrono::steady_clock::time_point m_last_401_error_time;
|
||||
// OrcaCloud webview session handoff: the newest request_web_session state from the cloud pane
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
std::string m_cloud_handoff_state;
|
||||
uint64_t m_cloud_handoff_gen { 0 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
HttpServer m_http_server;
|
||||
@@ -431,6 +436,8 @@ public:
|
||||
|
||||
bool show_plate_gridlines() const { return app_config->get_bool("show_plate_gridlines"); }
|
||||
void toggle_show_plate_gridlines() const { app_config->set_bool("show_plate_gridlines", !show_plate_gridlines()); }
|
||||
bool focus_mode() const { return app_config->get_bool("focus_mode"); }
|
||||
void toggle_focus_mode() const { app_config->set_bool("focus_mode", !focus_mode()); }
|
||||
|
||||
bool show_canvas_zoom_button() const { return app_config->get_bool("show_canvas_zoom_button"); }
|
||||
void toggle_canvas_zoom_button() const { app_config->set_bool("show_canvas_zoom_button", !show_canvas_zoom_button()); }
|
||||
@@ -540,7 +547,11 @@ public:
|
||||
|
||||
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
// `user_initiated` distinguishes a user sign-out from the desktop losing its session on its
|
||||
// own (refresh failed, 401). Only an explicit sign-out also ends the cloud webview's own
|
||||
// session; an involuntary one leaves the webview alone (guide §8). Defaults to the safe
|
||||
// direction: a new caller that forgets the flag will not silently sign the webview out.
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER, bool user_initiated = false);
|
||||
void post_logout_to_webview(const std::string& provider);
|
||||
int request_user_unbind(std::string dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
std::string handle_web_request(std::string cmd);
|
||||
@@ -556,6 +567,15 @@ public:
|
||||
void on_update_machine_list(wxCommandEvent& evt);
|
||||
void on_user_login(wxCommandEvent &evt);
|
||||
void on_user_login_handle(wxCommandEvent& evt);
|
||||
// OrcaCloud webview session handoff (guide §5/§7). cloud_webview_handoff builds the cloud
|
||||
// pane's Start URL (with the desktop user_id while signed in). request_cloud_web_session is
|
||||
// called from the pane when the page asks for a session; it opens the desktop login dialog if
|
||||
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
void mint_cloud_web_session(const std::string& state, uint64_t gen);
|
||||
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
|
||||
void enable_user_preset_folder(bool enable);
|
||||
|
||||
// BBS
|
||||
|
||||
@@ -2334,243 +2334,60 @@ void MenuFactory::append_menu_item_center(wxMenu* menu)
|
||||
|
||||
void MenuFactory::append_menu_item_align_distribute(wxMenu *menu)
|
||||
{
|
||||
using AlignType = GLGizmoAlignment::AlignType;
|
||||
auto can = [](AlignType type) {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
|
||||
return type >= AlignType::DISTRIBUTE_X ? alignment_helper.can_distribute(type) : alignment_helper.can_align(type);
|
||||
};
|
||||
auto run = [](AlignType type) {
|
||||
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
|
||||
if (type >= AlignType::DISTRIBUTE_X)
|
||||
alignment_helper.distribute_objects(type);
|
||||
else
|
||||
alignment_helper.align_objects(type);
|
||||
};
|
||||
|
||||
struct AlignMenuItem
|
||||
{
|
||||
wxString label;
|
||||
AlignType type;
|
||||
std::string icon;
|
||||
};
|
||||
const AlignMenuItem items[] = {
|
||||
// icons shared with caanvas. BitmapCache converts colors dark to light. so dark variations used for proper colors
|
||||
{_L("Distribute left-right") + " (X)" , AlignType::DISTRIBUTE_X, "distribute_x_dark"},
|
||||
{_L("Distribute front-back") + " (Y)" , AlignType::DISTRIBUTE_Y, "distribute_y_dark"},
|
||||
{_L("Distribute top-bottom") + " (Z)" , AlignType::DISTRIBUTE_Z, "distribute_z_dark"},
|
||||
{wxString(), AlignType::NONE, ""},
|
||||
{_L("Align left") + " (-X)", AlignType::X_MIN, "align_x_min_dark"},
|
||||
{_L("Align left-right center") + " (X)" , AlignType::CENTER_X, "align_x_center_dark"},
|
||||
{_L("Align right") + " (+X)", AlignType::X_MAX, "align_x_max_dark"},
|
||||
{wxString(), AlignType::NONE, ""},
|
||||
{_L("Align front") + " (-Y)", AlignType::Y_MIN, "align_y_min_dark"},
|
||||
{_L("Align front-back center") + " (Y)" , AlignType::CENTER_Y, "align_y_center_dark"},
|
||||
{_L("Align back") + " (+Y)", AlignType::Y_MAX, "align_y_max_dark"},
|
||||
{wxString(), AlignType::NONE, ""},
|
||||
{_L("Align bottom") + " (-Z)", AlignType::Z_MIN, "align_z_min_dark"},
|
||||
{_L("Align top-bottom center") + " (Z)" , AlignType::CENTER_Z, "align_z_center_dark"},
|
||||
{_L("Align top") + " (+Z)", AlignType::Z_MAX, "align_z_max_dark"},
|
||||
};
|
||||
|
||||
wxMenu* align_distribute_menu = new wxMenu();
|
||||
if (!align_distribute_menu)
|
||||
return;
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Distribute left-right") + " (X)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->distribute_selection_x();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_X);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Distribute front-back") + " (Y)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->distribute_selection_y();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Y);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Distribute top-bottom") + " (Z)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->distribute_selection_z();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Z);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
align_distribute_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align left") + " (-X)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_x_min();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align left-right center") + " (X)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_x_center();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_X);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align right") + " (+X)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_x_max();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MAX);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
align_distribute_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align front") + " (-Y)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_y_min();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MIN);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align front-back center") + " (Y)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_y_center();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Y);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align back") + " (+Y)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_y_max();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MAX);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
align_distribute_menu->AppendSeparator();
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align bottom") + " (-Z)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_z_min();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MIN);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align top-bottom center") + " (Z)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_z_center();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Z);
|
||||
},
|
||||
m_parent);
|
||||
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, _L("Align top") + " (+Z)", "",
|
||||
[](wxCommandEvent &) {
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
|
||||
plater()->align_selection_z_max();
|
||||
},
|
||||
"", nullptr,
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MAX);
|
||||
},
|
||||
m_parent);
|
||||
for (const AlignMenuItem &item : items) {
|
||||
const AlignType type = item.type;
|
||||
if (type == AlignType::NONE)
|
||||
align_distribute_menu->AppendSeparator();
|
||||
else
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, item.label, "", [run, type](wxCommandEvent &) { run(type); }, item.icon, nullptr,
|
||||
[can, type]() { return can(type); }, m_parent);
|
||||
}
|
||||
|
||||
// Anything that can be distributed can also be aligned, and aligning in X has no extra condition.
|
||||
append_submenu(menu, align_distribute_menu, wxID_ANY, _L("Align/Distribute"), _L("Align and distribute objects"), "",
|
||||
[]() {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
auto canvas3d = plater()->get_view3D_canvas3D();
|
||||
GLGizmoAlignment alignment_helper(*canvas3d);
|
||||
|
||||
bool has_any_align = alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_X) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Y) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Z) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MAX) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MAX) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MIN) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MAX) ||
|
||||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MIN);
|
||||
|
||||
bool has_any_distribute = alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_X) ||
|
||||
alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Y) ||
|
||||
alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Z);
|
||||
|
||||
return has_any_align || has_any_distribute;
|
||||
}, m_parent);
|
||||
[can]() { return can(AlignType::X_MIN); }, m_parent);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_drop(wxMenu* menu)
|
||||
@@ -2583,7 +2400,7 @@ void MenuFactory::append_menu_item_drop(wxMenu* menu)
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
else {
|
||||
return (std::abs(plater()->get_view3D_canvas3D()->get_selection().get_bounding_box().min.z()) > -SINKING_Z_THRESHOLD);
|
||||
return plater()->get_view3D_canvas3D()->get_selection().can_drop();
|
||||
} //disable if model is on the bed / not in View3D
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
#include "GLGizmoAlignment.hpp"
|
||||
#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
@@ -13,7 +25,7 @@ GLGizmoAlignment::GLGizmoAlignment(GLCanvas3D& canvas) : m_canvas(canvas)
|
||||
|
||||
bool GLGizmoAlignment::align_objects(AlignType type, bool align_parent)
|
||||
{
|
||||
if (!validate_selection_for_align()) {
|
||||
if (!can_align(type, align_parent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -92,32 +104,29 @@ bool GLGizmoAlignment::align_to_x_min(bool align_parent)
|
||||
|
||||
bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
std::string type_str = type == AlignType::CENTER_X ? "X" : (type == AlignType::CENTER_Y ? "Y" : "Z");
|
||||
std::string operation_name = type == AlignType::CENTER_X ? "X Center" : (type == AlignType::CENTER_Y ? "Y Center" : "Z Center");
|
||||
double selection_center = 0.f;
|
||||
if (align_parent) {
|
||||
selection_center = get_current_coord(operation_name, m_parent_box);
|
||||
BoundingBoxf3 parent_box = get_parent_box();
|
||||
selection_center = get_current_coord(operation_name, parent_box);
|
||||
|
||||
// Handle multiple volume selection
|
||||
if (is_part_align_parent()) {
|
||||
if (is_parts_selection(true)) {
|
||||
if (!take_snapshot("Align to parent center " + type_str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
auto selection_bbox = selection.get_bounding_box();
|
||||
double current_coord = get_current_coord(operation_name, selection_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
// Move each selected volume to align to the center of their collective bounding box
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume *volume = selection.get_volume(idx);
|
||||
if (volume) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
|
||||
}
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
double current_coord = get_current_coord(operation_name, selection_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
// Move the selected volumes together so that their collective bounding box is centered in the parent
|
||||
for (const GLVolume *volume : get_selected_part_volumes()) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
finish_operation("Align to parent center " + type_str);
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
BoundingBoxf3 big_bb;
|
||||
@@ -125,7 +134,6 @@ bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
|
||||
if (objects.empty()) return false;
|
||||
// Handle multiple objects or parts selection
|
||||
|
||||
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
|
||||
if (!take_snapshot("Align to parent center " + type_str)) {
|
||||
return false;
|
||||
}
|
||||
@@ -138,60 +146,52 @@ bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
|
||||
finish_operation("Align to parent center " + type_str);
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
// Handle single object selection// Handle multiple volume selection
|
||||
if (selection.is_single_full_object() ||
|
||||
selection.is_multiple_volume() || selection.is_multiple_modifier() || selection.is_single_volume() || selection.is_single_modifier()) {
|
||||
// Handle the parts of a single instance
|
||||
if (is_parts_selection(false)) {
|
||||
if (!take_snapshot("Align Parts Center " + type_str)){
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
selection_center = get_current_coord(operation_name, selection_bbox);
|
||||
|
||||
// Move each selected volume to align to the center of their collective bounding box
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume* volume = selection.get_volume(idx);
|
||||
if (volume) {
|
||||
auto temp_box = volume->transformed_convex_hull_bounding_box();
|
||||
double current = get_current_coord(operation_name, temp_box);
|
||||
double offset = selection_center - current;
|
||||
Vec3d displacement = generate_displacement(operation_name, offset);
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
|
||||
}
|
||||
// Move each part to align to the center of their collective bounding box
|
||||
for (const GLVolume *volume : get_selected_part_volumes()) {
|
||||
auto temp_box = volume->transformed_convex_hull_bounding_box();
|
||||
double current = get_current_coord(operation_name, temp_box);
|
||||
double offset = selection_center - current;
|
||||
Vec3d displacement = generate_displacement(operation_name, offset);
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
|
||||
finish_operation("Align Parts Center" + type_str, true);
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.empty()) return false;
|
||||
// Handle multiple objects or parts selection
|
||||
if (selection.is_multiple_full_object()) {
|
||||
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
|
||||
double selection_center = get_current_coord(operation_name, selection_bbox);
|
||||
// Handle multiple instances
|
||||
selection_center = get_current_coord(operation_name, big_bb);
|
||||
|
||||
if (!take_snapshot("Align Objects Center" + type_str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
// Multiple objects: use selection bounding box for alignment
|
||||
for (const auto& obj : objects) {
|
||||
BoundingBoxf3 temp_bbox = obj.bbox;
|
||||
double current_coord = get_current_coord(operation_name, temp_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
finish_operation("Align Objects Center" + type_str);
|
||||
return true;
|
||||
if (!take_snapshot("Align Objects Center" + type_str)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
get_selection().setup_cache();
|
||||
// Multiple objects: use selection bounding box for alignment
|
||||
for (const auto& obj : objects) {
|
||||
BoundingBoxf3 temp_bbox = obj.bbox;
|
||||
double current_coord = get_current_coord(operation_name, temp_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_z_max(bool align_parent)
|
||||
@@ -244,37 +244,31 @@ bool GLGizmoAlignment::distribute_z()
|
||||
template<typename GetCoordFunc, typename SetCoordFunc>
|
||||
bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name ,bool align_parent)
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
if (align_parent) {
|
||||
// Find reference coordinate
|
||||
double reference_coord = get_current_coord(operation_name, m_parent_box);
|
||||
BoundingBoxf3 parent_box = get_parent_box();
|
||||
double reference_coord = get_current_coord(operation_name, parent_box);
|
||||
// Handle parts selection differently
|
||||
if (is_part_align_parent()) {
|
||||
if (is_parts_selection(true)) {
|
||||
if (!take_snapshot("align to parent node " + operation_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
// Get all selected volumes
|
||||
std::vector<const GLVolume *> volumes;
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume *volume = selection.get_volume(idx);
|
||||
if (volume) { volumes.push_back(volume); }
|
||||
}
|
||||
|
||||
const std::vector<const GLVolume *> volumes = get_selected_part_volumes();
|
||||
if (volumes.empty())
|
||||
return false;
|
||||
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
double current_coord = get_current_coord(operation_name, selection_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
|
||||
for (const GLVolume *volume : volumes) {
|
||||
if (displacement.norm() > 1e-6) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation("align to parent node " + operation_name);
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -296,26 +290,18 @@ bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFun
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation("align to parent node " + operation_name);
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
// Handle parts selection differently
|
||||
if (selection.is_single_full_object() || selection.is_multiple_volume() || selection.is_multiple_modifier()) {
|
||||
if (is_parts_selection(false)) {
|
||||
if (!take_snapshot(operation_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
// Get all selected volumes
|
||||
std::vector<const GLVolume*> volumes;
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume* volume = selection.get_volume(idx);
|
||||
if (volume) {
|
||||
volumes.push_back(volume);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
|
||||
if (volumes.empty()) return false;
|
||||
|
||||
// Find reference coordinate
|
||||
@@ -381,11 +367,11 @@ bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFun
|
||||
}
|
||||
|
||||
if (displacement.norm() > 1e-6) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(operation_name,true);
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -420,22 +406,15 @@ bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFun
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(operation_name);
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename GetCoordFunc>
|
||||
bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int axis, const std::string& operation_name)
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
|
||||
if (selection.is_single_full_object() || selection.is_multiple_volume() || selection.is_multiple_modifier()) {
|
||||
std::vector<const GLVolume*> volumes;
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume* v = selection.get_volume(idx);
|
||||
if (v) volumes.push_back(v);
|
||||
}
|
||||
|
||||
if (is_parts_selection(false)) {
|
||||
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
|
||||
if (volumes.size() < 3) return false;
|
||||
if (!take_snapshot(operation_name)) {
|
||||
return false;
|
||||
@@ -454,7 +433,7 @@ bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int ax
|
||||
|
||||
std::sort(entries.begin(), entries.end(), [](const VolEntry& a, const VolEntry& b){ return a.coord < b.coord; });
|
||||
|
||||
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
BoundingBoxf3 left_bbox = entries.front().v->transformed_convex_hull_bounding_box();
|
||||
BoundingBoxf3 right_bbox = entries.back().v->transformed_convex_hull_bounding_box();
|
||||
auto axis_extent = [&](const BoundingBoxf3 &bb) { return (axis == 0 ? (bb.max.x() - bb.min.x()) : axis == 1 ? (bb.max.y() - bb.min.y()) : (bb.max.z() - bb.min.z())); };
|
||||
@@ -475,11 +454,11 @@ bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int ax
|
||||
Vec3d displacement = Vec3d::Zero();
|
||||
displacement[axis] = offset;
|
||||
const GLVolume *v = entries[i].v;
|
||||
get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement);
|
||||
get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement, false);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(operation_name,true);
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
BoundingBoxf3 big_bb;
|
||||
@@ -513,52 +492,19 @@ bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int ax
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(operation_name);
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::can_align(AlignType type) const
|
||||
bool GLGizmoAlignment::can_align(AlignType type, bool align_parent) const
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
|
||||
bool is_single_object = selection.is_single_full_object();
|
||||
bool is_multiple_objects = selection.is_multiple_full_object();
|
||||
bool is_single_part = selection.is_single_volume() || selection.is_single_modifier();
|
||||
bool is_multiple_parts = selection.is_multiple_volume() || selection.is_multiple_modifier();
|
||||
|
||||
|
||||
return validate_selection_for_align();
|
||||
return get_items_count(type, align_parent) >= (align_parent ? 1 : 2);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::can_distribute(AlignType type) const
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
if (selection.is_single_full_object()) {
|
||||
size_t count = selection.get_volume_idxs().size();
|
||||
if (count >= 3)
|
||||
return true;
|
||||
}
|
||||
if (selection.is_single_volume() ||
|
||||
selection.is_single_modifier()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selection.is_multiple_volume() || selection.is_multiple_modifier()) {
|
||||
size_t count = selection.get_volume_idxs().size();
|
||||
if (count < 3) return false;
|
||||
return (type == AlignType::DISTRIBUTE_X ||
|
||||
type == AlignType::DISTRIBUTE_Y ||
|
||||
type == AlignType::DISTRIBUTE_Z);
|
||||
}
|
||||
|
||||
if (selection.is_multiple_full_object()) {
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.size() < 3) return false;
|
||||
return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z);
|
||||
}
|
||||
|
||||
return validate_selection_for_distribute();
|
||||
return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z) &&
|
||||
get_items_count(type, false) >= 3;
|
||||
}
|
||||
|
||||
double GLGizmoAlignment::get_current_coord(std::string operation_name, BoundingBoxf3 &bbox)
|
||||
@@ -598,15 +544,6 @@ Vec3d GLGizmoAlignment::generate_displacement(std::string operation_name, double
|
||||
return displacement;
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::is_part_align_parent()
|
||||
{
|
||||
Selection &selection = get_selection();
|
||||
if (selection.is_multiple_volume() || selection.is_multiple_modifier() || selection.is_single_volume() || selection.is_single_modifier()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<GLGizmoAlignment::ObjectInfo> GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const
|
||||
{
|
||||
std::vector<ObjectInfo> objects;
|
||||
@@ -633,8 +570,74 @@ std::vector<GLGizmoAlignment::ObjectInfo> GLGizmoAlignment::get_selected_objects
|
||||
return objects;
|
||||
}
|
||||
|
||||
void GLGizmoAlignment::set_parent_box(const BoundingBoxf3 &bb) {
|
||||
m_parent_box = bb;
|
||||
bool GLGizmoAlignment::is_parts_selection(bool align_parent) const
|
||||
{
|
||||
const Selection &selection = get_selection();
|
||||
const auto &content = selection.get_content();
|
||||
if (content.size() != 1 || content.begin()->first >= 1000 || content.begin()->second.size() != 1)
|
||||
return false;
|
||||
return !align_parent || selection.get_mode() == Selection::Volume;
|
||||
}
|
||||
|
||||
std::vector<const GLVolume *> GLGizmoAlignment::get_selected_part_volumes() const
|
||||
{
|
||||
std::vector<const GLVolume *> volumes;
|
||||
if (!is_parts_selection(false))
|
||||
return volumes;
|
||||
|
||||
const Selection &selection = get_selection();
|
||||
const ModelObject *object = selection.get_model()->objects[selection.get_object_idx()];
|
||||
if (object->is_cut())
|
||||
return volumes;
|
||||
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume *volume = selection.get_volume(idx);
|
||||
if (volume->volume_idx() < 0)
|
||||
continue;
|
||||
// Moving a whole instance's parts leaves its modifiers, negative volumes and support blockers in place,
|
||||
// as dragging a part does.
|
||||
if (selection.get_mode() == Selection::Volume || object->volumes[volume->volume_idx()]->is_model_part())
|
||||
volumes.push_back(volume);
|
||||
}
|
||||
return volumes;
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLGizmoAlignment::get_items_box() const
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
if (is_parts_selection(false)) {
|
||||
for (const GLVolume *volume : get_selected_part_volumes())
|
||||
box.merge(volume->transformed_convex_hull_bounding_box());
|
||||
} else
|
||||
get_selected_objects_info(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLGizmoAlignment::get_parent_box() const
|
||||
{
|
||||
const Selection &selection = get_selection();
|
||||
if (is_parts_selection(true))
|
||||
return selection.get_model()->objects[selection.get_object_idx()]->instance_convex_hull_bounding_box(selection.get_instance_idx());
|
||||
|
||||
// can_align() requires every selected instance on one plate, so the first one gives the plate.
|
||||
BoundingBoxf3 box;
|
||||
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
|
||||
if (obj_idx >= 1000 || inst_idxs.empty())
|
||||
continue;
|
||||
PartPlate *plate = plate_list.get_plate(plate_list.find_instance(obj_idx, *inst_idxs.begin()));
|
||||
if (plate == nullptr)
|
||||
break;
|
||||
BoundingBoxf3 instances_box;
|
||||
get_selected_objects_info(instances_box);
|
||||
box = plate->get_plate_box();
|
||||
box.max.z() = instances_box.max.z();
|
||||
// Keep the aligned objects off the plate edge.
|
||||
box.min += Vec3d(0.1, 0.1, 0.1);
|
||||
box.max -= Vec3d(0.1, 0.1, 0.1);
|
||||
break;
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
Selection& GLGizmoAlignment::get_selection() const
|
||||
@@ -653,41 +656,61 @@ void GLGizmoAlignment::apply_transformation(int obj_idx, int inst_idx, const Vec
|
||||
get_selection().translate(obj_idx, inst_idx, displacement);
|
||||
}
|
||||
|
||||
void GLGizmoAlignment::finish_operation(const std::string &operation_name, bool force_volume_move)
|
||||
void GLGizmoAlignment::finish_operation(bool force_volume_move)
|
||||
{
|
||||
get_selection().notify_instance_update(-1, -1);
|
||||
m_canvas.do_move(operation_name, force_volume_move);
|
||||
// A part move changes the ModelVolume shared by every copy of the object, so the other copies' volumes must show
|
||||
// it too, or do_move() would write their stale transformation back to the model.
|
||||
if (force_volume_move)
|
||||
get_selection().synchronize_unselected_volumes();
|
||||
// The operation already took its snapshot.
|
||||
m_canvas.do_move("", force_volume_move);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::validate_selection_for_align() const
|
||||
size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent, std::string *why_not) const
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
auto cannot_move = [why_not](const std::string &reason) {
|
||||
if (why_not != nullptr)
|
||||
*why_not = reason;
|
||||
return size_t(0);
|
||||
};
|
||||
|
||||
if (selection.is_single_full_object() ||
|
||||
selection.is_single_volume() ||
|
||||
selection.is_single_modifier()) {
|
||||
return true;
|
||||
if (is_parts_selection(align_parent)) {
|
||||
const size_t count = get_selected_part_volumes().size();
|
||||
return count > 0 ? count : cannot_move(_u8L("Parts of a cut object cannot be moved"));
|
||||
}
|
||||
|
||||
return selection.get_volume_idxs().size() >= 2;
|
||||
}
|
||||
// Instances are moved through their instance offsets, which do_move() commits only from an Instance selection.
|
||||
const Selection &selection = get_selection();
|
||||
if (selection.get_mode() != Selection::Instance)
|
||||
return 0;
|
||||
|
||||
bool GLGizmoAlignment::validate_selection_for_distribute() const
|
||||
{
|
||||
const Selection& selection = get_selection();
|
||||
const bool is_z = type == AlignType::Z_MIN || type == AlignType::CENTER_Z || type == AlignType::Z_MAX ||
|
||||
type == AlignType::DISTRIBUTE_Z;
|
||||
// The plate gives no Z target, and aligning to its bottom would duplicate Drop.
|
||||
if (is_z && align_parent)
|
||||
return cannot_move(_u8L("Aligning to the plate is available in X and Y only"));
|
||||
|
||||
if (selection.is_single_full_object() || selection.is_multiple_full_object()) {
|
||||
std::set<int> object_indices;
|
||||
for (int volume_idx : selection.get_volume_idxs()) {
|
||||
const GLVolume* volume = selection.get_volume(volume_idx);
|
||||
if (volume) {
|
||||
object_indices.insert(volume->object_idx());
|
||||
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
int plate_idx = -1;
|
||||
size_t count = 0;
|
||||
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
|
||||
if (obj_idx >= 1000)
|
||||
continue;
|
||||
// Auto-drop would put a raised object back on the bed and leave a lowered one sunk.
|
||||
if (is_z && selection.get_model()->objects[obj_idx]->auto_drop)
|
||||
return cannot_move(_u8L("Turn off Auto-Drop for the selected objects to move them in Z"));
|
||||
if (align_parent) {
|
||||
// The plate holding the selection is the target, so it must hold all of it.
|
||||
for (int inst_idx : inst_idxs) {
|
||||
const int idx = plate_list.find_instance(obj_idx, inst_idx);
|
||||
if (idx == -1 || (plate_idx != -1 && idx != plate_idx))
|
||||
return cannot_move(_u8L("All selected objects must be on the same plate"));
|
||||
plate_idx = idx;
|
||||
}
|
||||
}
|
||||
return object_indices.size() >= 3;
|
||||
count += inst_idxs.size();
|
||||
}
|
||||
|
||||
return selection.get_volume_idxs().size() >= 3;
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
@@ -58,14 +59,26 @@ public:
|
||||
bool distribute_y();
|
||||
bool distribute_z();
|
||||
|
||||
bool can_align(AlignType type) const;
|
||||
bool can_align(AlignType type, bool align_parent = false) const;
|
||||
bool can_distribute(AlignType type) const;
|
||||
|
||||
std::vector<ObjectInfo> get_selected_objects_info(BoundingBoxf3 &big_bb) const;
|
||||
double get_current_coord(std::string operation_name, BoundingBoxf3 &bbox);
|
||||
Vec3d generate_displacement(std::string operation_name, double offset);
|
||||
bool is_part_align_parent();
|
||||
void set_parent_box(const BoundingBoxf3 &bb);
|
||||
|
||||
// A parts selection (one instance of one object) aligns the parts inside it; anything else aligns whole instances.
|
||||
// With align_parent, only a Volume-mode selection counts as parts: a whole instance aligns to its plate instead.
|
||||
bool is_parts_selection(bool align_parent) const;
|
||||
// The volumes a parts operation moves: the selected volumes, or only the model parts when a whole instance is
|
||||
// selected. Empty for a cut object, whose parts must keep their place.
|
||||
std::vector<const GLVolume *> get_selected_part_volumes() const;
|
||||
// World bounding box of the items an operation moves, the reference of every non-parent operation.
|
||||
BoundingBoxf3 get_items_box() const;
|
||||
// World bounding box aligned to with align_parent: the instance of a parts selection, or the plate holding the
|
||||
// selected instances, up to their top.
|
||||
BoundingBoxf3 get_parent_box() const;
|
||||
// Number of items the operation would move, or 0 when it cannot move them; why_not then gets the reason, if any.
|
||||
size_t get_items_count(AlignType type, bool align_parent, std::string *why_not = nullptr) const;
|
||||
|
||||
private:
|
||||
GLCanvas3D& m_canvas;
|
||||
@@ -80,13 +93,7 @@ private:
|
||||
Selection& get_selection() const;
|
||||
bool take_snapshot(const std::string& name);
|
||||
void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement);
|
||||
void finish_operation(const std::string &operation_name, bool force_volume_move = false);
|
||||
|
||||
bool validate_selection_for_align() const;
|
||||
bool validate_selection_for_distribute() const;
|
||||
|
||||
private:
|
||||
BoundingBoxf3 m_parent_box;
|
||||
void finish_operation(bool force_volume_move = false);
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
|
||||
@@ -644,7 +644,8 @@ void GLGizmoFdmSupports::update_from_model_object(bool first_update)
|
||||
const TriangleMesh* mesh = &mv->mesh();
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
|
||||
m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false);
|
||||
// The colour table has NONE/ENFORCER/BLOCKER only: a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false, EnforcerBlockerType::BLOCKER);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
|
||||
//BBS: add timestamp logic
|
||||
|
||||
@@ -397,7 +397,8 @@ void GLGizmoFuzzySkin::update_from_model_object(bool first_update)
|
||||
const TriangleMesh* mesh = &mv->mesh();
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
|
||||
m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false);
|
||||
// Fuzzy skin paints a single state (FUZZY_SKIN): a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false, EnforcerBlockerType::FUZZY_SKIN);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoAlignment.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -109,6 +114,8 @@ void GLGizmoMove3D::on_set_state() {
|
||||
m_last_selected_volume_idx = -1;
|
||||
change_cs_by_selection();
|
||||
}
|
||||
if (m_object_manipulation)
|
||||
m_object_manipulation->reset_align_state();
|
||||
}
|
||||
|
||||
void GLGizmoMove3D::on_start_dragging()
|
||||
@@ -157,6 +164,8 @@ void GLGizmoMove3D::on_render()
|
||||
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
|
||||
render_align_preview();
|
||||
|
||||
const auto &[box, box_trafo] = selection.get_bounding_box_in_current_reference_system();
|
||||
m_bounding_box = box;
|
||||
m_center = box_trafo.translation();
|
||||
@@ -340,6 +349,93 @@ double GLGizmoMove3D::calc_projection(const UpdateData& data) const
|
||||
return projection;
|
||||
}
|
||||
|
||||
void GLGizmoMove3D::render_align_preview()
|
||||
{
|
||||
using AlignType = GLGizmoAlignment::AlignType;
|
||||
if (m_object_manipulation == nullptr || m_object_manipulation->get_hovered_align_type() == AlignType::NONE)
|
||||
return;
|
||||
|
||||
// The box the hovered operation aligns to, in world coordinates like the operation itself.
|
||||
const AlignType type = m_object_manipulation->get_hovered_align_type();
|
||||
const GLGizmoAlignment &helper = m_object_manipulation->get_alignment_helper();
|
||||
const BoundingBoxf3 box = m_object_manipulation->get_align_to_parent() ? helper.get_parent_box() : helper.get_items_box();
|
||||
if (!box.defined)
|
||||
return;
|
||||
m_parent.get_selection().render_bounding_box(box, {0.0f, 150.0f / 255.0f, 136.0f / 255.0f}, m_parent.get_scale());
|
||||
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
if (!m_align_plane.is_initialized()) {
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||||
init_data.reserve_vertices(4);
|
||||
init_data.reserve_indices(6);
|
||||
init_data.add_vertex(Vec3f(-0.5f, -0.5f, 0.0f));
|
||||
init_data.add_vertex(Vec3f(0.5f, -0.5f, 0.0f));
|
||||
init_data.add_vertex(Vec3f(0.5f, 0.5f, 0.0f));
|
||||
init_data.add_vertex(Vec3f(-0.5f, 0.5f, 0.0f));
|
||||
init_data.add_triangle(0, 1, 2);
|
||||
init_data.add_triangle(0, 2, 3);
|
||||
m_align_plane.init_from(std::move(init_data));
|
||||
m_align_plane.set_color({ 1.0f, 1.0f, 1.0f, 0.5f });
|
||||
}
|
||||
|
||||
// A plane across the box at its min, center or max on the hovered axis. Distributing shows up to six evenly
|
||||
// spaced planes.
|
||||
int axis = Z;
|
||||
double t = 0.5;
|
||||
switch (type) {
|
||||
case AlignType::X_MIN: axis = X; t = 0.0; break;
|
||||
case AlignType::X_MAX: axis = X; t = 1.0; break;
|
||||
case AlignType::CENTER_X:
|
||||
case AlignType::DISTRIBUTE_X: axis = X; break;
|
||||
case AlignType::Y_MIN: axis = Y; t = 0.0; break;
|
||||
case AlignType::Y_MAX: axis = Y; t = 1.0; break;
|
||||
case AlignType::CENTER_Y:
|
||||
case AlignType::DISTRIBUTE_Y: axis = Y; break;
|
||||
case AlignType::Z_MIN: t = 0.0; break;
|
||||
case AlignType::Z_MAX: t = 1.0; break;
|
||||
default: break;
|
||||
}
|
||||
const Vec3d size = box.size();
|
||||
std::vector<double> positions;
|
||||
if (type >= AlignType::DISTRIBUTE_X) {
|
||||
const size_t count = std::min<size_t>(helper.get_items_count(type, false), 6);
|
||||
for (size_t i = 1; i <= count; ++i)
|
||||
positions.push_back(box.min[axis] + i * size[axis] / (count + 1));
|
||||
} else
|
||||
positions.push_back(box.min[axis] + t * size[axis]);
|
||||
|
||||
const int u = (axis + 1) % 3;
|
||||
const int v = (axis + 2) % 3;
|
||||
Transform3d plane_matrix = Transform3d::Identity();
|
||||
plane_matrix.linear().col(0) = size[u] * Vec3d::Unit(u);
|
||||
plane_matrix.linear().col(1) = size[v] * Vec3d::Unit(v);
|
||||
plane_matrix.linear().col(2) = Vec3d::Unit(axis);
|
||||
Vec3d plane_center = box.center();
|
||||
|
||||
glsafe(::glDisable(GL_CULL_FACE));
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
// Translucent planes must hide neither each other nor the grabbers drawn after them.
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
shader->start_using();
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
for (double position : positions) {
|
||||
plane_center[axis] = position;
|
||||
plane_matrix.translation() = plane_center;
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * plane_matrix);
|
||||
m_align_plane.render();
|
||||
}
|
||||
shader->stop_using();
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
glsafe(::glEnable(GL_CULL_FACE));
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
void GLGizmoMove3D::change_cs_by_selection() {
|
||||
int obejct_idx, volume_idx;
|
||||
ModelVolume *model_volume = m_parent.get_selection().get_selected_single_volume(obejct_idx, volume_idx);
|
||||
|
||||
@@ -34,6 +34,8 @@ class GLGizmoMove3D : public GLGizmoBase
|
||||
Vec3d old_center{ Vec3d::Zero() };
|
||||
};
|
||||
std::array<GrabberConnection, 3> m_grabber_connections;
|
||||
// Unit square previewing the target plane of the hovered align or distribute icon.
|
||||
GLModel m_align_plane;
|
||||
|
||||
//BBS: add size adjust related
|
||||
GizmoObjectManipulation* m_object_manipulation;
|
||||
@@ -76,6 +78,7 @@ protected:
|
||||
|
||||
private:
|
||||
double calc_projection(const UpdateData& data) const;
|
||||
void render_align_preview();
|
||||
void change_cs_by_selection(); //cs mean Coordinate System
|
||||
private:
|
||||
int m_last_selected_obejct_idx, m_last_selected_volume_idx;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoSeam.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
//#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
@@ -327,7 +328,8 @@ void GLGizmoSeam::update_from_model_object(bool first_update)
|
||||
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
|
||||
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false);
|
||||
// The colour table has NONE/ENFORCER/BLOCKER only: a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false, EnforcerBlockerType::BLOCKER);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
#include <slic3r/GUI/AnimatedTexture.hpp>
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/CameraUtils.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
@@ -3739,7 +3740,8 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
|
||||
break;
|
||||
}
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
m_triangle_selectors.back()->deserialize(data, false);
|
||||
// The mask uses a single state (ENFORCER): a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(data, false, EnforcerBlockerType::ENFORCER);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
}
|
||||
|
||||
@@ -3829,6 +3831,189 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
||||
m_panel_icon_map[names[i]] = icons[i];
|
||||
}
|
||||
|
||||
// Seconds on a real clock, for the tooltips' own timing.
|
||||
//
|
||||
// Not ImGui::GetTime(): nothing in OrcaSlicer ever sets io.DeltaTime, so it keeps imgui's default of
|
||||
// 1/60 per frame and its clock counts frames rather than seconds. On a canvas that renders only when
|
||||
// something asks it to, that is not a clock at all - an animation driven by it crawled while the canvas
|
||||
// was idle and raced while a moving mouse was rendering it flat out.
|
||||
static double tip_now()
|
||||
{
|
||||
using clock = std::chrono::steady_clock;
|
||||
static const clock::time_point base = clock::now();
|
||||
return std::chrono::duration<double>(clock::now() - base).count();
|
||||
}
|
||||
|
||||
// How long the pointer has to rest on a control before its tooltip appears. The panel is dense and the
|
||||
// tooltip lands on the model beside it, so with no wait at all a sweep from the top of the panel down to
|
||||
// Bake flashes a dozen large panels over the scene. Once one has just been up the wait drops to almost
|
||||
// nothing, so reading several settings in a row stays quick - but not to nothing, or that same sweep
|
||||
// would flash again the moment one tooltip had been seen.
|
||||
static constexpr double TIP_DELAY = 0.45;
|
||||
static constexpr double TIP_DELAY_AGAIN = 0.10;
|
||||
static constexpr double TIP_STICKY = 0.70;
|
||||
|
||||
void GLGizmoTextureDisplacement::panel_tooltip(const wxString &text, const std::vector<TipMedia> &media)
|
||||
{
|
||||
panel_tooltip(into_u8(text), media);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::panel_tooltip(const std::string &text, const std::vector<TipMedia> &media)
|
||||
{
|
||||
// Nothing explains a control while the control is being used: dragging a slider, typing a value
|
||||
// into one, picking from a combo, dragging a layer card. The pointer sits on the very thing being
|
||||
// edited, so without this the tooltip covers the model exactly while the user is watching it change.
|
||||
// The full wait comes back afterwards rather than the short one - having just let go of a slider is
|
||||
// not a reason to be shown a picture of it.
|
||||
if (ImGui::IsAnyItemActive() || ImGui::IsAnyMouseDown()) {
|
||||
m_tip_item = ImVec2(-1.f, -1.f);
|
||||
m_tip_last_shown = -1e9;
|
||||
return;
|
||||
}
|
||||
|
||||
// The item's position stands in for its identity: moving to another control restarts the wait, and
|
||||
// staying on this one carries it on.
|
||||
const double now = tip_now();
|
||||
const ImVec2 item = ImGui::GetItemRectMin();
|
||||
if (item.x != m_tip_item.x || item.y != m_tip_item.y) {
|
||||
m_tip_item = item;
|
||||
m_tip_hover_since = now;
|
||||
}
|
||||
if (now - m_tip_hover_since < (now - m_tip_last_shown < TIP_STICKY ? TIP_DELAY_AGAIN : TIP_DELAY)) {
|
||||
// A pointer resting on a control produces no events at all, so unless the wait asks for frames
|
||||
// itself nothing redraws the canvas and the wait never finishes - the tooltip then appears only
|
||||
// on the next pixel of mouse movement.
|
||||
//
|
||||
// request_extra_frame() and not schedule_extra_frame(): the latter only arranges a timer, whose
|
||||
// event an idle event is assumed to follow, and GLCanvas3D::on_idle() renders nothing unless the
|
||||
// canvas was already dirty when it ran. request_extra_frame() ends on_idle with
|
||||
// evt.RequestMore(), which obliges wx to deliver the next one. It is what 3DBed's fade uses.
|
||||
// Costing a few hundred milliseconds of frames is the cheaper half of that trade.
|
||||
m_parent.request_extra_frame();
|
||||
return;
|
||||
}
|
||||
m_tip_last_shown = now;
|
||||
|
||||
// ImGui hides a window on the first frame it is submitted, to measure it before showing it - and
|
||||
// for a tooltip it does so every time one reappears, because the windows are recycled (see the
|
||||
// Popup|Tooltip branch of Begin()). So the frame on which a tooltip is decided is never the frame
|
||||
// on which it is seen, and one that then asks for nothing more stays invisible until the next pixel
|
||||
// of mouse movement happens to produce another frame. Keep frames coming until it has settled.
|
||||
const int shown_frame = ImGui::GetFrameCount();
|
||||
m_tip_settled = (shown_frame == m_tip_shown_frame + 1) ? m_tip_settled + 1 : 0;
|
||||
m_tip_shown_frame = shown_frame;
|
||||
if (m_tip_settled < 2)
|
||||
m_parent.request_extra_frame();
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float gap = m_imgui->scaled(0.5f);
|
||||
const float wrap = m_imgui->scaled(20.f);
|
||||
// The width has to be known here, not left to ImGui, because the tooltip is placed by its left edge
|
||||
// (below) and an illustration has to be laid out against something. One carrying illustrations takes
|
||||
// the full wrap width; a plain one takes exactly what its text needs, as it did when ImGui sized it,
|
||||
// so a two-word tooltip is not a wide box with two words in it.
|
||||
const float text_w = ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap).x;
|
||||
const float width = (media.empty() ? std::min(wrap, std::ceil(text_w)) : wrap) + 2.f * style.WindowPadding.x;
|
||||
|
||||
// Beside the anchor (the panel, or the texture picker when the tooltip comes from inside it), never
|
||||
// over it - the same reasoning, and the same placement, as the picker popup itself. ImGui normally
|
||||
// keeps a tooltip glued to the mouse; it honours a position set here instead (see the Orca branch in
|
||||
// imgui's Begin()), and still clamps the result inside the canvas.
|
||||
const bool left = m_tooltip_anchor_min.x - gap - width >= 0.f;
|
||||
const float x = left ? m_tooltip_anchor_min.x - gap - width : m_tooltip_anchor_max.x + gap;
|
||||
ImGui::SetNextWindowPos(ImVec2(x, ImGui::GetItemRectMin().y)); // level with the control it explains
|
||||
ImGui::SetNextWindowSize(ImVec2(width, 0.f)); // height still follows the content
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
|
||||
ImGui::PushTextWrapPos(0.f); // the window's own edge, which is what the fixed width above sets
|
||||
ImGui::TextUnformatted(text.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
render_tip_media(media, width - 2.f * style.WindowPadding.x);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_tip_media(const std::vector<TipMedia> &media, float width)
|
||||
{
|
||||
// Collect what is actually there first: the layout divides the width between the illustrations, so
|
||||
// one that is missing has to drop out of the count rather than leave a gap.
|
||||
std::vector<std::pair<const AnimatedTexture *, const TipMedia *>> shown;
|
||||
std::string key;
|
||||
for (const TipMedia &m : media) {
|
||||
const AnimatedTexture *texture =
|
||||
m_tip_media.get((boost::filesystem::path(resources_dir()) / "images" / "texture_displacement" / m.file).string());
|
||||
if (texture != nullptr && texture->is_valid() && texture->width() > 0 && texture->height() > 0) {
|
||||
shown.emplace_back(texture, &m);
|
||||
key += m.file + "|";
|
||||
}
|
||||
}
|
||||
if (shown.empty())
|
||||
return;
|
||||
|
||||
// Each animation plays from its first frame when its tooltip opens: these are short loops showing
|
||||
// one setting going from one value to another, and joining one halfway through shows the user the
|
||||
// end of a change they never saw the start of.
|
||||
// Restart on a different tooltip, and on the same one opening again - which is what a gap in the
|
||||
// frames it was drawn on means, since it is drawn on every frame for as long as it is on screen.
|
||||
const int this_frame = ImGui::GetFrameCount();
|
||||
if (key != m_tip_media_key || this_frame != m_tip_media_frame + 1) {
|
||||
m_tip_media_key = key;
|
||||
m_tip_media_start = tip_now();
|
||||
}
|
||||
m_tip_media_frame = this_frame;
|
||||
const double time = tip_now() - m_tip_media_start;
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float spacing = style.ItemSpacing.x;
|
||||
const float cell_w = (width - spacing * float(shown.size() - 1)) / float(shown.size());
|
||||
const ImU32 frame = ImGui::GetColorU32(ImGuiCol_Text, 0.25f);
|
||||
|
||||
// A single illustration would otherwise be drawn at the tooltip's full width, and a square one at
|
||||
// full width is a picture as tall as the text beside it is long. Capped and centred in its column.
|
||||
const float max_h = m_imgui->scaled(10.f);
|
||||
|
||||
ImGui::Spacing();
|
||||
for (size_t i = 0; i < shown.size(); ++i) {
|
||||
const AnimatedTexture *texture = shown[i].first;
|
||||
const TipMedia &entry = *shown[i].second;
|
||||
const float cell_x = ImGui::GetCursorPosX();
|
||||
|
||||
float w = cell_w;
|
||||
float h = std::round(cell_w * float(texture->height()) / float(texture->width()));
|
||||
if (h > max_h) {
|
||||
w = std::round(max_h * float(texture->width()) / float(texture->height()));
|
||||
h = max_h;
|
||||
}
|
||||
|
||||
ImGui::BeginGroup();
|
||||
const unsigned int id = texture->texture_id(time);
|
||||
ImGui::SetCursorPosX(cell_x + std::round(0.5f * (cell_w - w)));
|
||||
ImGui::Image((ImTextureID) (intptr_t) id, ImVec2(w, h));
|
||||
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), frame, style.FrameRounding);
|
||||
// The canvas only redraws on demand, so an animation has to ask for its own frames. It asks for
|
||||
// them as fast as they come rather than pacing itself with a timer: which frame is drawn is
|
||||
// decided by the clock above, so a redraw that arrives early simply draws the same frame again,
|
||||
// and a guaranteed frame is worth more here than a frugal one that may never arrive.
|
||||
if (texture->is_animated())
|
||||
m_parent.request_extra_frame();
|
||||
|
||||
if (!entry.caption.empty()) {
|
||||
// Centred under its own illustration, and never wider than its column.
|
||||
const std::string caption = into_u8(entry.caption);
|
||||
const float text_w = ImGui::CalcTextSize(caption.c_str()).x;
|
||||
ImGui::SetCursorPosX(cell_x + std::max(0.f, (cell_w - text_w) * 0.5f));
|
||||
ImGui::PushTextWrapPos(cell_x + cell_w);
|
||||
ImGui::TextDisabled("%s", caption.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
if (i + 1 < shown.size())
|
||||
ImGui::SameLine(0.f, spacing);
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::add_texture_layer()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -3978,6 +4163,14 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
return;
|
||||
}
|
||||
|
||||
// While this popup is up its own tooltips belong beside it, not beside the panel it is already
|
||||
// covering the left of. Restored before every exit below.
|
||||
const ImVec2 prev_anchor_min = m_tooltip_anchor_min;
|
||||
const ImVec2 prev_anchor_max = m_tooltip_anchor_max;
|
||||
m_tooltip_anchor_min = ImGui::GetWindowPos();
|
||||
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
|
||||
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
|
||||
|
||||
m_imgui->push_bold_font();
|
||||
m_imgui->text(_L("Choose a texture"));
|
||||
m_imgui->pop_bold_font();
|
||||
@@ -4036,7 +4229,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
if (entry.path == layer->path)
|
||||
dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f);
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(entry.name, m_imgui->scaled(18.f));
|
||||
panel_tooltip(entry.name);
|
||||
|
||||
if (user && ImGui::IsMouseHoveringRect(a, b)) {
|
||||
const float r = std::round(tile * 0.17f);
|
||||
@@ -4049,7 +4242,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
dl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), IM_COL32_WHITE, 1.5f);
|
||||
dl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), IM_COL32_WHITE, 1.5f);
|
||||
if (hot)
|
||||
m_imgui->tooltip(_u8L("Remove from My textures"), m_imgui->scaled(18.f));
|
||||
panel_tooltip(_u8L("Remove from My textures"));
|
||||
if (remove_clicked)
|
||||
to_remove = entry;
|
||||
}
|
||||
@@ -4076,10 +4269,9 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
|
||||
import_clicked = true;
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
|
||||
"bakes from and saved to your personal texture folder, kept separate from the "
|
||||
"textures shipped with OrcaSlicer."),
|
||||
m_imgui->scaled(20.f));
|
||||
panel_tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
|
||||
"bakes from and saved to your personal texture folder, kept separate from the "
|
||||
"textures shipped with OrcaSlicer."));
|
||||
end_grid(origin, count + 1);
|
||||
}
|
||||
|
||||
@@ -4089,6 +4281,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
||||
|
||||
if (chosen || import_clicked)
|
||||
ImGui::CloseCurrentPopup();
|
||||
m_tooltip_anchor_min = prev_anchor_min;
|
||||
m_tooltip_anchor_max = prev_anchor_max;
|
||||
ImGui::EndPopup();
|
||||
|
||||
// Only once the loop over the library is done: importing, picking (which reorders the recently used)
|
||||
@@ -6065,18 +6259,16 @@ void GLGizmoTextureDisplacement::render_debug_stage_panel(ModelVolume *mv)
|
||||
run_stage_debug();
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Run the same bake the Bake button runs, keeping the mesh after every "
|
||||
"stage, and step through them here. Nothing is committed to the model - "
|
||||
"this is for finding which stage a bad result came from."),
|
||||
m_imgui->scaled(20.f));
|
||||
panel_tooltip(_u8L("Run the same bake the Bake button runs, keeping the mesh after every "
|
||||
"stage, and step through them here. Nothing is committed to the model - "
|
||||
"this is for finding which stage a bad result came from."));
|
||||
|
||||
ImGui::SameLine();
|
||||
m_imgui->bbl_checkbox(_L("Check topology"), m_debug_check_topology);
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Count open and non-manifold edges after each stage, which is how a stage "
|
||||
"that tore the mesh is spotted. It scans every edge, so it adds noticeably "
|
||||
"to the capture on a fine bake."),
|
||||
m_imgui->scaled(20.f));
|
||||
panel_tooltip(_u8L("Count open and non-manifold edges after each stage, which is how a stage "
|
||||
"that tore the mesh is spotted. It scans every edge, so it adds noticeably "
|
||||
"to the capture on a fine bake."));
|
||||
|
||||
if (m_debug_stages.empty())
|
||||
return;
|
||||
@@ -6144,9 +6336,8 @@ void GLGizmoTextureDisplacement::render_debug_stage_panel(ModelVolume *mv)
|
||||
show_error(nullptr, _u8L("Could not write the stage meshes."));
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_u8L("Write every captured stage out as an OBJ file, so they can be opened "
|
||||
"side by side in a mesh viewer."),
|
||||
m_imgui->scaled(20.f));
|
||||
panel_tooltip(_u8L("Write every captured stage out as an OBJ file, so they can be opened "
|
||||
"side by side in a mesh viewer."));
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareParams ¶ms,
|
||||
@@ -6273,6 +6464,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
|
||||
GizmoImguiBegin(get_name(), flags);
|
||||
// Where this frame's tooltips place themselves (see panel_tooltip()). The size is last frame's -
|
||||
// this window auto-resizes, so its height is only known once its content has been laid out - which
|
||||
// is close enough for a tooltip that needs a frame of hovering to appear in the first place.
|
||||
m_tooltip_anchor_min = ImGui::GetWindowPos();
|
||||
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
|
||||
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
|
||||
ensure_panel_icons();
|
||||
process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current
|
||||
// After the pane's commands, which can close it or change the view, and before the View row, so the row
|
||||
@@ -6299,7 +6496,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const float gap_s = std::round(m_imgui->scaled(0.4f));
|
||||
const float label_w = m_imgui->scaled(4.8f); // label column of a label + slider row
|
||||
const float card_pad = std::round(m_imgui->scaled(0.55f));
|
||||
const float wrap_w = m_imgui->scaled(20.f);
|
||||
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
|
||||
const ImVec4 col_link = ImGuiWrapper::COL_ORCA;
|
||||
const ImVec4 col_frame = ImGui::GetStyleColorVec4(ImGuiCol_Separator);
|
||||
@@ -6313,9 +6509,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
return changed;
|
||||
};
|
||||
const auto hover_tip = [&](const auto &text) {
|
||||
const auto hover_tip = [&](const auto &text, const std::vector<TipMedia> &media = {}) {
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(text, wrap_w);
|
||||
panel_tooltip(text, media);
|
||||
};
|
||||
|
||||
// Framed icon toggle: a 1 px frame, and when on a teal frame over a teal tint with the icon in its
|
||||
@@ -6323,7 +6519,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// `unavailable`, when not empty, draws it faded but still hoverable, so the tooltip can say why it
|
||||
// cannot be used - a disabled ImGui item would swallow the hover and leave the user guessing.
|
||||
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, float sz, const wxString &label,
|
||||
const wxString &tip_text, const wxString &unavailable = wxString()) -> bool {
|
||||
const wxString &tip_text, const std::vector<TipMedia> &media = {},
|
||||
const wxString &unavailable = wxString()) -> bool {
|
||||
const bool na = !unavailable.empty();
|
||||
const auto it = m_panel_icon_map.find(iconfile);
|
||||
const float pad = std::max(1.f, std::round(sz * 0.14f));
|
||||
@@ -6350,12 +6547,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopID();
|
||||
if (ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(na ? tip_text + "\n\n" + unavailable : tip_text, wrap_w);
|
||||
panel_tooltip(na ? tip_text + "\n\n" + unavailable : tip_text, media);
|
||||
return clicked && !na;
|
||||
};
|
||||
// Borderless icon button (non-toggle): always the grey monochrome variant.
|
||||
const auto icon_button = [&](int uid, const std::string &iconfile, float sz, const wxString &label,
|
||||
const wxString &tip_text) -> bool {
|
||||
const wxString &tip_text, const std::vector<TipMedia> &media = {}) -> bool {
|
||||
const auto it = m_panel_icon_map.find(iconfile);
|
||||
const float pad = std::max(1.f, std::round(sz * 0.14f));
|
||||
bool clicked = false;
|
||||
@@ -6373,7 +6570,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopID();
|
||||
if (!tip_text.empty() && ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(tip_text, wrap_w);
|
||||
panel_tooltip(tip_text, media);
|
||||
return clicked;
|
||||
};
|
||||
// A short vertical rule between groups of buttons on one row; call it right after an item.
|
||||
@@ -6452,10 +6649,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
};
|
||||
|
||||
// Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right
|
||||
// edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the
|
||||
// range go on both. Returns whatever the slider returned.
|
||||
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range,
|
||||
const std::function<bool()> &slider) -> bool {
|
||||
// edge, and the caller has already left edit_icon_room in it), then the edit icon. The range goes on
|
||||
// both tooltips; `tip` and its illustrations `media` on the slider's. Returns whatever the slider returned.
|
||||
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::vector<TipMedia> &media,
|
||||
const std::string &range, const std::function<bool()> &slider) -> bool {
|
||||
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
|
||||
// frame, so it has to be issued before the slider it names, not from the icon beside it.
|
||||
if (m_slider_type_request == id) {
|
||||
@@ -6471,7 +6668,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
wxString full = tip;
|
||||
if (!full.IsEmpty())
|
||||
full += "\n\n";
|
||||
m_imgui->tooltip(full + from_u8(range), wrap_w);
|
||||
panel_tooltip(full + from_u8(range), media);
|
||||
}
|
||||
// SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was
|
||||
// drawn at), so an icon of the same height lands on the track wherever the row placed it.
|
||||
@@ -6488,15 +6685,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// already given, so no row states its bounds or its format twice and the tooltip cannot drift from
|
||||
// the track. `item_w` is as typed_row() takes it.
|
||||
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format,
|
||||
bool log, const wxString &tip) -> bool {
|
||||
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
||||
bool log, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
||||
return typed_row(id, item_w, tip, media, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
||||
return ImGui::SliderFloat(id, v, v_min, v_max, format,
|
||||
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
||||
});
|
||||
};
|
||||
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format,
|
||||
bool log, const wxString &tip) -> bool {
|
||||
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
||||
bool log, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
||||
return typed_row(id, item_w, tip, media, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
||||
return ImGui::SliderInt(id, v, v_min, v_max, format,
|
||||
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
||||
});
|
||||
@@ -6504,14 +6701,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
// Label column + the slider filling the rest of the row.
|
||||
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
|
||||
bool log, float right_inset, const wxString &tip) -> bool {
|
||||
bool log, float right_inset, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
||||
slider_label(label);
|
||||
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip);
|
||||
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip, media);
|
||||
};
|
||||
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
|
||||
float right_inset, const wxString &tip) -> bool {
|
||||
float right_inset, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
||||
slider_label(label);
|
||||
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip);
|
||||
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip, media);
|
||||
};
|
||||
const auto layer_name = [](const TextureDisplacementLayer &l) {
|
||||
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
|
||||
@@ -6667,11 +6864,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SetCursorPosX(toggles_x);
|
||||
if (icon_toggle(820, "texture_displacement_add.svg", !m_erase_mode, icon_sm, _L("Paint"),
|
||||
_L("Paint - add the active layer where you click or drag. The right button erases, and so does "
|
||||
"holding Shift.")))
|
||||
"holding Shift."),
|
||||
{ { "paint.gif" } }))
|
||||
m_erase_mode = false;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(821, "texture_displacement_add_negative.svg", m_erase_mode, icon_sm, _L("Erase"),
|
||||
_L("Erase - remove the active layer where you click or drag. The right button paints.")))
|
||||
_L("Erase - remove the active layer where you click or drag. The right button paints."),
|
||||
{ { "erase.gif" } }))
|
||||
m_erase_mode = true;
|
||||
}
|
||||
|
||||
@@ -6685,20 +6884,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const bool is_area_mode = m_tool_type == ToolType::SMART_FILL;
|
||||
const float row_y = ImGui::GetCursorPosY();
|
||||
if (icon_toggle(801, "texture_displacement_brush.svg", is_brush_mode, icon_md, _L("Brush"),
|
||||
_L("Brush - paint over the surface by dragging"))) {
|
||||
_L("Brush - paint over the surface by dragging"), { { "paint.gif" } })) {
|
||||
m_tool_type = ToolType::BRUSH;
|
||||
if (m_cursor_type == TriangleSelector::CursorType::POINTER)
|
||||
m_cursor_type = TriangleSelector::CursorType::CIRCLE;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"))) {
|
||||
if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"),
|
||||
{ { "tool_face.gif" } })) {
|
||||
m_tool_type = ToolType::BRUSH;
|
||||
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(803, "texture_displacement_connected_area.svg", is_area_mode, icon_md, _L("Connected area"),
|
||||
_L("Connected area - flood-fill the region reachable without crossing an edge sharper than the "
|
||||
"angle threshold"))) {
|
||||
"angle threshold"),
|
||||
{ { "tool_smart_fill.gif" } })) {
|
||||
m_tool_type = ToolType::SMART_FILL;
|
||||
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
||||
}
|
||||
@@ -6712,11 +6913,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
wxString();
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(806, "texture_displacement_select_all.svg", false, icon_md, _L("Select whole model"),
|
||||
_L("Select whole model - paint every face of the model with the active layer"), whole_na))
|
||||
_L("Select whole model - paint every face of the model with the active layer"),
|
||||
{ { "select_all.png" } }, whole_na))
|
||||
select_whole_model();
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(807, "texture_displacement_erase_all.svg", false, icon_md, _L("Erase whole model"),
|
||||
_L("Erase whole model - clear the active layer's paint from every face"), erase_na)) {
|
||||
_L("Erase whole model - clear the active layer's paint from every face"),
|
||||
{ { "erase_all.png" } }, erase_na)) {
|
||||
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"),
|
||||
UndoRedo::SnapshotType::GizmoAction);
|
||||
int idx = -1;
|
||||
@@ -6738,7 +6941,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
centre_on_row(frame_h);
|
||||
typed_slider("##cursor_radius",
|
||||
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
|
||||
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"));
|
||||
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"),
|
||||
{ { "cursor_size.gif" } });
|
||||
vsep(icon_sm);
|
||||
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
|
||||
centre_on_row(icon_sm);
|
||||
@@ -6754,7 +6958,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
centre_on_row(frame_h);
|
||||
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
|
||||
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false,
|
||||
_L("Angle threshold - the fill stops at edges sharper than this"));
|
||||
_L("Angle threshold - the fill stops at edges sharper than this"),
|
||||
{ { "smart_fill_angle_low.png", _L("Low") }, { "smart_fill_angle_high.png", _L("High") } });
|
||||
} else {
|
||||
centre_on_row(frame_h);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -6790,19 +6995,21 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f)));
|
||||
if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"),
|
||||
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update")))
|
||||
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update"),
|
||||
{ { "tile_on.png" } }))
|
||||
new_mode = picked = 0;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"),
|
||||
_L("Fast - the relief is only shaded on, not built, and only for the layer you are "
|
||||
"editing. Updates instantly while you paint")))
|
||||
"editing. Updates instantly while you paint"),
|
||||
{ { "view_fast.png" } }))
|
||||
new_mode = picked = 1;
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
||||
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
||||
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
{ { "view_checker.png" } }, uv_view_na)) {
|
||||
new_mode = picked = 2;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
@@ -6810,13 +7017,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
||||
"it is closed"),
|
||||
uv_view_na)) {
|
||||
{ { "view_distortion.png" } }, uv_view_na)) {
|
||||
new_mode = picked = 3;
|
||||
open_uv_editor = true;
|
||||
}
|
||||
vsep(icon_md);
|
||||
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above")))
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above"),
|
||||
{ { "view_wireframe.png" } }))
|
||||
wf_toggle = true;
|
||||
|
||||
const std::string auto_label = _u8L("Auto");
|
||||
@@ -7037,7 +7245,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
|
||||
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad,
|
||||
_L("Height of the relief: how far white in the texture lifts the surface, in "
|
||||
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
|
||||
"millimetres. Black does not move it at all, unless Midlevel says otherwise."),
|
||||
{ { "depth.gif" } });
|
||||
// Mapped by the model's own UV layout there is no millimetre scale to set: the layout is the
|
||||
// placement (see TextureProjectionMethod::ModelUV), so the slider would move nothing. The row
|
||||
// states why rather than hiding: float_row()'s tooltip is shown for a disabled slider too.
|
||||
@@ -7049,11 +7258,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"copy of the texture covers it exactly, so there is no tile size to set.") :
|
||||
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
|
||||
"more often and makes its detail finer; with Tile off, this is the size of the "
|
||||
"single copy."));
|
||||
"single copy."),
|
||||
tile_size_na ? std::vector<TipMedia>{} :
|
||||
std::vector<TipMedia>{ { "tile_size_large.png", _L("Larger") },
|
||||
{ "tile_size_small.png", _L("Smaller") } });
|
||||
m_imgui->disabled_end();
|
||||
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
|
||||
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
|
||||
"edge of the model."));
|
||||
"edge of the model."),
|
||||
{ { "rotation.gif" } });
|
||||
|
||||
if (m_layer_expanded[size_t(layer.slot)]) {
|
||||
{
|
||||
@@ -7073,7 +7286,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
|
||||
"still push out - one image both embosses and engraves.\n\n"
|
||||
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
|
||||
"cut can pass through the other side."));
|
||||
"cut can pass through the other side."),
|
||||
{ { "midlevel.gif" } });
|
||||
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
|
||||
ImGui::PushTextWrapPos(content_rx - card_pad);
|
||||
m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
|
||||
@@ -7083,12 +7297,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad,
|
||||
_L("Blurs the image before it is used, which rounds off hard steps and removes "
|
||||
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
|
||||
"costs fine detail."));
|
||||
"costs fine detail."),
|
||||
{ { "smoothing_off.png", _L("Off") }, { "smoothing_on.png", _L("On") } });
|
||||
|
||||
// Edge fade: the relief flattens toward the boundary of the painted area.
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
|
||||
hover_tip(_u8L("Flattens the relief as it approaches the edge of the painted area, so it "
|
||||
"blends into the bare surface instead of stopping at a step."));
|
||||
"blends into the bare surface instead of stopping at a step."),
|
||||
{ { "edge_fade_off.png", _L("Off") }, { "edge_fade_on.png", _L("On") } });
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(!layer.edge_smoothing);
|
||||
m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room),
|
||||
@@ -7102,7 +7318,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Invert"), layer.invert);
|
||||
hover_tip(_u8L("Turns the relief inside out: what stood out is cut in, and the other way "
|
||||
"round. The same as using a negative of the image."));
|
||||
"round. The same as using a negative of the image."),
|
||||
{ { "invert_off.png", _L("Off") }, { "invert_on.png", _L("On") } });
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0)));
|
||||
|
||||
@@ -7118,13 +7335,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(has_color ?
|
||||
_u8L("Prints the painted area in the texture's colors as well as its "
|
||||
"relief. Each color is matched to the nearest of your loaded "
|
||||
"filaments; anything you did not paint keeps the object's own.") :
|
||||
_u8L("This texture is a grayscale height map, so it has no colors to "
|
||||
"apply. Import a color image to use this."),
|
||||
wrap_w);
|
||||
panel_tooltip(has_color ?
|
||||
_u8L("Prints the painted area in the texture's colors as well as its "
|
||||
"relief. Each color is matched to the nearest of your loaded "
|
||||
"filaments; anything you did not paint keeps the object's own.") :
|
||||
_u8L("This texture is a grayscale height map, so it has no colors to "
|
||||
"apply. Import a color image to use this."));
|
||||
|
||||
// The rest of colour belongs to the whole stack, not to this layer, so it only appears once -
|
||||
// under whichever layer turned colour on.
|
||||
@@ -7134,7 +7350,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty = true;
|
||||
hover_tip(_u8L("Interleaves two filaments to fake the colors in between, so a handful "
|
||||
"of filaments can cover a photo or a gradient. An image of flat colors "
|
||||
"prints the same either way. Off uses one filament per area."));
|
||||
"prints the same either way. Off uses one filament per area."),
|
||||
{ { "mix_off.png", _L("Off") }, { "mix_on.png", _L("On") } });
|
||||
if (opts.color_mix_enabled) {
|
||||
cached_palette(); // brings m_palette_filaments up to date
|
||||
// Every mix can become a filament slot, and there are only so many beside the
|
||||
@@ -7142,7 +7359,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
|
||||
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
|
||||
_L("The most mixed filaments a bake may add. They are picked from the "
|
||||
"texture's colors, and only the ones the bake actually uses are created.")))
|
||||
"texture's colors, and only the ones the bake actually uses are created."),
|
||||
{ { "mixed_colors_few.png", _L("Few") }, { "mixed_colors_many.png", _L("Many") } }))
|
||||
m_preview_params_dirty = true;
|
||||
// After the slider, so a change shows in the same frame.
|
||||
const int n_colors = int(cached_palette().size());
|
||||
@@ -7156,7 +7374,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
|
||||
_L("Cleans up single stray triangles of the wrong color, which detail finer "
|
||||
"than the mesh leaves behind. Raise it if the result looks speckled, "
|
||||
"lower it if small features are being swallowed.")))
|
||||
"lower it if small features are being swallowed."),
|
||||
{ { "denoise_off.png", _L("Off") }, { "denoise_on.png", _L("On") } }))
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
}
|
||||
@@ -7173,26 +7392,33 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const char *file;
|
||||
wxString label;
|
||||
wxString tip;
|
||||
const char *picture; // the same ball with the checker laid on it this way, or null for none
|
||||
};
|
||||
const MappingIcon mappings[] = {
|
||||
{ "menu_obj_cube.svg", _L("Triplanar (blended)"),
|
||||
_L("Triplanar - projects the texture from all three axes at once and blends between them, so a "
|
||||
"patch wrapping around a sharp edge has no seam.") },
|
||||
"patch wrapping around a sharp edge has no seam."),
|
||||
"map_cube.png" },
|
||||
{ "menu_obj_cylinder.svg", _L("Cylindrical"),
|
||||
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes.") },
|
||||
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes."),
|
||||
"map_cylinder.png" },
|
||||
{ "menu_obj_sphere.svg", _L("Spherical"),
|
||||
_L("Spherical - wraps the texture around the painted area's own center in both directions.") },
|
||||
_L("Spherical - wraps the texture around the painted area's own center in both directions."),
|
||||
"map_sphere.png" },
|
||||
{ "texture_displacement_map_unwrap.svg", _L("Unwrap (LSCM)"),
|
||||
_L("Unwrap - flattens the painted area and maps the texture onto it with as little stretching as "
|
||||
"possible. The area is cut into pieces at its sharp edges first (see Seam angle), so each "
|
||||
"piece can lie flat on its own.") },
|
||||
"piece can lie flat on its own."),
|
||||
"map_unwrap.png" },
|
||||
{ "texture_displacement_map_view.svg", _L("From view"),
|
||||
_L("From view - projects straight onto the painted area from where you are looking, like a slide "
|
||||
"projector.") },
|
||||
"projector."),
|
||||
"map_view.png" },
|
||||
{ "texture_displacement_map_model_uv.svg", _L("Model UV"),
|
||||
_L("Model UV - uses the texture coordinates the model itself came with, so a texture made for "
|
||||
"that model lands exactly where it was painted. Nothing is unwrapped, and Tile size does "
|
||||
"not apply: the model's own layout is the placement.") },
|
||||
"not apply: the model's own layout is the placement."),
|
||||
nullptr },
|
||||
};
|
||||
// Only a volume that actually carries a UV channel can be mapped by it. Shown
|
||||
// greyed out rather than hidden, so the reason is visible instead of the control
|
||||
@@ -7207,8 +7433,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
const auto method = static_cast<TextureProjectionMethod>(mi);
|
||||
const wxString na = (method == TextureProjectionMethod::ModelUV) ? model_uv_na : wxString();
|
||||
const std::vector<TipMedia> picture = mappings[mi].picture != nullptr ?
|
||||
std::vector<TipMedia>{ { mappings[mi].picture } } :
|
||||
std::vector<TipMedia>{};
|
||||
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
|
||||
mappings[mi].tip, na) &&
|
||||
mappings[mi].tip, picture, na) &&
|
||||
layer.projection_method != method) {
|
||||
// Capture the current view the moment "From view" is chosen, so it does something
|
||||
// sensible immediately rather than projecting from a stale/default direction.
|
||||
@@ -7313,19 +7542,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Tile") + "##tile_enabled", layer.tile_enabled);
|
||||
hover_tip(_u8L("Repeats the texture across the painted area. Off places one copy, like a decal, "
|
||||
"at the size set by Tile size."));
|
||||
"at the size set by Tile size."),
|
||||
{ { "tile_off.png", _L("Off") }, { "tile_on.png", _L("On") } });
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
const wxString tile_na = layer.tile_enabled ? wxString() : _L("Turn Tile on to choose how the texture repeats.");
|
||||
if (icon_toggle(840, "texture_displacement_tile_repeat.svg", layer.tile_method == static_cast<TextureTileMethod>(0),
|
||||
icon_sm, _L("Repeat"), _L("Repeat"), tile_na)) {
|
||||
icon_sm, _L("Repeat"), _L("Repeat"), { { "tile_repeat.png" } }, tile_na)) {
|
||||
layer.tile_method = static_cast<TextureTileMethod>(0);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(841, "menu_mirror_x.svg", layer.tile_method == static_cast<TextureTileMethod>(1), icon_sm,
|
||||
_L("Mirrored repeat"), _L("Mirrored repeat - every other tile is flipped, so edges meet seamlessly"),
|
||||
tile_na)) {
|
||||
{ { "tile_mirror.png" } }, tile_na)) {
|
||||
layer.tile_method = static_cast<TextureTileMethod>(1);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
@@ -7370,15 +7600,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(base_layer ?
|
||||
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
|
||||
_u8L("What this layer does where it overlaps the ones below. Add and "
|
||||
"Subtract pile relief on or carve it away. Override replaces them, "
|
||||
"so only this layer's relief is left where it is painted. Multiply "
|
||||
"and Divide scale what is underneath, which turns this layer into a "
|
||||
"mask over it - there, Depth acts as a strength, and 1 mm leaves "
|
||||
"white areas untouched."),
|
||||
wrap_w);
|
||||
panel_tooltip(base_layer ?
|
||||
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
|
||||
_u8L("What this layer does where it overlaps the ones below. Add and "
|
||||
"Subtract pile relief on or carve it away. Override replaces them, "
|
||||
"so only this layer's relief is left where it is painted. Multiply "
|
||||
"and Divide scale what is underneath, which turns this layer into a "
|
||||
"mask over it - there, Depth acts as a strength, and 1 mm leaves "
|
||||
"white areas untouched."),
|
||||
base_layer ? std::vector<TipMedia>{} :
|
||||
std::vector<TipMedia>{ { "blend_add.png", from_u8(blend_add) },
|
||||
{ "blend_multiply.png", from_u8(blend_multiply) } });
|
||||
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (icon_toggle(842, "texture_displacement_adjust.svg", m_adjust_texture_mode, icon_sm, _L("Adjust placement"),
|
||||
@@ -7561,7 +7793,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
hover_tip(_u8L("Adds triangles only where you painted, instead of everywhere. On a large part with a small "
|
||||
"decal that is the difference between thousands of triangles and millions, and your paint "
|
||||
"survives the refinement."));
|
||||
"survives the refinement."),
|
||||
{ { "painted_only_off.png", _L("Off") }, { "painted_only_on.png", _L("On") } });
|
||||
|
||||
if (m_subdivide_adaptive) {
|
||||
if (m_subdivide_target_mm <= 0.f && mv != nullptr) {
|
||||
@@ -7589,7 +7822,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
preview_live();
|
||||
hover_tip(_u8L("Spends the triangles where the texture actually bends - packed along ridges and edges, "
|
||||
"sparse over flat ground - instead of spreading them evenly. The same detail for fewer "
|
||||
"triangles on most textures."));
|
||||
"triangles on most textures."),
|
||||
{ { "adaptive_off.png", _L("Off") }, { "adaptive_on.png", _L("On") } });
|
||||
|
||||
// The edge-length target is a baseline in both modes. In feature mode it is what guarantees the
|
||||
// curvature test can actually see the texture: left too coarse, a big triangle over a fine
|
||||
@@ -7642,7 +7876,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f,
|
||||
_L("Splits every triangle of the model into four, this many times over. Each step "
|
||||
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
|
||||
"only the painted area unless you need the whole model finer."))) {
|
||||
"only the painted area unless you need the whole model finer."),
|
||||
{ { "subdivide.gif" } })) {
|
||||
if (m_subdivide_editing)
|
||||
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
|
||||
m_parent.set_as_dirty();
|
||||
@@ -7710,12 +7945,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f,
|
||||
_L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
|
||||
"even mesh; this is what makes one out of an uneven import."));
|
||||
"even mesh; this is what makes one out of an uneven import."),
|
||||
{ { "remesh_before.png", _L("Before") }, { "remesh_after.png", _L("After") } });
|
||||
|
||||
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
|
||||
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
|
||||
"the remesher slides vertices along the surface and rounds every crisp edge off - "
|
||||
"a cube comes back with wobbly edges."));
|
||||
"a cube comes back with wobbly edges."),
|
||||
{ { "preserve_edges_off.png", _L("Off") }, { "preserve_edges_on.png", _L("On") } });
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
|
||||
typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false,
|
||||
@@ -7730,7 +7967,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big "
|
||||
"ones and merging the small ones - so displacement has an even density to work with. "
|
||||
"Replaces the geometry; your paint is carried onto the new triangles spatially, so it "
|
||||
"survives (already-baked relief is kept too)."));
|
||||
"survives (already-baked relief is kept too)."),
|
||||
{ { "remesh_before.png", _L("Before") }, { "remesh_after.png", _L("After") } });
|
||||
|
||||
// Settings for the whole stack rather than one layer. Standard mode pins them instead of showing them.
|
||||
if (mv != nullptr) {
|
||||
@@ -7746,7 +7984,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Smooth result"), opts.smooth_enabled);
|
||||
hover_tip(_u8L("Smooths the geometry after the texture has been applied, to take the hard steps out of a "
|
||||
"low-resolution image. Only what the displacement moved is touched. The Smoothing slider "
|
||||
"on a layer is a different thing: it blurs the image before it is used."));
|
||||
"on a layer is a different thing: it blurs the image before it is used."),
|
||||
{ { "geometry_smoothing.gif" } });
|
||||
if (opts.smooth_enabled) {
|
||||
float percent = opts.smooth_strength * 100.f;
|
||||
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f,
|
||||
@@ -7806,7 +8045,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"Relief already baked into the rest of the model is kept on top of it, so a "
|
||||
"second bake somewhere else gets the same budget as the first. Raise it if the "
|
||||
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
|
||||
"the refinement produced, however many that is."))) {
|
||||
"the refinement produced, however many that is."),
|
||||
{ { "subdivide.gif" } })) {
|
||||
opts.v2_max_triangles_k = shown_k;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
@@ -7825,7 +8065,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Collapse the thin triangles refinement inherits from the model's own "
|
||||
"tessellation, before displacement samples them. A sliver's three corners "
|
||||
"land on three unrelated parts of the texture, which is what makes the "
|
||||
"relief look jagged."));
|
||||
"relief look jagged."),
|
||||
{ { "collapse_slivers_off.png", _L("Off") }, { "collapse_slivers_on.png", _L("On") } });
|
||||
}
|
||||
// Below both pipelines' own settings, because it replays whichever of them is selected.
|
||||
if (SHOW_PIPELINE_DEV_CONTROLS)
|
||||
@@ -7926,8 +8167,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// Held in a local: the format outlives the call that reads it, and the slider and the
|
||||
// range in its tooltip are then formatted the same way.
|
||||
const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm");
|
||||
const bool v2edge_chosen = auto_res && rec.edge_mm > 0.f;
|
||||
const wxString v2edge_tip =
|
||||
auto_res && rec.edge_mm > 0.f ?
|
||||
v2edge_chosen ?
|
||||
from_u8(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
|
||||
"budget %2% k. Untick Auto to set them yourself."),
|
||||
rec.edge_mm, rec.budget_k)) :
|
||||
@@ -7938,7 +8180,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"back down and the detail goes with it.");
|
||||
m_imgui->disabled_begin(busy || auto_res);
|
||||
if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f,
|
||||
v2edge_format.c_str(), true, v2edge_tip)) {
|
||||
v2edge_format.c_str(), true, v2edge_tip,
|
||||
v2edge_chosen ? std::vector<TipMedia>{} : std::vector<TipMedia>{ { "subdivide.gif" } })) {
|
||||
opts.v2_refine_mm = shown;
|
||||
m_preview_params_dirty = true;
|
||||
m_parent.set_as_dirty();
|
||||
@@ -7980,7 +8223,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
&m_subdivide_budget_k, 10, 2000, "+%d k", true,
|
||||
_L("How many thousand triangles the refinement may add to the model. The triangle "
|
||||
"that fits worst is always split first, so even a run that spends the lot has "
|
||||
"spent it where it shows most. Raise it if the relief still looks coarse."))) {
|
||||
"spent it where it shows most. Raise it if the relief still looks coarse."),
|
||||
{ { "subdivide.gif" } })) {
|
||||
if (m_subdivide_editing)
|
||||
rebuild_subdivide_preview();
|
||||
m_parent.set_as_dirty();
|
||||
@@ -8037,18 +8281,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
m_imgui->pop_bold_font();
|
||||
GLGizmoUtils::pop_orca_button_style();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ?
|
||||
(m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so "
|
||||
"strokes on the model mark seams instead of painting - turn "
|
||||
"it off in the pane to paint.") :
|
||||
_u8L("Nothing is painted yet.")) :
|
||||
pro_mode() ?
|
||||
_u8L("Turn the painted height maps into real geometry, by moving the vertices that are "
|
||||
"already there. Use Subdivide first if the mesh is too coarse to show the detail.") :
|
||||
_u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even "
|
||||
"density and refined where the texture bends first, so the detail has vertices to "
|
||||
"land on - all in one step."),
|
||||
wrap_w);
|
||||
panel_tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ?
|
||||
(m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so "
|
||||
"strokes on the model mark seams instead of painting - turn "
|
||||
"it off in the pane to paint.") :
|
||||
_u8L("Nothing is painted yet.")) :
|
||||
pro_mode() ?
|
||||
_u8L("Turn the painted height maps into real geometry, by moving the vertices that are "
|
||||
"already there. Use Subdivide first if the mesh is too coarse to show the detail.") :
|
||||
_u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even "
|
||||
"density and refined where the texture bends first, so the detail has vertices to "
|
||||
"land on - all in one step."));
|
||||
|
||||
ImGui::SameLine();
|
||||
if (busy) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "GLGizmoPainterBase.hpp"
|
||||
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/AnimatedTexture.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
@@ -1155,6 +1156,50 @@ private:
|
||||
std::map<std::string, IconManager::Icons> m_panel_icon_map; // file name -> [normal, colour, disabled]
|
||||
bool m_panel_icons_tried = false;
|
||||
void ensure_panel_icons();
|
||||
|
||||
// One illustration under a tooltip's text: a still image or an animation (see AnimatedTexture),
|
||||
// named by file within resources/images/texture_displacement, with a caption under it. Several of
|
||||
// them in one tooltip are laid out side by side, which is how a setting is shown as a comparison -
|
||||
// the same model at two values of it.
|
||||
struct TipMedia
|
||||
{
|
||||
std::string file;
|
||||
wxString caption;
|
||||
};
|
||||
|
||||
// Every tooltip the panel shows. Placed beside the panel rather than under the mouse: the panel is
|
||||
// pinned to the right edge of the canvas, so a tooltip left to follow the mouse covers the control
|
||||
// it is explaining, and one carrying an illustration covers most of the panel with it.
|
||||
//
|
||||
// `media` is drawn under the text, at the tooltip's full width; files that are missing or will not
|
||||
// decode are skipped, so a tooltip whose illustration has not been drawn yet is exactly the text
|
||||
// tooltip it was before.
|
||||
void panel_tooltip(const std::string &text, const std::vector<TipMedia> &media = {});
|
||||
void panel_tooltip(const wxString &text, const std::vector<TipMedia> &media = {});
|
||||
void render_tip_media(const std::vector<TipMedia> &media, float width);
|
||||
|
||||
// Screen rectangle a tooltip places itself beside: the panel window, or - while the texture picker
|
||||
// is up and tooltips are coming from inside it - the picker popup, which already sits beside the
|
||||
// panel. Both are set from the window's own rectangle as it is drawn.
|
||||
ImVec2 m_tooltip_anchor_min{0.f, 0.f};
|
||||
ImVec2 m_tooltip_anchor_max{0.f, 0.f};
|
||||
// Illustrations any tooltip has shown, decoded and uploaded once (see AnimatedTexture.hpp).
|
||||
AnimatedTextureCache m_tip_media;
|
||||
// Which tooltip's illustrations are on screen, and when they first appeared: an animation plays
|
||||
// from its first frame each time its tooltip opens, rather than from wherever a free-running clock
|
||||
// happened to be.
|
||||
std::string m_tip_media_key;
|
||||
double m_tip_media_start = 0.;
|
||||
int m_tip_media_frame = -2; // the frame it was last drawn on, to spot the tooltip reopening
|
||||
// How long the pointer has rested on the control a tooltip belongs to, and when one was last on
|
||||
// screen, both on the real clock tip_now() keeps (see the .cpp for why ImGui's own will not do).
|
||||
ImVec2 m_tip_item{-1.f, -1.f};
|
||||
double m_tip_hover_since = 0.;
|
||||
double m_tip_last_shown = -1e9;
|
||||
// The frame a tooltip was last drawn on, and how many it has been drawn on in a row: ImGui hides
|
||||
// one on its first frame while it measures it, so it has to be kept on screen to be seen at all.
|
||||
int m_tip_shown_frame = -2;
|
||||
int m_tip_settled = 0;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -373,6 +373,25 @@ bool GLGizmosManager::init_icon_textures()
|
||||
icon_list.insert(std::make_pair((int) icon, texture_id));
|
||||
}
|
||||
|
||||
for (const auto& [icon, name] : std::initializer_list<std::pair<MENU_ICON_NAME, const char*>>{
|
||||
{ IC_ALIGN_X_MIN , "align_x_min" }, { IC_ALIGN_X_MIN_DARK , "align_x_min_dark" },
|
||||
{ IC_ALIGN_X_CENTER, "align_x_center" }, { IC_ALIGN_X_CENTER_DARK, "align_x_center_dark" },
|
||||
{ IC_ALIGN_X_MAX , "align_x_max" }, { IC_ALIGN_X_MAX_DARK , "align_x_max_dark" },
|
||||
{ IC_ALIGN_Y_MIN , "align_y_min" }, { IC_ALIGN_Y_MIN_DARK , "align_y_min_dark" },
|
||||
{ IC_ALIGN_Y_CENTER, "align_y_center" }, { IC_ALIGN_Y_CENTER_DARK, "align_y_center_dark" },
|
||||
{ IC_ALIGN_Y_MAX , "align_y_max" }, { IC_ALIGN_Y_MAX_DARK , "align_y_max_dark" },
|
||||
{ IC_ALIGN_Z_MIN , "align_z_min" }, { IC_ALIGN_Z_MIN_DARK , "align_z_min_dark" },
|
||||
{ IC_ALIGN_Z_CENTER, "align_z_center" }, { IC_ALIGN_Z_CENTER_DARK, "align_z_center_dark" },
|
||||
{ IC_ALIGN_Z_MAX , "align_z_max" }, { IC_ALIGN_Z_MAX_DARK , "align_z_max_dark" },
|
||||
{ IC_DISTRIBUTE_X , "distribute_x" }, { IC_DISTRIBUTE_X_DARK , "distribute_x_dark" },
|
||||
{ IC_DISTRIBUTE_Y , "distribute_y" }, { IC_DISTRIBUTE_Y_DARK , "distribute_y_dark" },
|
||||
{ IC_DISTRIBUTE_Z , "distribute_z" }, { IC_DISTRIBUTE_Z_DARK , "distribute_z_dark" }
|
||||
}) {
|
||||
if (!IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/" + name + ".svg", 16, 16, texture_id))
|
||||
return false;
|
||||
icon_list.insert(std::make_pair((int) icon, texture_id));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -470,37 +489,6 @@ bool GLGizmosManager::check_gizmos_closed_except(EType type) const
|
||||
return true;
|
||||
}
|
||||
|
||||
// Switches the active plate to the one containing the first selected object,
|
||||
// so that subsequent operations (align/distribute) act on a plate the user
|
||||
// can actually see.
|
||||
void GLGizmosManager::check_object_located_outside_plate(bool change_plate)
|
||||
{
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
auto curr_plate_index = plate_list.get_curr_plate_index();
|
||||
Selection& selection = m_parent.get_selection();
|
||||
auto idxs = selection.get_volume_idxs();
|
||||
if (idxs.empty())
|
||||
return;
|
||||
|
||||
const GLVolume* v = selection.get_volume(*idxs.begin());
|
||||
int object_idx = v->object_idx();
|
||||
const Model* model = m_parent.get_model();
|
||||
if (object_idx < 0 || object_idx >= (int) model->objects.size())
|
||||
return;
|
||||
|
||||
ModelObject* model_object = model->objects[object_idx];
|
||||
for (size_t i = 0; i < plate_list.get_plate_count(); ++i) {
|
||||
auto plate = plate_list.get_plate(i);
|
||||
for (auto object : plate->get_objects_on_this_plate()) {
|
||||
if (model_object == object) {
|
||||
if (change_plate && curr_plate_index != (int) i)
|
||||
plate_list.select_plate(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmosManager::set_hover_id(int id)
|
||||
{
|
||||
// Measure and assembly handles hover by itself
|
||||
@@ -648,6 +636,13 @@ bool GLGizmosManager::is_allow_select_all() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ctrl+click and Alt+click grow the selection only where that serves the gizmo: the Move gizmo aligns and distributes
|
||||
// several objects or parts, while Scale uses Ctrl for single sided scaling.
|
||||
bool GLGizmosManager::is_allow_multi_select_parts_or_objects() const
|
||||
{
|
||||
return m_current == Undefined || m_current == EType::Move;
|
||||
}
|
||||
|
||||
std::optional<ClippingPlane> GLGizmosManager::get_clipping_plane() const
|
||||
{
|
||||
if (! m_common_gizmos_data || ! m_common_gizmos_data->object_clipper())
|
||||
|
||||
@@ -201,6 +201,30 @@ public:
|
||||
IC_CANVAS_SECTION_ACTIVE_HOVER,
|
||||
IC_CANVAS_SECTION_ACTIVE_DARK,
|
||||
IC_CANVAS_SECTION_ACTIVE_DARK_HOVER,
|
||||
IC_ALIGN_X_MIN,
|
||||
IC_ALIGN_X_CENTER,
|
||||
IC_ALIGN_X_MAX,
|
||||
IC_ALIGN_Y_MIN,
|
||||
IC_ALIGN_Y_CENTER,
|
||||
IC_ALIGN_Y_MAX,
|
||||
IC_ALIGN_Z_MIN,
|
||||
IC_ALIGN_Z_CENTER,
|
||||
IC_ALIGN_Z_MAX,
|
||||
IC_DISTRIBUTE_X,
|
||||
IC_DISTRIBUTE_Y,
|
||||
IC_DISTRIBUTE_Z,
|
||||
IC_ALIGN_X_MIN_DARK,
|
||||
IC_ALIGN_X_CENTER_DARK,
|
||||
IC_ALIGN_X_MAX_DARK,
|
||||
IC_ALIGN_Y_MIN_DARK,
|
||||
IC_ALIGN_Y_CENTER_DARK,
|
||||
IC_ALIGN_Y_MAX_DARK,
|
||||
IC_ALIGN_Z_MIN_DARK,
|
||||
IC_ALIGN_Z_CENTER_DARK,
|
||||
IC_ALIGN_Z_MAX_DARK,
|
||||
IC_DISTRIBUTE_X_DARK,
|
||||
IC_DISTRIBUTE_Y_DARK,
|
||||
IC_DISTRIBUTE_Z_DARK,
|
||||
};
|
||||
|
||||
explicit GLGizmosManager(GLCanvas3D& parent);
|
||||
@@ -261,7 +285,6 @@ public:
|
||||
void reset_all_states();
|
||||
bool open_gizmo(EType type);
|
||||
bool check_gizmos_closed_except(EType) const;
|
||||
void check_object_located_outside_plate(bool change_plate = true);
|
||||
|
||||
void set_hover_id(int id);
|
||||
|
||||
@@ -302,6 +325,7 @@ public:
|
||||
|
||||
bool is_paint_gizmo();
|
||||
bool is_allow_select_all();
|
||||
bool is_allow_multi_select_parts_or_objects() const;
|
||||
// Empty when the open gizmo has no object clipper.
|
||||
std::optional<ClippingPlane> get_clipping_plane() const;
|
||||
bool wants_reslice_supports_on_undo() const;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
//#include "I18N.hpp"
|
||||
#include "GLGizmosManager.hpp"
|
||||
#include "GLGizmoAlignment.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
@@ -56,26 +57,8 @@ const double GizmoObjectManipulation::mm_to_in = 0.0393700787;
|
||||
const double GizmoObjectManipulation::oz_to_g = 28.34952;
|
||||
const double GizmoObjectManipulation::g_to_oz = 0.035274;
|
||||
|
||||
// Helper function to be used by drop to bed button. Returns lowest point of this
|
||||
// volume in world coordinate system.
|
||||
static double get_volume_min_z(const GLVolume* volume)
|
||||
{
|
||||
const Transform3f& world_matrix = volume->world_matrix().cast<float>();
|
||||
|
||||
// need to get the ModelVolume pointer
|
||||
const ModelObject* mo = wxGetApp().model().objects[volume->composite_id.object_id];
|
||||
const ModelVolume* mv = mo->volumes[volume->composite_id.volume_id];
|
||||
const TriangleMesh& hull = mv->get_convex_hull();
|
||||
|
||||
float min_z = std::numeric_limits<float>::max();
|
||||
for (const stl_vertex& vert : hull.its.vertices) {
|
||||
min_z = std::min(min_z, Vec3f::UnitZ().dot(world_matrix * vert));
|
||||
}
|
||||
return min_z;
|
||||
}
|
||||
|
||||
GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
|
||||
: m_glcanvas(glcanvas)
|
||||
: m_glcanvas(glcanvas), m_alignment_helper(glcanvas)
|
||||
{
|
||||
m_imperial_units = wxGetApp().app_config->get("use_inches") == "1";
|
||||
m_new_unit_string = m_imperial_units ? L_CONTEXT("in", "inches") : L("mm");
|
||||
@@ -86,6 +69,7 @@ GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
|
||||
|
||||
m_shortcuts_move = {
|
||||
{alt + _L("Left mouse button"), _L("Part selection")},
|
||||
{ctrl + _L("Left mouse button"), _L("Select multiple objects")},
|
||||
{shift + _L("Left mouse button"), _L("Fixed step drag")},
|
||||
{_L("Context Menu"), _L("Toggle Auto-Drop")}
|
||||
};
|
||||
@@ -293,12 +277,14 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
{
|
||||
const Selection& selection = m_glcanvas.get_selection();
|
||||
|
||||
// Assembly view has its own selection and transformations, which drop_selection() doesn't update
|
||||
m_enable_drop_to_bed = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D && selection.can_drop();
|
||||
|
||||
if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) {
|
||||
const GLVolume * volume = selection.get_first_volume();
|
||||
|
||||
Vec3d rotation;
|
||||
Vec3d scale;
|
||||
double min_z = 0.;
|
||||
|
||||
if (selection.is_single_full_instance()) {
|
||||
rotation = volume->get_instance_rotation();
|
||||
@@ -307,12 +293,10 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
else {
|
||||
rotation = volume->get_volume_rotation();
|
||||
scale = volume->get_volume_scaling_factor();
|
||||
min_z = get_volume_min_z(volume);
|
||||
}
|
||||
m_show_clear_rotation = !rotation.isApprox(m_init_rotation);
|
||||
m_show_reset_0_rotation = !rotation.isApprox(Vec3d::Zero());
|
||||
m_show_clear_scale = (m_cache.scale / 100.0f - Vec3d::Ones()).norm() > 0.001;
|
||||
m_show_drop_to_bed = (std::abs(min_z) > EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,8 +745,19 @@ void GizmoObjectManipulation::set_init_rotation(const Geometry::Transformation &
|
||||
m_init_rotation = value.get_rotation();
|
||||
}
|
||||
|
||||
void GizmoObjectManipulation::reset_align_state()
|
||||
{
|
||||
m_hovered_align_type = GLGizmoAlignment::AlignType::NONE;
|
||||
m_align_to_parent = !m_alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN);
|
||||
}
|
||||
|
||||
void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, std::string window_name, float x, float y, float bottom_limit)
|
||||
{
|
||||
float scale = m_glcanvas.get_scale();
|
||||
#ifdef WIN32
|
||||
scale *= (float)get_dpi_for_window(wxGetApp().GetTopWindow()) / (float)DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
// BBS: GUI refactor: move gizmo to the right
|
||||
if (abs(last_move_input_window_width) > 0.01f) {
|
||||
if (x + last_move_input_window_width > m_glcanvas.get_canvas_size().get_width()) {
|
||||
@@ -782,6 +777,8 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
ImGuiWrapper::push_toolbar_style(m_glcanvas.get_scale());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0, 6.0));
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
std::string name = this->m_new_title_string + "##" + window_name;
|
||||
imgui_wrapper->begin(_L(name), ImGuiWrapper::TOOLBAR_WINDOW_FLAGS);
|
||||
|
||||
@@ -798,16 +795,26 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
};
|
||||
|
||||
float space_size = imgui_wrapper->get_style_scaling() * 8;
|
||||
// The align section shares the combo width, the caption column and the input columns of the position row.
|
||||
const bool show_align = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D &&
|
||||
!m_glcanvas.get_selection().is_wipe_tower();
|
||||
//ORCA
|
||||
float coord_combo_width = std::max({
|
||||
imgui_wrapper->calc_text_size(_L("World")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Object")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Part")).x
|
||||
}) + imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f);
|
||||
});
|
||||
float label_max = std::max({
|
||||
imgui_wrapper->calc_text_size(_L("Position")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Relative")).x
|
||||
});
|
||||
if (show_align) {
|
||||
coord_combo_width = std::max({coord_combo_width, imgui_wrapper->calc_text_size(_L("Selection")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Object")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Plate")).x});
|
||||
label_max = std::max(label_max, imgui_wrapper->calc_text_size(_L("Distribute")).x);
|
||||
}
|
||||
coord_combo_width += imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f);
|
||||
float caption_max = std::max(label_max, coord_combo_width - 3 * space_size);
|
||||
float end_text_size = imgui_wrapper->calc_text_size(this->m_new_unit_string).x;
|
||||
|
||||
@@ -821,6 +828,13 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
|
||||
// Rotation
|
||||
float unit_size = imgui_wrapper->calc_text_size(MAX_SIZE).x + space_size;
|
||||
// Four align buttons sit under each axis' input, a space before the next axis, so the inputs widen to hold them.
|
||||
const float align_icon_size = 16.f * scale;
|
||||
const float align_button_size = align_icon_size + 2.f * style.FramePadding.x; // ORCA
|
||||
if (show_align) {
|
||||
unit_size = std::max(unit_size, 3.f * align_button_size);
|
||||
unit_size = std::round(unit_size); // ORCA keep columns on whole pixels
|
||||
}
|
||||
int index = 1;
|
||||
int index_unit = 1;
|
||||
|
||||
@@ -909,6 +923,134 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
}
|
||||
if (!focued_on_text) m_glcanvas.handle_sidebar_focus_event("", false);
|
||||
|
||||
// Align and distribute along the world axes, within the selection or to the object or plate holding it.
|
||||
using AlignType = GLGizmoAlignment::AlignType;
|
||||
const bool old_align_to_parent = m_align_to_parent;
|
||||
AlignType hovered_align_type = AlignType::NONE;
|
||||
if (show_align) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Each row is row_height tall, separated by ItemSpacing.y. Every control is centered vertically in its row.
|
||||
const float row_y = ImGui::GetCursorPosY();
|
||||
const float button_height = align_icon_size + 2.f * style.FramePadding.y;
|
||||
ImGuiWrapper::push_combo_style(m_glcanvas.get_scale());
|
||||
const float combo_height = ImGui::GetFrameHeight();
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
const float row_height = std::max({ImGui::GetTextLineHeight(), button_height, combo_height});
|
||||
const float row_gap = style.ItemSpacing.y;
|
||||
int row = 0;
|
||||
auto row_top = [&](int r) { return row_y + r * (row_height + row_gap); };
|
||||
// Snap to whole pixels to avoid blurry icons
|
||||
auto center_in_row = [&](int r, float item_height) { return std::floor(row_top(r) + (row_height - item_height) * 0.5f); };
|
||||
|
||||
// Row 0: align mode combo in the caption column.
|
||||
ImGui::SetCursorPosY(center_in_row(row, combo_height));
|
||||
|
||||
// The last mode aligns to the parent. Aligning within the selection needs two items.
|
||||
std::vector<std::string> align_modes = {
|
||||
_u8L("Selection"),
|
||||
m_alignment_helper.is_parts_selection(true) ? _u8L("Object") : _u8L("Plate")
|
||||
};
|
||||
if (!m_alignment_helper.can_align(AlignType::X_MIN)) {
|
||||
align_modes.erase(align_modes.begin());
|
||||
m_align_to_parent = true;
|
||||
}
|
||||
size_t align_mode_idx = m_align_to_parent ? align_modes.size() - 1 : 0;
|
||||
ImGuiWrapper::push_combo_style(m_glcanvas.get_scale());
|
||||
// render_combo() takes its ImGui id from the empty label, which the coordinate combo uses too.
|
||||
ImGui::PushID("align_mode");
|
||||
if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width))
|
||||
m_align_to_parent = align_mode_idx + 1 == align_modes.size();
|
||||
ImGui::PopID();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
wxString tooltip_str = _L("Reference for alignment");
|
||||
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
const bool dark_mode = wxGetApp().dark_mode();
|
||||
int current_axis = -1;
|
||||
auto align_icon = [&](int axis, AlignType type, GLGizmosManager::MENU_ICON_NAME icon,
|
||||
GLGizmosManager::MENU_ICON_NAME dark_icon, const wxString &tip) {
|
||||
const bool distribute = type >= AlignType::DISTRIBUTE_X;
|
||||
const bool enabled = m_align_to_parent ? m_alignment_helper.can_align(type, true) :
|
||||
distribute ? m_alignment_helper.can_distribute(type) :
|
||||
m_alignment_helper.can_align(type);
|
||||
if (axis != current_axis) {
|
||||
ImGui::SameLine(int(caption_max + (axis + 1) * space_size + axis * unit_size)); // ORCA ensure placement snaps to pixel to fix blur
|
||||
if (current_axis < 0)
|
||||
ImGui::SetCursorPosY(center_in_row(row, button_height));
|
||||
current_axis = axis;
|
||||
} else
|
||||
ImGui::SameLine(0, 0);
|
||||
|
||||
if (!enabled) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3f);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_Button]);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_Button]);
|
||||
}
|
||||
ImTextureID texture = m_glcanvas.get_gizmos_manager().get_icon_texture_id(dark_mode ? dark_icon : icon);
|
||||
if (ImGui::ImageButton3(texture, texture, ImVec2(align_icon_size, align_icon_size)) && enabled) {
|
||||
if (distribute)
|
||||
m_alignment_helper.distribute_objects(type);
|
||||
else
|
||||
m_alignment_helper.align_objects(type, m_align_to_parent);
|
||||
}
|
||||
if (!enabled) {
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
wxString text = tip;
|
||||
if (enabled)
|
||||
hovered_align_type = type;
|
||||
else {
|
||||
// Too few items is the reason when there are items to move (only distribute needs three);
|
||||
// otherwise the helper names it.
|
||||
std::string why_not;
|
||||
if (m_alignment_helper.get_items_count(type, m_align_to_parent, &why_not) > 0)
|
||||
why_not = _u8L("Select at least 3 parts or objects");
|
||||
if (!why_not.empty())
|
||||
text += "\n" + from_u8(why_not);
|
||||
}
|
||||
imgui_wrapper->tooltip(text, imgui_wrapper->calc_text_size(text).x + 3 * space_size);
|
||||
}
|
||||
};
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
align_icon(0, AlignType::X_MIN, GLGizmosManager::IC_ALIGN_X_MIN, GLGizmosManager::IC_ALIGN_X_MIN_DARK, _L("Align left") + " (-X)");
|
||||
align_icon(0, AlignType::CENTER_X, GLGizmosManager::IC_ALIGN_X_CENTER, GLGizmosManager::IC_ALIGN_X_CENTER_DARK, _L("Align left-right center") + " (X)");
|
||||
align_icon(0, AlignType::X_MAX, GLGizmosManager::IC_ALIGN_X_MAX, GLGizmosManager::IC_ALIGN_X_MAX_DARK, _L("Align right") + " (+X)");
|
||||
|
||||
align_icon(1, AlignType::Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN_DARK, _L("Align front") + " (-Y)");
|
||||
align_icon(1, AlignType::CENTER_Y, GLGizmosManager::IC_ALIGN_Y_CENTER, GLGizmosManager::IC_ALIGN_Y_CENTER_DARK, _L("Align front-back center") + " (Y)");
|
||||
align_icon(1, AlignType::Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX_DARK, _L("Align back") + " (+Y)");
|
||||
|
||||
align_icon(2, AlignType::Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN_DARK, _L("Align bottom") + " (-Z)");
|
||||
align_icon(2, AlignType::CENTER_Z, GLGizmosManager::IC_ALIGN_Z_CENTER, GLGizmosManager::IC_ALIGN_Z_CENTER_DARK, _L("Align top-bottom center") + " (Z)");
|
||||
align_icon(2, AlignType::Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX_DARK, _L("Align top") + " (+Z)");
|
||||
|
||||
if (!m_align_to_parent) {
|
||||
row = 1;
|
||||
current_axis = -1;
|
||||
ImGui::SetCursorPosY(center_in_row(row, ImGui::GetTextLineHeight()));
|
||||
imgui_wrapper->text(_L("Distribute"));
|
||||
align_icon(0, AlignType::DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X_DARK, _L("Distribute left-right") + " (X)");
|
||||
align_icon(1, AlignType::DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y_DARK, _L("Distribute front-back") + " (Y)");
|
||||
align_icon(2, AlignType::DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z_DARK, _L("Distribute top-bottom") + " (Z)");
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::SetCursorPosY(row_top(row + 1));
|
||||
}
|
||||
// The Move gizmo draws the preview with the scene, which an ImGui-only frame does not redraw.
|
||||
if (hovered_align_type != m_hovered_align_type || m_align_to_parent != old_align_to_parent) {
|
||||
m_hovered_align_type = hovered_align_type;
|
||||
m_glcanvas.set_as_dirty();
|
||||
m_glcanvas.request_extra_frame();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
@@ -917,6 +1059,17 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
|
||||
GLGizmoUtils::render_tooltip_button(imgui_wrapper, m_glcanvas, m_shortcuts_move, x, y);
|
||||
|
||||
// ORCA use existing function for drop and show button on bottom bar to keep gizmo compact
|
||||
imgui_wrapper->disabled_begin(!m_enable_drop_to_bed);
|
||||
ImGui::SameLine(0, space_size * 1.5f);
|
||||
if (imgui_wrapper->button(_L("Drop"))){
|
||||
wxGetApp().plater()->drop_selection();
|
||||
UpdateAndShow(true);
|
||||
}
|
||||
imgui_wrapper->disabled_end();
|
||||
if(ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
imgui_wrapper->tooltip(_L("Move the selection along Z so that its lowest point touches the bed."), ImGui::GetFontSize() * 20.0f);
|
||||
|
||||
ImGui::SameLine();
|
||||
GLGizmoUtils::begin_right_aligned_buttons({ _L("Done") });
|
||||
if (imgui_wrapper->button(_L("Done"))) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "slic3r/GUI/GUI_Geometry.hpp"
|
||||
#include "GLGizmoAlignment.hpp"
|
||||
|
||||
//#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
|
||||
@@ -102,7 +103,7 @@ public:
|
||||
bool m_show_reset_0_rotation{false};
|
||||
bool m_show_clear_rotation { false };
|
||||
bool m_show_clear_scale { false };
|
||||
bool m_show_drop_to_bed { false };
|
||||
bool m_enable_drop_to_bed { false };
|
||||
enum class RotateType { None, Relative, Absolute
|
||||
};
|
||||
RotateType m_last_rotate_type{RotateType::None}; // 0:no input 1:relative 2:absolute
|
||||
@@ -151,6 +152,13 @@ public:
|
||||
|
||||
void set_init_rotation(const Geometry::Transformation &value);
|
||||
|
||||
// Align/distribute state of the Move panel, which the Move gizmo previews.
|
||||
const GLGizmoAlignment &get_alignment_helper() const { return m_alignment_helper; }
|
||||
GLGizmoAlignment::AlignType get_hovered_align_type() const { return m_hovered_align_type; }
|
||||
bool get_align_to_parent() const { return m_align_to_parent; }
|
||||
// When the Move gizmo opens or closes: no preview, and "Align selected" whenever the selection allows it.
|
||||
void reset_align_state();
|
||||
|
||||
private:
|
||||
void reset_settings_value();
|
||||
void update_settings_value(const Selection& selection);
|
||||
@@ -175,6 +183,12 @@ private:
|
||||
GLCanvas3D& m_glcanvas;
|
||||
unsigned int m_last_active_item { 0 };
|
||||
|
||||
GLGizmoAlignment m_alignment_helper;
|
||||
// Operation of the enabled align icon under the mouse.
|
||||
GLGizmoAlignment::AlignType m_hovered_align_type{GLGizmoAlignment::AlignType::NONE};
|
||||
// Align to the object or plate holding the selection rather than within the selection.
|
||||
bool m_align_to_parent{false};
|
||||
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_move;
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
|
||||
@@ -23,27 +23,23 @@ IMToolbarItem::~IMToolbarItem()
|
||||
}
|
||||
|
||||
|
||||
bool IMToolbarItem::generate_texture()
|
||||
void IMToolbarItem::set_thumbnail(const ThumbnailData& thumbnail)
|
||||
{
|
||||
GLint last_texture;
|
||||
unsigned m_image_texture{ 0 };
|
||||
unsigned char* pixels = (unsigned char*)(&image_data[0]);
|
||||
if (!thumbnail.is_valid())
|
||||
return;
|
||||
|
||||
GLint last_texture;
|
||||
GLuint id = (GLuint)(intptr_t)texture_id;
|
||||
glsafe(::glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture));
|
||||
glsafe(::glGenTextures(1, &m_image_texture));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_image_texture));
|
||||
if (id == 0)
|
||||
glsafe(::glGenTextures(1, &id));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, id));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ROW_LENGTH, 0));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
|
||||
|
||||
// Store our identifier
|
||||
texture_id = (ImTextureID)(intptr_t)m_image_texture;
|
||||
|
||||
// Restore state
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, thumbnail.width, thumbnail.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, thumbnail.pixels.data()));
|
||||
texture_id = (ImTextureID)(intptr_t)id;
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, last_texture));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IMToolbar::del_all_item()
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define DEFAULT_TOOLBAR_BUTTON_HEIGHT 80.0f
|
||||
|
||||
namespace Slic3r {
|
||||
struct ThumbnailData;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class IMToolbarItem
|
||||
@@ -33,11 +35,9 @@ public:
|
||||
SliceState slice_state;
|
||||
|
||||
ImTextureID texture_id { 0 };
|
||||
std::vector<unsigned char> image_data;
|
||||
unsigned int image_width;
|
||||
unsigned int image_height;
|
||||
|
||||
bool generate_texture();
|
||||
// Uploads a valid thumbnail as the texture; the item keeps no copy of its pixels.
|
||||
void set_thumbnail(const ThumbnailData& thumbnail);
|
||||
~IMToolbarItem();
|
||||
};
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", L("Drop to Bed"), L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->get_selection().can_drop(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", L("Scale to Fit Print Volume"), L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
@@ -6154,6 +6155,14 @@ int PartPlateList::select_plate(int index)
|
||||
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
|
||||
}
|
||||
|
||||
// With the other plates hidden, the view follows the selected plate.
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && m_plater->is_view3D_shown() && wxGetApp().focus_mode()) {
|
||||
Camera& camera = m_plater->get_camera();
|
||||
Vec3d target = camera.get_target();
|
||||
target.head<2>() = m_plate_list[index]->get_bounding_box().center().head<2>();
|
||||
camera.set_target(target);
|
||||
}
|
||||
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = m_plate_list[index]->get_plate_name();
|
||||
@@ -7063,7 +7072,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
|
||||
/*rendering related functions*/
|
||||
//render
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome, bool hide_inactive)
|
||||
{
|
||||
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
|
||||
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
|
||||
@@ -7078,7 +7087,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
||||
load_icon_textures();
|
||||
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
|
||||
int current_index = (*it)->get_index();
|
||||
if (only_current && (current_index != m_current_plate))
|
||||
if ((only_current || hide_inactive) && (current_index != m_current_plate))
|
||||
continue;
|
||||
if (current_index == m_current_plate) {
|
||||
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
|
||||
@@ -7539,7 +7548,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
return ret;
|
||||
}
|
||||
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx)
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx, bool with_statistics)
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
@@ -7613,6 +7622,8 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
}
|
||||
//parse filament info
|
||||
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
|
||||
if (with_statistics)
|
||||
plate_data_item->statistics = gcode_statistics_json(*m_plate_list[i]->get_slice_result());
|
||||
|
||||
// Record mixed (virtual) filaments actually used on this plate.
|
||||
// Source is ToolOrdering::used_mixed_filaments (slots that appeared in
|
||||
|
||||
@@ -1008,7 +1008,7 @@ public:
|
||||
|
||||
/*rendering related functions*/
|
||||
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false, bool hide_inactive = false);
|
||||
void set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void render_hover_tooltip() const;
|
||||
@@ -1070,7 +1070,7 @@ public:
|
||||
/* load/store releted functions, with_gcode = true and plate_idx = -1, export all gcode
|
||||
* if with_gcode = true and specify plate_idx, export plate_idx gcode only
|
||||
*/
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1);
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1, bool with_statistics = false);
|
||||
int load_from_3mf_structure(PlateDataPtrs& plate_data_list, int filament_count = 1);
|
||||
//load gcode files
|
||||
int load_gcode_files();
|
||||
|
||||
+21
-109
@@ -245,7 +245,6 @@
|
||||
#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount
|
||||
#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles
|
||||
#include "ProjectDirtyStateManager.hpp"
|
||||
#include "Gizmos/GLGizmoAlignment.hpp"
|
||||
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
|
||||
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
|
||||
#include "Gizmos/GizmoObjectManipulation.hpp"
|
||||
@@ -437,6 +436,7 @@ void Plater::show_illegal_characters_warning(wxWindow* parent)
|
||||
void Plater::mark_plate_toolbar_image_dirty()
|
||||
{
|
||||
m_b_plate_toolbar_image_dirty = true;
|
||||
++m_plate_toolbar_image_version;
|
||||
}
|
||||
|
||||
bool Plater::is_plate_toolbar_image_dirty() const
|
||||
@@ -7485,18 +7485,6 @@ struct Plater::priv
|
||||
void delete_all_objects_from_model();
|
||||
void reset(bool apply_presets_change = false, bool reload_presets = true);
|
||||
void center_selection();
|
||||
void distribute_selection_x();
|
||||
void distribute_selection_y();
|
||||
void distribute_selection_z();
|
||||
void align_selection_x_min();
|
||||
void align_selection_x_center();
|
||||
void align_selection_x_max();
|
||||
void align_selection_y_min();
|
||||
void align_selection_y_center();
|
||||
void align_selection_y_max();
|
||||
void align_selection_z_min();
|
||||
void align_selection_z_center();
|
||||
void align_selection_z_max();
|
||||
void drop_selection();
|
||||
void mirror(Axis axis);
|
||||
void split_object(bool auto_drop = true);
|
||||
@@ -10926,78 +10914,6 @@ void Plater::priv::center_selection()
|
||||
view3D->center_selected();
|
||||
}
|
||||
|
||||
void Plater::priv::distribute_selection_x()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_X);
|
||||
}
|
||||
|
||||
void Plater::priv::distribute_selection_y()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_Y);
|
||||
}
|
||||
|
||||
void Plater::priv::distribute_selection_z()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_Z);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_x_min()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::X_MIN);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_x_center()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_X);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_x_max()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::X_MAX);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_y_min()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Y_MIN);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_y_center()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_Y);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_y_max()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Y_MAX);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_z_min()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Z_MIN);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_z_center()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_Z);
|
||||
}
|
||||
|
||||
void Plater::priv::align_selection_z_max()
|
||||
{
|
||||
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
|
||||
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Z_MAX);
|
||||
}
|
||||
|
||||
void Plater::priv::drop_selection()
|
||||
{
|
||||
view3D->drop_selected();
|
||||
@@ -18353,16 +18269,20 @@ wxString Plater::get_project_name()
|
||||
|
||||
void Plater::update_all_plate_thumbnails(bool force_update)
|
||||
{
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++) {
|
||||
PartPlate* plate = get_partplate_list().get_plate(i);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++)
|
||||
update_plate_thumbnails(i, force_update);
|
||||
}
|
||||
|
||||
void Plater::update_plate_thumbnails(int plate_idx, bool force_update)
|
||||
{
|
||||
PartPlate* plate = get_partplate_list().get_plate(plate_idx);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, plate_idx};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19649,7 +19569,10 @@ void Plater::export_gcode_3mf(bool export_all)
|
||||
int plate_idx = get_partplate_list().get_curr_plate_index();
|
||||
if (export_all)
|
||||
plate_idx = PLATE_ALL_IDX;
|
||||
export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence
|
||||
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
|
||||
if (wxGetApp().app_config->get_bool("export_statistics"))
|
||||
strategy = strategy | SaveStrategy::WithStatistics;
|
||||
export_3mf(output_path, strategy, plate_idx);
|
||||
|
||||
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
|
||||
|
||||
@@ -20503,7 +20426,8 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
|
||||
//BBS: add bbs 3mf logic
|
||||
PlateDataPtrs plate_data_list;
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx);
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
|
||||
strategy & SaveStrategy::WithStatistics);
|
||||
|
||||
// BBS: backup
|
||||
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
@@ -22311,18 +22235,6 @@ void Plater::suppress_background_process(const bool stop_background_process)
|
||||
BackgroundSlicingProcess& Plater::background_process() { return p->background_process; }
|
||||
|
||||
void Plater::center_selection() { p->center_selection(); }
|
||||
void Plater::distribute_selection_x() { p->distribute_selection_x(); }
|
||||
void Plater::distribute_selection_y() { p->distribute_selection_y(); }
|
||||
void Plater::distribute_selection_z() { p->distribute_selection_z(); }
|
||||
void Plater::align_selection_x_min() { p->align_selection_x_min(); }
|
||||
void Plater::align_selection_x_center() { p->align_selection_x_center(); }
|
||||
void Plater::align_selection_x_max() { p->align_selection_x_max(); }
|
||||
void Plater::align_selection_y_min() { p->align_selection_y_min(); }
|
||||
void Plater::align_selection_y_center() { p->align_selection_y_center(); }
|
||||
void Plater::align_selection_y_max() { p->align_selection_y_max(); }
|
||||
void Plater::align_selection_z_min() { p->align_selection_z_min(); }
|
||||
void Plater::align_selection_z_center() { p->align_selection_z_center(); }
|
||||
void Plater::align_selection_z_max() { p->align_selection_z_max(); }
|
||||
void Plater::drop_selection() { p->drop_selection(); }
|
||||
void Plater::mirror(Axis axis) { p->mirror(axis); }
|
||||
void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); }
|
||||
|
||||
@@ -434,6 +434,7 @@ public:
|
||||
bool is_new_project_and_check_state() { return m_new_project_and_check_state; }
|
||||
wxString get_project_name();
|
||||
void update_all_plate_thumbnails(bool force_update = false);
|
||||
void update_plate_thumbnails(int plate_idx, bool force_update);
|
||||
void update_obj_preview_thumbnail(ModelObject *, int obj_idx, int vol_idx, std::vector<Slic3r::ColorRGBA> colors, int camera_view_angle_type);
|
||||
void invalid_all_plate_thumbnails();
|
||||
void force_update_all_plate_thumbnails();
|
||||
@@ -762,18 +763,6 @@ public:
|
||||
//BBS: add clone logic
|
||||
void clone_selection();
|
||||
void center_selection();
|
||||
void distribute_selection_x();
|
||||
void distribute_selection_y();
|
||||
void distribute_selection_z();
|
||||
void align_selection_x_min();
|
||||
void align_selection_x_center();
|
||||
void align_selection_x_max();
|
||||
void align_selection_y_min();
|
||||
void align_selection_y_center();
|
||||
void align_selection_y_max();
|
||||
void align_selection_z_min();
|
||||
void align_selection_z_center();
|
||||
void align_selection_z_max();
|
||||
void drop_selection();
|
||||
void search(bool plater_is_active, Preset::Type type, wxWindow *tag, TextInput *etag, wxWindow *stag);
|
||||
void mirror(Axis axis);
|
||||
@@ -1083,6 +1072,8 @@ public:
|
||||
void mark_plate_toolbar_image_dirty();
|
||||
bool is_plate_toolbar_image_dirty() const;
|
||||
void clear_plate_toolbar_image_dirty();
|
||||
// Bumped each time the plate thumbnails go stale, so each consumer can track it on its own.
|
||||
unsigned plate_toolbar_image_version() const { return m_plate_toolbar_image_version; }
|
||||
|
||||
private:
|
||||
struct priv;
|
||||
@@ -1107,6 +1098,7 @@ private:
|
||||
int m_valid_plates_count { 0 };
|
||||
int m_check_status = 0; // 0 not check, 1 check success, 2 check failed
|
||||
bool m_b_plate_toolbar_image_dirty{ true };
|
||||
unsigned m_plate_toolbar_image_version{ 0 };
|
||||
|
||||
void suppress_snapshots();
|
||||
void allow_snapshots();
|
||||
|
||||
@@ -645,7 +645,7 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr
|
||||
combobox->SetSelection(current_region);
|
||||
return;
|
||||
} else {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
config->set("region", region.ToStdString());
|
||||
auto area = config->get_country_code();
|
||||
if (agent) {
|
||||
@@ -2312,6 +2312,13 @@ void PreferencesDialog::create_items()
|
||||
auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets");
|
||||
g_sizer->Add(item_show_unsupported);
|
||||
|
||||
auto item_export_statistics = create_item_checkbox(_L("Export statistics with G-code"),
|
||||
_L("If enabled, exporting a G-code file also writes its statistics as JSON next to it, as <name>.statistics.json, "
|
||||
"and an exported sliced 3MF file holds them inside: print time, filament and feature totals, "
|
||||
"and the same per layer and per object."),
|
||||
"export_statistics");
|
||||
g_sizer->Add(item_export_statistics);
|
||||
|
||||
//// DEVELOPER > Experimental Features
|
||||
g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND);
|
||||
|
||||
@@ -2481,7 +2488,7 @@ wxBoxSizer* PreferencesDialog::create_debug_page()
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
std::string country_code = config->get_country_code();
|
||||
if (agent) {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
agent->set_country_code(country_code);
|
||||
}
|
||||
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"), ConfirmBeforeSendDialog::VisibleButtons::ONLY_CONFIRM); // ORCA VisibleButtons instead ButtonStyle
|
||||
|
||||
@@ -558,15 +558,37 @@ void Selection::center()
|
||||
return;
|
||||
}
|
||||
|
||||
double Selection::get_drop_min_z() const
|
||||
{
|
||||
if (!m_valid)
|
||||
return 0.;
|
||||
if (m_mode == Volume)
|
||||
return get_bounding_box().min.z();
|
||||
|
||||
double min_z = DBL_MAX;
|
||||
for (const auto& [obj_idx, inst_idxs] : m_cache.content) {
|
||||
if (obj_idx >= (int)m_model->objects.size())
|
||||
continue; // wipe tower
|
||||
for (int inst_idx : inst_idxs)
|
||||
min_z = std::min(min_z, m_model->objects[obj_idx]->get_instance_min_z(inst_idx));
|
||||
}
|
||||
return min_z == DBL_MAX ? 0. : min_z;
|
||||
}
|
||||
|
||||
bool Selection::can_drop() const
|
||||
{
|
||||
return std::abs(get_drop_min_z()) > -SINKING_Z_THRESHOLD;
|
||||
}
|
||||
|
||||
void Selection::drop()
|
||||
{
|
||||
if (std::abs(this->get_bounding_box().min.z()) < -SINKING_Z_THRESHOLD) {
|
||||
return; // shouldnt happen, but better check anyways, already checked in append_menu_item_drop()
|
||||
}
|
||||
const double min_z = get_drop_min_z();
|
||||
if (std::abs(min_z) < -SINKING_Z_THRESHOLD)
|
||||
return; // already checked in can_drop()
|
||||
|
||||
wxGetApp().plater()->take_snapshot(L("Move object"));
|
||||
|
||||
this->move_to_center(Vec3d(0, 0, -this->get_bounding_box().min.z()));
|
||||
this->move_to_center(Vec3d(0, 0, -min_z));
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& volume = *(*m_volumes)[i];
|
||||
@@ -1757,50 +1779,27 @@ void Selection::translate(unsigned int object_idx, unsigned int instance_idx, co
|
||||
|
||||
//BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": obj %1%, instance %2%") % object_idx % instance_idx;
|
||||
//BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": %1%, displacement {%2%, %3%, %4%}") % __LINE__ % displacement(X) % displacement(Y) % displacement(Z);
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& v = *(*m_volumes)[i];
|
||||
if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx)
|
||||
v.set_instance_offset(v.get_instance_offset() + displacement);
|
||||
}
|
||||
|
||||
std::set<unsigned int> done; // prevent processing volumes twice
|
||||
done.insert(m_list.begin(), m_list.end());
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
if (done.size() == m_volumes->size())
|
||||
break;
|
||||
|
||||
int object_idx = (*m_volumes)[i]->object_idx();
|
||||
if (object_idx >= 1000)
|
||||
continue;
|
||||
|
||||
// Process unselected volumes of the object.
|
||||
for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) {
|
||||
if (done.size() == m_volumes->size())
|
||||
break;
|
||||
|
||||
if (done.find(j) != done.end())
|
||||
continue;
|
||||
|
||||
GLVolume& v = *(*m_volumes)[j];
|
||||
if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx)
|
||||
continue;
|
||||
|
||||
v.set_instance_offset(v.get_instance_offset() + displacement);
|
||||
done.insert(j);
|
||||
}
|
||||
// Every volume of the instance, selected or not, so that the instance moves as a whole.
|
||||
for (GLVolume* v : *m_volumes) {
|
||||
if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx)
|
||||
v->set_instance_offset(v->get_instance_offset() + displacement);
|
||||
}
|
||||
|
||||
this->set_bounding_boxes_dirty();
|
||||
}
|
||||
|
||||
void Selection::translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement) {
|
||||
void Selection::translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement, bool local)
|
||||
{
|
||||
if (!m_valid) return;
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume &v = *(*m_volumes)[i];
|
||||
if (v.object_idx() == (int) object_idx && v.instance_idx() == (int) instance_idx && v.volume_idx() == (int) volume_idx)
|
||||
v.set_volume_offset(v.get_volume_offset() + displacement);
|
||||
if (v.object_idx() == (int) object_idx && v.instance_idx() == (int) instance_idx && v.volume_idx() == (int) volume_idx) {
|
||||
Vec3d local_displacement = displacement;
|
||||
if (!local)
|
||||
local_displacement = v.get_instance_transformation().get_matrix_no_offset().inverse() * displacement;
|
||||
v.set_volume_offset(v.get_volume_offset() + local_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
this->set_bounding_boxes_dirty();
|
||||
|
||||
@@ -248,6 +248,9 @@ public:
|
||||
void clone(int numbers = 1);
|
||||
void center();
|
||||
void drop();
|
||||
bool can_drop() const;
|
||||
// Lowest world Z that drop() moves to the bed; whole objects count model parts only, as auto drop does.
|
||||
double get_drop_min_z() const;
|
||||
void center_plate(const int plate_idx);
|
||||
void set_printable(bool printable);
|
||||
bool get_auto_drop() const;
|
||||
@@ -360,7 +363,8 @@ public:
|
||||
|
||||
void translate(unsigned int object_idx, const Vec3d& displacement);
|
||||
void translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement);
|
||||
void translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement);
|
||||
// local: true (default) if displacement is already in instance-local space; false treats it as world-space delta (L^-1 applied).
|
||||
void translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement, bool local = true);
|
||||
|
||||
void rotate(unsigned int object_idx, unsigned int instance_idx, const Transform3d &overwrite_tran);
|
||||
void rotate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Transform3d &overwrite_tran);
|
||||
|
||||
+33
-31
@@ -2052,17 +2052,19 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!boost::any_cast<bool>(value) && timelapse_enabled) {
|
||||
bool enable_wrapping = m_config->option<ConfigOptionBool>("enable_wrapping_detection")->value;
|
||||
if (!boost::any_cast<bool>(value) && (timelapse_enabled || enable_wrapping)) {
|
||||
bool set_enable_prime_tower = false;
|
||||
MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower\?"),
|
||||
_L("Warning"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dlg.ShowModal() == wxID_NO) {
|
||||
DynamicPrintConfig new_conf = *m_config;
|
||||
new_conf.set_key_value("enable_prime_tower", new ConfigOptionBool(true));
|
||||
m_config_manipulation.apply(m_config, &new_conf);
|
||||
set_enable_prime_tower = true;
|
||||
if (timelapse_enabled) {
|
||||
MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without a prime tower. Are you sure you want to disable the prime tower\?"),
|
||||
_L("Warning"), wxICON_WARNING | wxYES | wxNO);
|
||||
if (dlg.ShowModal() == wxID_NO) {
|
||||
DynamicPrintConfig new_conf = *m_config;
|
||||
new_conf.set_key_value("enable_prime_tower", new ConfigOptionBool(true));
|
||||
m_config_manipulation.apply(m_config, &new_conf);
|
||||
set_enable_prime_tower = true;
|
||||
}
|
||||
}
|
||||
bool enable_wrapping = m_config->option<ConfigOptionBool>("enable_wrapping_detection")->value;
|
||||
if (enable_wrapping && !set_enable_prime_tower) {
|
||||
MessageDialog dlg(wxGetApp().plater(),
|
||||
_L("A prime tower is required for clumping detection. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?"),
|
||||
@@ -2850,9 +2852,9 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("precise_outer_wall", "quality_settings_precision#precise-wall");
|
||||
optgroup->append_single_option_line("precise_z_height", "quality_settings_precision#precise-z-height");
|
||||
optgroup->append_single_option_line("hole_to_polyhole", "quality_settings_precision#polyholes");
|
||||
optgroup->append_single_option_line("hole_to_polyhole_threshold", "quality_settings_precision#polyholes");
|
||||
optgroup->append_single_option_line("hole_to_polyhole_twisted", "quality_settings_precision#polyholes");
|
||||
optgroup->append_single_option_line("hole_to_polyhole_max_edges", "quality_settings_precision#polyholes");
|
||||
optgroup->append_single_option_line("hole_to_polyhole_threshold", "quality_settings_precision#polyhole-detection-margin");
|
||||
optgroup->append_single_option_line("hole_to_polyhole_twisted", "quality_settings_precision#polyhole-twist");
|
||||
optgroup->append_single_option_line("hole_to_polyhole_max_edges", "quality_settings_precision#maximum-polyhole-edge-count");
|
||||
|
||||
optgroup = page->new_optgroup(L("Ironing"), L"param_ironing");
|
||||
optgroup->append_single_option_line("ironing_type", "quality_settings_ironing#type");
|
||||
@@ -2873,7 +2875,7 @@ void TabPrint::build()
|
||||
|
||||
optgroup = page->new_optgroup(L("Wall generator"), L"param_wall_generator");
|
||||
optgroup->append_single_option_line("wall_generator", "quality_settings_wall_generator");
|
||||
optgroup->append_single_option_line("wall_transition_angle", "quality_settings_wall_generator#wall-transitioning-threshhold-angle");
|
||||
optgroup->append_single_option_line("wall_transition_angle", "quality_settings_wall_generator#wall-transitioning-threshold-angle");
|
||||
optgroup->append_single_option_line("wall_transition_filter_deviation", "quality_settings_wall_generator#wall-transitioning-filter-margin");
|
||||
optgroup->append_single_option_line("wall_transition_length", "quality_settings_wall_generator#wall-transitioning-length");
|
||||
optgroup->append_single_option_line("wall_distribution_count", "quality_settings_wall_generator#wall-distribution-count");
|
||||
@@ -3061,8 +3063,8 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("initial_layer_travel_acceleration", "speed_settings_acceleration#initial-layer-travel", 0);
|
||||
optgroup->append_single_option_line("top_surface_acceleration", "speed_settings_acceleration#top-surface", 0);
|
||||
optgroup->append_single_option_line("travel_acceleration", "speed_settings_acceleration#travel", 0);
|
||||
optgroup->append_single_option_line("accel_to_decel_enable", "speed_settings_acceleration");
|
||||
optgroup->append_single_option_line("accel_to_decel_factor", "speed_settings_acceleration");
|
||||
optgroup->append_single_option_line("accel_to_decel_enable", "speed_settings_acceleration#enable-accel-to-decel");
|
||||
optgroup->append_single_option_line("accel_to_decel_factor", "speed_settings_acceleration#accel-to-decel");
|
||||
|
||||
optgroup = page->new_optgroup(L("Junction Deviation"), L"param_junction_deviation", 15);
|
||||
optgroup->append_single_option_line("default_junction_deviation", "speed_settings_jerk_xy#junction-deviation", 0);
|
||||
@@ -3080,7 +3082,7 @@ void TabPrint::build()
|
||||
optgroup = page->new_optgroup(L("Advanced"), L"param_advanced", 15);
|
||||
optgroup->append_single_option_line("max_volumetric_extrusion_rate_slope", "speed_settings_advanced");
|
||||
optgroup->append_single_option_line("max_volumetric_extrusion_rate_slope_segment_length", "speed_settings_advanced");
|
||||
optgroup->append_single_option_line("extrusion_rate_smoothing_external_perimeter_only", "speed_settings_advanced");
|
||||
optgroup->append_single_option_line("extrusion_rate_smoothing_external_perimeter_only", "speed_settings_advanced#apply-only-on-external-features");
|
||||
|
||||
page = add_options_page(L("Support"), "custom-gcode_support"); // ORCA: icon only visible on placeholders
|
||||
optgroup = page->new_optgroup(L("Support"), L"param_support");
|
||||
@@ -3133,7 +3135,7 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("support_object_xy_distance", "support_settings_advanced#supportobject-xy-distance");
|
||||
optgroup->append_single_option_line("support_object_first_layer_gap", "support_settings_advanced#supportobject-first-layer-gap");
|
||||
optgroup->append_single_option_line("bridge_no_support", "support_settings_advanced#dont-support-bridges");
|
||||
optgroup->append_single_option_line("max_bridge_length", "support_settings_advanced");
|
||||
optgroup->append_single_option_line("max_bridge_length", "support_settings_advanced#max-bridge-length");
|
||||
optgroup->append_single_option_line("independent_support_layer_height", "support_settings_advanced#independent-support-layer-height");
|
||||
|
||||
optgroup = page->new_optgroup(L("Tree supports"), L"param_support_tree");
|
||||
@@ -3147,21 +3149,21 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("tree_support_branch_angle", "support_settings_tree#branch-angle");
|
||||
optgroup->append_single_option_line("tree_support_branch_angle_organic", "support_settings_tree#branch-angle");
|
||||
optgroup->append_single_option_line("tree_support_angle_slow", "support_settings_tree#preferred-branch-angle");
|
||||
optgroup->append_single_option_line("tree_support_auto_brim", "support_settings_tree");
|
||||
optgroup->append_single_option_line("tree_support_brim_width", "support_settings_tree");
|
||||
optgroup->append_single_option_line("tree_support_auto_brim", "support_settings_tree#auto-brim-width");
|
||||
optgroup->append_single_option_line("tree_support_brim_width", "support_settings_tree#tree-support-brim-width");
|
||||
|
||||
page = add_options_page(L("Multimaterial"), "custom-gcode_multi_material"); // ORCA: icon only visible on placeholders
|
||||
optgroup = page->new_optgroup(L("Prime tower"), L"param_tower");
|
||||
optgroup->append_single_option_line("enable_prime_tower", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("prime_tower_skip_points", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("enable_tower_interface_features", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("prime_tower_skip_points", "multimaterial_settings_prime_tower#skip-points");
|
||||
optgroup->append_single_option_line("enable_tower_interface_features", "multimaterial_settings_prime_tower#enable-tower-interface-features");
|
||||
optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower#cool-down-from-interface-boost-during-prime-tower");
|
||||
optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower#internal-ribs");
|
||||
optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width");
|
||||
optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower#belt-purge-tower-width");
|
||||
optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower#prime-volume");
|
||||
optgroup->append_single_option_line("prime_tower_brim_width", "multimaterial_settings_prime_tower#brim-width");
|
||||
optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower#infill-gap");
|
||||
optgroup->append_single_option_line("wipe_tower_rotation_angle", "multimaterial_settings_prime_tower#wipe-tower-rotation-angle");
|
||||
optgroup->append_single_option_line("wipe_tower_bridging", "multimaterial_settings_prime_tower#maximal-bridging-distance");
|
||||
optgroup->append_single_option_line("wipe_tower_extra_spacing", "multimaterial_settings_prime_tower#wipe-tower-purge-lines-spacing");
|
||||
@@ -3174,7 +3176,7 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall");
|
||||
optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers");
|
||||
optgroup->append_single_option_line("wipe_tower_sparse_layers_combination", "multimaterial_settings_prime_tower#combine-sparse-layers");
|
||||
optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower");
|
||||
optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower#prime-all-printing-extruders");
|
||||
|
||||
optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features");
|
||||
optgroup->append_single_option_line("outer_wall_filament_id", "multimaterial_settings_filament_for_features#outer-walls");
|
||||
@@ -3193,7 +3195,7 @@ void TabPrint::build()
|
||||
|
||||
optgroup = page->new_optgroup(L("Flush options"), L"param_flush");
|
||||
optgroup->append_single_option_line("flush_into_infill", "multimaterial_settings_flush_options#flush-into-objects-infill");
|
||||
optgroup->append_single_option_line("flush_into_objects", "multimaterial_settings_flush_options");
|
||||
optgroup->append_single_option_line("flush_into_objects", "multimaterial_settings_flush_options#flush-into-this-object");
|
||||
optgroup->append_single_option_line("flush_into_support", "multimaterial_settings_flush_options#flush-into-objects-support");
|
||||
optgroup = page->new_optgroup(L("Advanced"), L"advanced");
|
||||
optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam");
|
||||
@@ -3245,7 +3247,7 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("spiral_finishing_flow_ratio", "others_settings_special_mode#spiral-finishing-flow-ratio");
|
||||
|
||||
optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse");
|
||||
optgroup->append_single_option_line("enable_wrapping_detection");
|
||||
optgroup->append_single_option_line("enable_wrapping_detection", "others_settings_special_mode#enable-clumping-detection");
|
||||
|
||||
optgroup = page->new_optgroup(L("Fuzzy skin"), L"fuzzy_skin");
|
||||
optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin#fuzzy-skin-mode");
|
||||
@@ -4861,7 +4863,7 @@ void TabFilament::build()
|
||||
|
||||
option = optgroup->get_option("compatible_printers_condition");
|
||||
option.opt.full_width = true;
|
||||
optgroup->append_single_option_line(option, "material_dependencies#compatible-printers");
|
||||
optgroup->append_single_option_line(option, "material_dependencies#printer-condition");
|
||||
|
||||
optgroup = page->new_optgroup(L("Compatible process profiles"), "param_dependencies_presets");
|
||||
create_line_with_widget(optgroup.get(), "compatible_prints", "", [this](wxWindow* parent) {
|
||||
@@ -4870,7 +4872,7 @@ void TabFilament::build()
|
||||
|
||||
option = optgroup->get_option("compatible_prints_condition");
|
||||
option.opt.full_width = true;
|
||||
optgroup->append_single_option_line(option, "material_dependencies#compatible-process-profiles");
|
||||
optgroup->append_single_option_line(option, "material_dependencies#process-condition");
|
||||
|
||||
page = add_options_page(L("Notes"), "custom-gcode_note"); // ORCA: icon only visible on placeholders
|
||||
optgroup = page->new_optgroup(L("Notes"),"note", 0);
|
||||
@@ -6063,7 +6065,7 @@ if (is_marlin_flavor)
|
||||
optgroup->append_single_option_line("extruder_printable_height", "printer_extruder_basic_information#extruder-layer-height-limits", extruder_idx);
|
||||
Option option = optgroup->get_option("extruder_printable_area", extruder_idx);
|
||||
option.opt.full_width = true;
|
||||
optgroup->append_single_option_line(option, "printer_extruder_basic_information#extruder-offset-position");
|
||||
optgroup->append_single_option_line(option, "printer_extruder_basic_information#extruder-printable-area");
|
||||
|
||||
optgroup->m_on_change = [this](const t_config_option_key& opt_key, boost::any value)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "WebGuideDialog.hpp"
|
||||
#include "CloudProvider.hpp"
|
||||
#include "ConfigWizard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -769,7 +770,7 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
if (wxGetApp().is_user_login()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: user_logout on user_guide_finish";
|
||||
// agent->user_logout();
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,17 @@
|
||||
#include <wx/textdlg.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/url.h>
|
||||
#include <wx/uri.h>
|
||||
|
||||
#include <slic3r/GUI/Widgets/WebView.hpp>
|
||||
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
|
||||
#include <wx/webview.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <webkit2/webkit2.h>
|
||||
#endif
|
||||
#include <wx/infobar.h>
|
||||
|
||||
namespace pt = boost::property_tree;
|
||||
@@ -55,6 +60,13 @@ namespace GUI {
|
||||
|
||||
#define LOGIN_INFO_UPDATE_TIMER_ID 10002
|
||||
|
||||
// OrcaCloud is a native pane of its own: the site refuses to be framed
|
||||
// (x-frame-options: DENY / CSP frame-ancestors 'none').
|
||||
static const char *const kCloudUrl = "https://cloud.orcaslicer.com";
|
||||
// #LeftBoard in resources/web/homepage/css/home.css is a 262px column plus a 1px right
|
||||
// border; the homepage keeps that width as the nav column while the cloud pane is open.
|
||||
static const int kLeftNavWidth = 263;
|
||||
|
||||
// The home page's strings (resources/web/homepage), keyed by their data-i18n and T() ids.
|
||||
static json home_page_strings()
|
||||
{
|
||||
@@ -136,7 +148,11 @@ WebViewPanel::WebViewPanel(wxWindow *parent)
|
||||
m_reset_on_show = WebView::NeedsRecreateOnShow();
|
||||
SetSizer(topsizer);
|
||||
|
||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
// The homepage shell is the left nav column; when OrcaCloud opens, its page lives in a
|
||||
// second native pane beside it (the site refuses to be framed).
|
||||
m_content_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_content_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
topsizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
// Log backend information
|
||||
/* m_browser->GetUserAgent() may lead crash
|
||||
@@ -292,12 +308,282 @@ void WebViewPanel::create_browser()
|
||||
m_browser->Hide();
|
||||
}
|
||||
|
||||
// The cloud page gets its own native view, created on first use and kept alive afterwards
|
||||
// so its session and scroll position survive hiding.
|
||||
void WebViewPanel::create_cloud_browser()
|
||||
{
|
||||
m_cloud_browser = WebView::CreateWebView(this, wxEmptyString);
|
||||
|
||||
#ifdef __linux__
|
||||
// The pane's web session is a cookie (guide §3), and WebKitGTK keeps cookies only in memory
|
||||
// unless a persistent store is set up, so without this the sign-in is lost on every restart
|
||||
// (and on every reset_browser). Each wxWebView has its own context, so give the cloud pane its
|
||||
// own store instead of sharing PrinterWebView's. Mirrors PrinterWebView::create_browser.
|
||||
if (auto* wv = static_cast<WebKitWebView*>(m_cloud_browser->GetNativeBackend())) {
|
||||
const auto cookies_path = boost::filesystem::path(data_dir() + "/cache/cloud_cookies.db");
|
||||
auto* cookie_manager = webkit_web_context_get_cookie_manager(webkit_web_view_get_context(wv));
|
||||
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookies_path.c_str(),
|
||||
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Webview events are command events, so they would otherwise propagate up to the
|
||||
// panel's handlers, which drive the homepage view. Keep the cloud pane's events here.
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_TITLE_CHANGED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
|
||||
m_cloud_browser->Hide();
|
||||
if (m_content_sizer)
|
||||
m_content_sizer->Add(m_cloud_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
}
|
||||
|
||||
// The scheme://host[:port] origin of an http(s) URL, lower-cased and with the default port
|
||||
// dropped, or empty for any other scheme or a malformed URL. Comparing origins (not a string
|
||||
// prefix) is what keeps a lookalike host such as "https://cloud.orcaslicer.com.evil.com" out.
|
||||
static wxString http_origin_of(const wxString& url)
|
||||
{
|
||||
const wxURI uri(url);
|
||||
if (!uri.HasScheme() || !uri.HasServer())
|
||||
return {};
|
||||
const wxString scheme = uri.GetScheme().Lower();
|
||||
if (scheme != "http" && scheme != "https")
|
||||
return {};
|
||||
wxString origin = scheme + "://" + uri.GetServer().Lower();
|
||||
const wxString port = uri.GetPort();
|
||||
if (!port.IsEmpty() && port != (scheme == "https" ? "443" : "80"))
|
||||
origin += ":" + port;
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Whether a NAVIGATING event will replace the pane's top-level document rather than load a
|
||||
// subframe. Edge and macOS report the target frame; WebKitGTK does not (IsTargetMainFrame() is
|
||||
// always false there), so a user-gesture navigation stands in: a page's own embedded frames load
|
||||
// without a gesture and stay put, while a user following a link to another site does not.
|
||||
static bool cloud_navigation_is_top_level(const wxWebViewEvent& evt)
|
||||
{
|
||||
#ifdef __WXGTK__
|
||||
return evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER;
|
||||
#else
|
||||
return evt.IsTargetMainFrame();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sends an off-site navigation to the system browser instead of embedding it. Deferred: this runs
|
||||
// inside a native webview navigation callback, so launching must not re-enter it.
|
||||
static void open_cloud_url_externally(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << "cloud pane: opening externally " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
wxGetApp().CallAfter([url] { wxLaunchDefaultBrowser(url); });
|
||||
}
|
||||
|
||||
// Consumed on the cloud view so it never reaches the homepage handlers. {web} pages stay in the
|
||||
// pane; every other site (OAuth providers included) opens in the system browser (guide §3).
|
||||
void WebViewPanel::OnCloudWebViewEvent(wxWebViewEvent& evt)
|
||||
{
|
||||
const wxEventType event_type = evt.GetEventType();
|
||||
if (event_type == wxEVT_WEBVIEW_NAVIGATING) {
|
||||
const wxString url = evt.GetURL();
|
||||
// Guide §3: keep every {web} page in the pane and open other sites (OAuth providers
|
||||
// included) in the system browser. Only top-level navigations are diverted, and
|
||||
// should_divert_cloud_url() excludes same-origin URLs and internal schemes.
|
||||
if (should_divert_cloud_url(url) && cloud_navigation_is_top_level(evt)) {
|
||||
evt.Veto();
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
} else if (event_type == wxEVT_WEBVIEW_NEWWINDOW) {
|
||||
const wxString url = evt.GetURL();
|
||||
if (!url.empty()) {
|
||||
if (is_cloud_origin(url))
|
||||
load_cloud_url(url);
|
||||
else
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
evt.Veto();
|
||||
} else if (event_type == wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED) {
|
||||
on_cloud_script_message(evt.GetString().utf8_string());
|
||||
} else if (event_type == wxEVT_WEBVIEW_LOADED || event_type == wxEVT_WEBVIEW_ERROR) {
|
||||
// A Start URL deferred behind a sign-out (see load_cloud_after_signout).
|
||||
if (!m_cloud_after_signout_url.IsEmpty()) {
|
||||
const wxString next = std::exchange(m_cloud_after_signout_url, wxString());
|
||||
load_cloud_url(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The page asks for a web session by posting JSON through window.wx; only the newest request
|
||||
// counts (tracked in GUI_App). Nothing here touches the UI directly: the desktop login dialog and
|
||||
// the network mint must not run on the webview's message-dispatch stack.
|
||||
void WebViewPanel::on_cloud_script_message(const std::string& body)
|
||||
{
|
||||
const json msg = json::parse(body, nullptr, false);
|
||||
if (msg.is_discarded() || !msg.is_object() || msg.value("command", std::string()) != "request_web_session")
|
||||
return;
|
||||
|
||||
std::string state;
|
||||
if (msg.contains("data") && msg["data"].is_object())
|
||||
state = msg["data"].value("state", std::string());
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
wxGetApp().CallAfter([state = std::move(state)] { wxGetApp().request_cloud_web_session(state); });
|
||||
}
|
||||
|
||||
void WebViewPanel::apply_cloud_layout(bool cloud_on)
|
||||
{
|
||||
if (!m_content_sizer)
|
||||
return;
|
||||
|
||||
// Cloud mode pins the homepage to the nav column width; otherwise it fills the panel.
|
||||
if (m_browser) {
|
||||
if (wxSizerItem* item = m_content_sizer->GetItem(m_browser))
|
||||
item->SetProportion(cloud_on ? 0 : 1);
|
||||
// kLeftNavWidth is in DIP: the web view's CSS px are DIP, so scale to physical pixels.
|
||||
m_browser->SetMinSize(cloud_on ? m_browser->FromDIP(wxSize(kLeftNavWidth, -1)) : wxDefaultSize);
|
||||
}
|
||||
Layout();
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_url(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
// Track the origin of whatever the pane is told to load (Start URL, Complete URL, sign-out or
|
||||
// the plain cloud fallback), so navigation policy has the right origin from the first load on.
|
||||
const wxString origin = http_origin_of(url);
|
||||
if (!origin.IsEmpty())
|
||||
m_cloud_base_url = origin;
|
||||
if (!m_cloud_browser)
|
||||
return;
|
||||
// Raw LoadURL: WebView::LoadUrl re-serialises through wxURI, which must not alter the
|
||||
// fragment that carries the one-time handoff token. Log only the redacted URL.
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
m_cloud_browser->LoadURL(url);
|
||||
}
|
||||
|
||||
bool WebViewPanel::is_cloud_origin(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
return !origin.IsEmpty() && !m_cloud_base_url.IsEmpty() && origin == m_cloud_base_url;
|
||||
}
|
||||
|
||||
bool WebViewPanel::should_divert_cloud_url(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
// Unknown cloud origin or a non-web scheme: never divert (the pane must keep working).
|
||||
if (origin.IsEmpty() || m_cloud_base_url.IsEmpty())
|
||||
return false;
|
||||
return origin != m_cloud_base_url;
|
||||
}
|
||||
|
||||
// Explicit sign-out ends the pane's own web session. That session is a cookie JS cannot clear
|
||||
// (HttpOnly, guide §3/§8) and ClearHistory() is a no-op on WebKitGTK, so drop the pane's stored
|
||||
// data natively. wx 3.3.2 ignores the type mask on every backend (any non-zero mask clears
|
||||
// everything) and needs an invalid `since` to delete cookies, so the default "all" call is used.
|
||||
void WebViewPanel::clear_cloud_storage()
|
||||
{
|
||||
if (m_cloud_browser)
|
||||
m_cloud_browser->ClearBrowsingData(wxWEBVIEW_BROWSING_DATA_ALL);
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_after_signout(const wxString& url)
|
||||
{
|
||||
if (m_cloud_pending_signout && !m_cloud_signout_url.IsEmpty()) {
|
||||
// The pane did not exist when the user signed out, so its persisted session must be
|
||||
// cleared before the sign-out page runs and `url` loads once that navigation finishes
|
||||
// (see OnCloudWebViewEvent).
|
||||
m_cloud_pending_signout = false;
|
||||
m_cloud_after_signout_url = url;
|
||||
clear_cloud_storage();
|
||||
load_cloud_url(m_cloud_signout_url);
|
||||
return;
|
||||
}
|
||||
load_cloud_url(url);
|
||||
}
|
||||
|
||||
void WebViewPanel::schedule_cloud_load(const wxString& url)
|
||||
{
|
||||
if (!m_cloud_browser) {
|
||||
load_cloud_after_signout(url);
|
||||
return;
|
||||
}
|
||||
// CreateWebView queues the "wx" handler add on the view (macOS/Linux); queue the load behind
|
||||
// it so the page's first scripts already see the bridge (guide §3).
|
||||
m_cloud_browser->CallAfter([this, url]() { load_cloud_after_signout(url); });
|
||||
}
|
||||
|
||||
void WebViewPanel::cloud_signed_out(const wxString& signout_url)
|
||||
{
|
||||
if (signout_url.empty())
|
||||
return;
|
||||
m_cloud_signout_url = signout_url;
|
||||
// The next Start URL must re-seed the pane (it now reflects the signed-out state), so forget
|
||||
// the previous seed regardless of which branch below runs.
|
||||
m_cloud_seed_url.clear();
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_pending_signout = false;
|
||||
// Run the sign-out page (ends the server-side session), then clear the pane's stored data.
|
||||
load_cloud_url(signout_url);
|
||||
clear_cloud_storage();
|
||||
} else {
|
||||
// Pane never opened this run: clear the persisted session on its first open.
|
||||
m_cloud_pending_signout = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
{
|
||||
if (on) {
|
||||
// load_cloud_url records the origin from whatever it loads, including the plain cloud
|
||||
// fallback used when `url` is empty.
|
||||
if (!m_cloud_browser) {
|
||||
create_cloud_browser();
|
||||
m_cloud_seed_url = url;
|
||||
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
|
||||
} else if (!url.empty() && url != m_cloud_seed_url) {
|
||||
// Re-seed only when the Start URL actually changed — a login, an account switch or a
|
||||
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
|
||||
// Start URL re-runs the page's auth check on every open, which flashes "Signing you
|
||||
// in…" and re-requests a session (guide §10: mint when the page asks, not per open).
|
||||
// The pane is kept alive, so an unchanged open just shows the page it already holds.
|
||||
m_cloud_seed_url = url;
|
||||
load_cloud_after_signout(url);
|
||||
}
|
||||
m_cloud_browser->Show();
|
||||
} else if (m_cloud_browser) {
|
||||
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
|
||||
// again (guide §10). Nothing is cleared here.
|
||||
m_cloud_browser->Hide();
|
||||
}
|
||||
apply_cloud_layout(on && m_cloud_browser != nullptr);
|
||||
}
|
||||
|
||||
void WebViewPanel::reset_browser()
|
||||
{
|
||||
// Rebuild both views the same way, keeping the cloud pane's open/closed state.
|
||||
const bool cloud_on = m_cloud_browser && m_cloud_browser->IsShown();
|
||||
|
||||
m_browser->Destroy(); // also removes it from the sizer
|
||||
create_browser();
|
||||
GetSizer()->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
Layout();
|
||||
m_content_sizer->Insert(size_t(0), m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_browser->Destroy();
|
||||
m_cloud_browser = nullptr;
|
||||
create_cloud_browser();
|
||||
schedule_cloud_load(wxString(kCloudUrl));
|
||||
m_cloud_browser->Show(cloud_on);
|
||||
// The rebuilt pane holds no seed, so the next open loads the Start URL again.
|
||||
m_cloud_seed_url.clear();
|
||||
}
|
||||
|
||||
apply_cloud_layout(cloud_on);
|
||||
}
|
||||
|
||||
bool WebViewPanel::Show(bool show)
|
||||
@@ -647,7 +933,7 @@ void WebViewPanel::update_mode()
|
||||
*/
|
||||
void WebViewPanel::OnNavigationRequest(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
const wxString &url = evt.GetURL();
|
||||
if (url.StartsWith("File://") || url.StartsWith("file://")) {
|
||||
if (!url.Contains("/web/homepage/index.html")) {
|
||||
@@ -693,7 +979,7 @@ void WebViewPanel::OnNavigationComplete(wxWebViewEvent& evt)
|
||||
{
|
||||
m_browser->Show();
|
||||
Layout();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
|
||||
UpdateState();
|
||||
@@ -712,6 +998,9 @@ void WebViewPanel::OnDocumentLoaded(wxWebViewEvent& evt)
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'");
|
||||
}
|
||||
// A reloaded homepage loses its cloud-mode class; restore it if the pane is open.
|
||||
if (m_cloud_browser && m_cloud_browser->IsShown())
|
||||
RunScript("SetCloudMode(true)");
|
||||
UpdateState();
|
||||
SendCloudProvidersInfo();
|
||||
}
|
||||
@@ -727,7 +1016,7 @@ void WebViewPanel::OnTitleChanged(wxWebViewEvent &evt)
|
||||
*/
|
||||
void WebViewPanel::OnNewWindow(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
wxString flag = " (other)";
|
||||
|
||||
if (evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "wx/fs_mem.h"
|
||||
#include "wx/stdpaths.h"
|
||||
#include <wx/panel.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/tbarbase.h>
|
||||
#include "wx/textctrl.h"
|
||||
#include <wx/timer.h>
|
||||
@@ -114,14 +115,62 @@ public:
|
||||
|
||||
void update_mode();
|
||||
|
||||
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
|
||||
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
|
||||
// whether it already has a session; empty falls back to the plain cloud page.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString());
|
||||
|
||||
// True while the OrcaCloud pane exists and is visible.
|
||||
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
|
||||
|
||||
// Loads `url` in the cloud pane — the webview that owns the session handoff. Raw LoadURL (no
|
||||
// wxURI re-serialisation) so the Complete URL's #fragment stays intact; never logged.
|
||||
void load_cloud_url(const wxString& url);
|
||||
|
||||
// Clears the cloud pane's own session on an explicit desktop sign-out. If the pane has not
|
||||
// been created yet, the sign-out is remembered and applied when it is first opened.
|
||||
void cloud_signed_out(const wxString& signout_url);
|
||||
|
||||
bool Show(bool show = true) override;
|
||||
private:
|
||||
void create_browser();
|
||||
void create_cloud_browser();
|
||||
void reset_browser();
|
||||
// Consumes the cloud pane's webview events so they stay off the homepage handlers.
|
||||
void OnCloudWebViewEvent(wxWebViewEvent& evt);
|
||||
// True when `url` has the same scheme/host/port as the cloud origin ({web}).
|
||||
bool is_cloud_origin(const wxString& url) const;
|
||||
// True when a navigation to `url` must leave the pane: an http(s) URL on another origin than
|
||||
// the (known) cloud origin. False for non-web schemes and while the origin is unknown.
|
||||
bool should_divert_cloud_url(const wxString& url) const;
|
||||
// Parses a request_web_session message and defers it to GUI_App.
|
||||
void on_cloud_script_message(const std::string& body);
|
||||
// Loads `url`, first clearing a lazily-required sign-out (see cloud_signed_out).
|
||||
void load_cloud_after_signout(const wxString& url);
|
||||
// First load of a freshly created pane: deferred so CreateWebView's queued "wx" handler add
|
||||
// runs first and the page sees window.wx at load time (guide §3).
|
||||
void schedule_cloud_load(const wxString& url);
|
||||
// Clears the pane's stored web data (its cookie-backed session) on an explicit sign-out.
|
||||
void clear_cloud_storage();
|
||||
// Re-applies the content sizer's proportions and min sizes for the cloud state.
|
||||
void apply_cloud_layout(bool cloud_on);
|
||||
|
||||
wxWebView* m_browser;
|
||||
wxWebView* m_cloud_browser{nullptr};
|
||||
wxBoxSizer* m_content_sizer{nullptr};
|
||||
wxString m_home_url;
|
||||
bool m_reset_on_show{false};
|
||||
// {web} origin (scheme://host[:port]) of the loaded pane. Set from every load, including the
|
||||
// plain cloud fallback, so navigation policy can keep {web} pages in the pane and send other
|
||||
// sites to the system browser.
|
||||
wxString m_cloud_base_url;
|
||||
// Start URL the pane was last seeded with. An open whose URL matches this is an ordinary
|
||||
// re-open (nothing changed), so it only shows the pane instead of reloading it (guide §10).
|
||||
wxString m_cloud_seed_url;
|
||||
// Sign-out clear the pane must apply once it exists / next loads.
|
||||
wxString m_cloud_signout_url;
|
||||
wxString m_cloud_after_signout_url;
|
||||
bool m_cloud_pending_signout{false};
|
||||
wxButton * m_button_stop;
|
||||
wxTextCtrl *m_url;
|
||||
#if !BBL_RELEASE_TO_PUBLIC
|
||||
|
||||
@@ -444,6 +444,12 @@ bool WebView::DownloadAndInstallWebViewRuntime()
|
||||
return DownloadAndInstallWV2RT() == 0;
|
||||
}
|
||||
#endif
|
||||
wxString WebView::RedactUrl(const wxString& url)
|
||||
{
|
||||
const size_t cut = url.find_first_of(wxString("?#"));
|
||||
return cut == wxString::npos ? url : url.Left(cut);
|
||||
}
|
||||
|
||||
void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
{
|
||||
auto url2 = url;
|
||||
@@ -451,7 +457,7 @@ void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
url2.Replace("\\", "/");
|
||||
#endif
|
||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << url2.ToUTF8();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << RedactUrl(url2).ToUTF8();
|
||||
webView->LoadURL(url2);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public:
|
||||
#endif
|
||||
static void LoadUrl(wxWebView * webView, wxString const &url);
|
||||
|
||||
// Returns the URL truncated at the first '?' or '#', so a query/fragment carrying secrets
|
||||
// (e.g. a session-handoff token) is never written to a log.
|
||||
static wxString RedactUrl(const wxString& url);
|
||||
|
||||
static bool RunScript(wxWebView * webView, wxString const & msg);
|
||||
|
||||
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
|
||||
|
||||
@@ -485,6 +485,14 @@ void Http::priv::http_perform()
|
||||
if (!postfields.empty()) {
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size());
|
||||
} else if (method == "POST" && form == nullptr && mime == nullptr) {
|
||||
// A POST with no body. Without CURLOPT_POSTFIELDS, curl takes the request body from
|
||||
// the read callback, but form_file_read_cb only has a usable user pointer for a PUT
|
||||
// (set_put_body) or a streamed multipart part (form_add_file); for an empty POST it
|
||||
// dereferences libcurl's default READDATA as a form_file and crashes. Point curl at a
|
||||
// zero-length body so the read callback is never used.
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(0));
|
||||
}
|
||||
|
||||
CURLcode res = ::curl_easy_perform(curl);
|
||||
|
||||
@@ -109,6 +109,10 @@ constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers";
|
||||
|
||||
constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login";
|
||||
|
||||
// Gateway endpoint that mints a one-time web-session handoff token for the signed-in desktop
|
||||
// user. The cloud pane reloads the returned token into the webview (see build_web_session_*).
|
||||
constexpr const char* ORCA_WEB_SESSION_HANDOFF_PATH = "/api/v1/account/web-session-handoff";
|
||||
|
||||
constexpr const char* CONFIG_ORCA_API_URL = "orca_api_url";
|
||||
constexpr const char* CONFIG_ORCA_AUTH_URL = "orca_auth_url";
|
||||
constexpr const char* CONFIG_ORCA_CLOUD_URL = "orca_cloud_url";
|
||||
@@ -3019,6 +3023,148 @@ std::string OrcaCloudServiceAgent::get_cloud_login_url(const std::string& langua
|
||||
return url;
|
||||
}
|
||||
|
||||
// Percent-encodes every byte outside the RFC 3986 unreserved set (A-Za-z0-9-._~), so token values
|
||||
// survive fragment parsing intact. Deliberately tiny: no URL-encoder dependency.
|
||||
static std::string percent_encode_unreserved(const std::string& s)
|
||||
{
|
||||
static const char* const hex = "0123456789ABCDEF";
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (unsigned char c : s) {
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '.' || c == '_' || c == '~') {
|
||||
out.push_back(static_cast<char>(c));
|
||||
} else {
|
||||
out.push_back('%');
|
||||
out.push_back(hex[c >> 4]);
|
||||
out.push_back(hex[c & 0x0F]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_start_url(const std::string& next) const
|
||||
{
|
||||
if (cloud_base_url.empty())
|
||||
return "";
|
||||
|
||||
// The page replaces a `next` that is not a same-site path with /app/profiles, so pass a
|
||||
// same-site path (the guide's default) and let it validate.
|
||||
std::string url = cloud_base_url + "/auth/handoff?next=" +
|
||||
percent_encode_unreserved(next.empty() ? "/app/profiles" : next);
|
||||
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
if (session.logged_in && !session.user_id.empty())
|
||||
url += "&user_id=" + percent_encode_unreserved(session.user_id);
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_complete_url(const std::string& token_hash,
|
||||
const std::string& state) const
|
||||
{
|
||||
if (cloud_base_url.empty() || token_hash.empty() || state.empty())
|
||||
return "";
|
||||
|
||||
// Fragment (not query) so the one-time token never reaches a server or a log.
|
||||
return cloud_base_url + "/auth/handoff#token_hash=" + percent_encode_unreserved(token_hash) +
|
||||
"&state=" + percent_encode_unreserved(state);
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_signout_url() const
|
||||
{
|
||||
if (cloud_base_url.empty())
|
||||
return "";
|
||||
return cloud_base_url + "/auth/handoff?signout=1";
|
||||
}
|
||||
|
||||
// Extracts the machine-readable code from a handoff error body. 401 uses {"error":"<code>"};
|
||||
// the other statuses use {"error":{"code":"<code>","message":"..."}}.
|
||||
static std::string handoff_error_code(const std::string& body)
|
||||
{
|
||||
const json j = json::parse(body, nullptr, false);
|
||||
if (j.is_discarded() || !j.is_object() || !j.contains("error"))
|
||||
return "";
|
||||
const auto& err = j["error"];
|
||||
if (err.is_string())
|
||||
return err.get<std::string>();
|
||||
if (err.is_object() && err.contains("code") && err["code"].is_string())
|
||||
return err["code"].get<std::string>();
|
||||
return "";
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::request_web_session_handoff(std::string& token_hash, std::string& error_code,
|
||||
unsigned int& http_code)
|
||||
{
|
||||
token_hash.clear();
|
||||
error_code.clear();
|
||||
http_code = 0;
|
||||
|
||||
const std::string url = api_base_url + ORCA_WEB_SESSION_HANDOFF_PATH;
|
||||
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << ORCA_WEB_SESSION_HANDOFF_PATH;
|
||||
|
||||
// Refresh the desktop token the usual way before minting (newest access token).
|
||||
ensure_token_fresh("web_session_handoff");
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
auto http = Http::post(url);
|
||||
http.tls_verify(true);
|
||||
|
||||
const std::string token = get_access_token();
|
||||
|
||||
auto headers = data_headers();
|
||||
for (const auto& pair : headers) {
|
||||
http.header(pair.first, pair.second);
|
||||
}
|
||||
if (!token.empty()) {
|
||||
http.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
// No request body: the endpoint identifies the user by the bearer token alone.
|
||||
http.on_complete([&](std::string resp_body, unsigned resp_status) {
|
||||
result.success = true;
|
||||
result.status = resp_status;
|
||||
result.body = resp_body;
|
||||
})
|
||||
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
|
||||
result.success = false;
|
||||
result.status = resp_status == 0 ? 404 : resp_status;
|
||||
result.body = resp_body;
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: web-session-handoff HTTP error - " << error;
|
||||
})
|
||||
.timeout_max(30)
|
||||
.perform_sync();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: web-session-handoff exception - " << e.what();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
// Reuse the shared 401 policy (refresh once, then retry), exactly like http_get/http_post:
|
||||
// `suppress` is true for a transient refresh failure (keep the session and token, no logout)
|
||||
// and false when the refresh token was rejected — so a 401 that remains after the refresh
|
||||
// surfaces through the generic HTTP-error callback and drives the normal desktop logout.
|
||||
// Every other handoff status (409/429/503/transport) stays silent here, so the page can
|
||||
// offer "Try again" instead of the desktop showing a generic cloud error.
|
||||
const bool suppress = resolve_unauthorized(res, perform, "web_session_handoff");
|
||||
if (!suppress && res.status == 401)
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
|
||||
http_code = res.status;
|
||||
if (res.status == 200) {
|
||||
const json j = json::parse(res.body, nullptr, false);
|
||||
if (!j.is_discarded() && j.is_object() && j.contains("token_hash") && j["token_hash"].is_string())
|
||||
token_hash = j["token_hash"].get<std::string>();
|
||||
} else {
|
||||
error_code = handoff_error_code(res.body);
|
||||
}
|
||||
|
||||
return (res.success && res.status == 200) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::get_mw_user_preference(std::function<void(std::string)> callback)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_preference (stub)";
|
||||
|
||||
@@ -203,6 +203,28 @@ public:
|
||||
// ========================================================================
|
||||
std::string get_cloud_service_host() override;
|
||||
std::string get_cloud_login_url(const std::string& language = "") override;
|
||||
|
||||
// ========================================================================
|
||||
// OrcaCloud webview session handoff (OrcaCloud "Webview Session Handoff" guide)
|
||||
// ========================================================================
|
||||
// The cloud pane never receives the desktop's access/refresh tokens. Instead the page posts
|
||||
// request_web_session, the desktop mints a single-use token through the gateway and loads the
|
||||
// Complete URL; the site then signs itself in. These builders return "" when no cloud URL is
|
||||
// configured.
|
||||
//
|
||||
// Start URL: {web}/auth/handoff?next=<path>[&user_id=<desktop user id>]. `next` defaults to
|
||||
// /app/profiles; user_id is included only while signed in.
|
||||
std::string build_web_session_start_url(const std::string& next = std::string()) const;
|
||||
// Complete URL: {web}/auth/handoff#token_hash=<hash>&state=<state>. Both values URL-encoded.
|
||||
std::string build_web_session_complete_url(const std::string& token_hash, const std::string& state) const;
|
||||
// Clears only the webview's own session; loaded on an explicit desktop sign-out.
|
||||
std::string build_web_session_signout_url() const;
|
||||
// POST {api}/api/v1/account/web-session-handoff with the desktop access token (no body),
|
||||
// refreshing once on 401 through the shared policy. On 200 writes the single-use `token_hash`;
|
||||
// otherwise writes the server's error code (e.g. WEB_SESSION_HANDOFF_UNAVAILABLE) into
|
||||
// `error_code`. Blocking: call from a worker thread. Never logs the token.
|
||||
int request_web_session_handoff(std::string& token_hash, std::string& error_code, unsigned int& http_code);
|
||||
|
||||
int connect_server() override;
|
||||
bool is_server_connected() override;
|
||||
int refresh_connection() override;
|
||||
|
||||
Reference in New Issue
Block a user