diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake index 8056a01d3a..95f54c1a29 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/wxWidgets/wxWidgets.cmake @@ -38,6 +38,7 @@ prusaslicer_add_cmake_project(wxWidgets -DwxUSE_LIBSDL=OFF -DwxUSE_XTEST=OFF -DwxUSE_GLCANVAS_EGL=OFF + -DwxUSE_WEBREQUEST=OFF ) if (MSVC) diff --git a/resources/icons/measure.svg b/resources/icons/measure.svg new file mode 100644 index 0000000000..3ea137a1ed --- /dev/null +++ b/resources/icons/measure.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + diff --git a/resources/profiles/Anker.idx b/resources/profiles/Anker.idx new file mode 100644 index 0000000000..4a988bbdff --- /dev/null +++ b/resources/profiles/Anker.idx @@ -0,0 +1,2 @@ +min_slic3r_version = 2.6.0-alpha1 +1.0.0 Initial Version diff --git a/resources/profiles/Anker.ini b/resources/profiles/Anker.ini new file mode 100644 index 0000000000..c7408f6e4d --- /dev/null +++ b/resources/profiles/Anker.ini @@ -0,0 +1,393 @@ +# Print profiles for the AnkerMake printers. +# https://github.com/prusa3d/PrusaSlicer/pull/9075 by @just-trey + +[vendor] +# Vendor name will be shown by the Config Wizard. +name = Anker +# Configuration version of this file. Config file will only be installed, if the config_version differs. +# This means, the server may force the PrusaSlicer configuration to be downgraded. +config_version = 1.0.0 +# Where to get the updates from? +config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Anker/ + +# The printer models will be shown by the Configuration Wizard in this order, +# also the first model installed & the first nozzle installed will be activated after install. +# Printer model name will be shown by the installation wizard. + +[printer_model:M5] +name = AnkerMake M5 +variants = 0.4 +technology = FFF +family = AnkerMake +bed_model = M5-bed.stl +bed_texture = M5-texture.svg +default_materials = Generic PLA+ @ANKER; Generic PLA @ANKER; Generic PET @ANKER; Generic ABS @ANKER + +# All presets starting with asterisk, for example *common*, are intermediate and they will +# not make it into the user interface. + +# Common print preset +[print:*common*] +avoid_crossing_perimeters = 0 +bridge_acceleration = 2500 +bridge_angle = 0 +bridge_flow_ratio = 0.95 +bridge_speed = 150 +brim_separation = 0.1 +brim_type = outer_only +brim_width = 0 +clip_multipart_objects = 1 +complete_objects = 0 +default_acceleration = 2500 +dont_support_bridges = 1 +elefant_foot_compensation = 0.1 +ensure_vertical_shell_thickness = 1 +external_perimeter_speed = 150 +external_perimeters_first = 0 +extra_perimeters = 0 +extruder_clearance_height = 30 +extruder_clearance_radius = 45 +fill_angle = 45 +fill_density = 15% +fill_pattern = cubic +first_layer_acceleration = 2500 +first_layer_acceleration_over_raft = 0 +first_layer_extrusion_width = 200% +first_layer_speed = 50% +first_layer_speed_over_raft = 30 +gap_fill_enabled = 1 +gap_fill_speed = 150 +gcode_comments = 0 +infill_acceleration = 2500 +infill_anchor = 600% +infill_anchor_max = 50 +infill_every_layers = 1 +infill_extruder = 1 +infill_first = 0 +infill_only_where_needed = 0 +infill_overlap = 23% +infill_speed = 250 +interface_shells = 0 +max_print_speed = 250 +max_volumetric_extrusion_rate_slope_negative = 0 +max_volumetric_extrusion_rate_slope_positive = 0 +max_volumetric_speed = 0 +min_skirt_length = 4 +notes = +only_retract_when_crossing_perimeters = 0 +ooze_prevention = 0 +output_filename_format = {input_filename_base}_{digits(layer_height,1,2)}mm_{filament_type[0]}_{printer_model}.gcode +overhangs = 1 +perimeter_acceleration = 2500 +perimeter_extruder = 1 +perimeter_extrusion_width = 0 +perimeter_generator = arachne +perimeter_speed = 250 +perimeters = 3 +post_process = +print_settings_id = +raft_layers = 0 +resolution = 0 +seam_position = aligned +single_extruder_multi_material_priming = 0 +skirt_distance = 3 +skirt_height = 1 +skirts = 3 +small_perimeter_speed = 150 +solid_infill_below_area = 0 +solid_infill_every_layers = 0 +solid_infill_extruder = 1 +solid_infill_speed = 175 +spiral_vase = 0 +standby_temperature_delta = -5 +support_material = 0 +support_material_angle = 0 +support_material_buildplate_only = 0 +support_material_contact_distance = 0.15 +support_material_enforce_layers = 0 +support_material_extruder = 0 +support_material_interface_contact_loops = 0 +support_material_interface_extruder = 0 +support_material_interface_layers = 2 +support_material_interface_spacing = 0.2 +support_material_interface_speed = 100% +support_material_pattern = rectilinear +support_material_spacing = 2 +support_material_speed = 125 +support_material_synchronize_layers = 0 +support_material_threshold = 40 +support_material_with_sheath = 0 +support_material_xy_spacing = 60% +thick_bridges = 1 +thin_walls = 0 +top_solid_infill_speed = 150 +travel_speed = 300 +travel_speed_z = 10 +wipe_tower = 0 +wipe_tower_bridging = 10 +wipe_tower_rotation_angle = 0 +wipe_tower_width = 60 +wipe_tower_x = 170 +wipe_tower_y = 140 +xy_size_compensation = 0 + +[print:*0.08mm*] +inherits = *common* +layer_height = 0.08 +first_layer_height = 0.12 +bottom_solid_layers = 9 +top_solid_layers = 11 +bridge_flow_ratio = 0.70 + +[print:*0.10mm*] +inherits = *common* +layer_height = 0.10 +first_layer_height = 0.14 +bottom_solid_layers = 7 +top_solid_layers = 9 +bridge_flow_ratio = 0.70 + +[print:*0.12mm*] +inherits = *common* +layer_height = 0.12 +first_layer_height = 0.16 +bottom_solid_layers = 6 +top_solid_layers = 7 +bridge_flow_ratio = 0.70 + +[print:*0.16mm*] +inherits = *common* +layer_height = 0.16 +first_layer_height = 0.20 +bottom_solid_layers = 5 +top_solid_layers = 7 +bridge_flow_ratio = 0.85 + +[print:*0.20mm*] +inherits = *common* +layer_height = 0.20 +first_layer_height = 0.24 +bottom_solid_layers = 4 +top_solid_layers = 5 + +[print:*0.24mm*] +inherits = *common* +layer_height = 0.24 +first_layer_height = 0.28 +bottom_solid_layers = 3 +top_solid_layers = 4 + +[print:*0.28mm*] +inherits = *common* +layer_height = 0.28 +first_layer_height = 0.28 +bottom_solid_layers = 3 +top_solid_layers = 4 + +[print:0.08 mm SUPERDETAIL (0.4 mm nozzle) @ANKER] +inherits = *0.08mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[print:0.10 mm HIGHDETAIL (0.4 mm nozzle) @ANKER] +inherits = *0.10mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[print:0.12 mm DETAIL (0.4 mm nozzle) @ANKER] +inherits = *0.12mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[print:0.16 mm OPTIMAL (0.4 mm nozzle) @ANKER] +inherits = *0.16mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[print:0.20 mm NORMAL (0.4 mm nozzle) @ANKER] +inherits = *0.20mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[print:0.24 mm DRAFT (0.4 mm nozzle) @ANKER] +inherits = *0.24mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[print:0.28 mm SUPERDRAFT (0.4 mm nozzle) @ANKER] +inherits = *0.28mm* +compatible_printers_condition = printer_model=~/(M5).*/ and nozzle_diameter[0]==0.4 + +[filament:*common*] +cooling = 0 +compatible_printers = +extrusion_multiplier = 1 +filament_cost = 0 +filament_density = 0 +filament_diameter = 1.75 +filament_notes = "" +filament_settings_id = "" +filament_soluble = 0 +min_print_speed = 30 +slowdown_below_layer_time = 8 +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_ANKERMAKE.*/ + +[filament:*PLA*] +inherits = *common* +bed_temperature = 60 +fan_below_layer_time = 100 +filament_colour = #DDDDDD +filament_type = PLA +filament_density = 1.24 +filament_cost = 20 +first_layer_bed_temperature = 60 +first_layer_temperature = 210 +fan_always_on = 1 +max_fan_speed = 100 +min_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 2 +temperature = 205 + +[filament:*PLA+*] +inherits = *common* +bed_temperature = 60 +fan_below_layer_time = 100 +filament_colour = #DDDDDD +filament_type = PLA +filament_density = 1.24 +filament_cost = 20 +first_layer_bed_temperature = 60 +first_layer_temperature = 220 +fan_always_on = 1 +max_fan_speed = 100 +min_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 2 +temperature = 210 + +[filament:*PET*] +inherits = *common* +bed_temperature = 70 +disable_fan_first_layers = 2 +fan_below_layer_time = 20 +filament_colour = #DDDDDD +filament_type = PETG +filament_density = 1.27 +filament_cost = 30 +first_layer_bed_temperature = 70 +first_layer_temperature = 240 +fan_always_on = 1 +max_fan_speed = 50 +min_fan_speed = 50 +bridge_fan_speed = 100 +temperature = 240 + +[filament:*ABS*] +inherits = *common* +bed_temperature = 90 +disable_fan_first_layers = 2 +fan_below_layer_time = 20 +filament_colour = #DDDDDD +filament_type = ABS +filament_density = 1.04 +filament_cost = 20 +first_layer_bed_temperature = 100 +first_layer_temperature = 245 +fan_always_on = 0 +max_fan_speed = 0 +min_fan_speed = 0 +bridge_fan_speed = 30 +top_fan_speed = 0 +temperature = 245 + +[filament:Generic PLA @ANKER] +inherits = *PLA* +filament_vendor = Generic + +[filament:Generic PLA+ @ANKER] +inherits = *PLA+* +filament_vendor = Generic + +[filament:Generic PETG @ANKER] +inherits = *PET* +filament_vendor = Generic + +[filament:Generic ABS @ANKER] +inherits = *ABS* +first_layer_bed_temperature = 90 +bed_temperature = 90 +filament_vendor = Generic + + +# Common printer preset +[printer:*common*] +printer_technology = FFF +before_layer_gcode = ;BEFORE_LAYER_CHANGE\n;{layer_z} +between_objects_gcode = +pause_print_gcode = +deretract_speed = 60 +extruder_colour = #FCE94F +extruder_offset = 0x0 +gcode_flavor = marlin +silent_mode = 0 +remaining_times = 0 +machine_max_acceleration_e = 2500 +machine_max_acceleration_extruding = 2500 +machine_max_acceleration_retracting = 2500 +machine_max_acceleration_travel = 1500,1250 +machine_max_acceleration_x = 2500 +machine_max_acceleration_y = 2500 +machine_max_acceleration_z = 2500 +machine_max_feedrate_e = 100 +machine_max_feedrate_x = 300 +machine_max_feedrate_y = 300 +machine_max_feedrate_z = 20 +machine_max_jerk_e = 3 +machine_max_jerk_x = 30 +machine_max_jerk_y = 30 +machine_max_jerk_z = 0.3 +machine_min_extruding_rate = 0 +machine_min_travel_rate = 0 +layer_gcode = ;AFTER_LAYER_CHANGE\n;{layer_z} +max_print_height = 250 +printer_notes = +printer_settings_id = +retract_before_travel = 2 +retract_before_wipe = 70% +retract_layer_change = 1 +retract_length_toolchange = 1 +retract_lift = 0 +retract_lift_above = 0 +retract_lift_below = 0 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 60 +single_extruder_multi_material = 0 +thumbnails = 64x64,256x256 +thumbnails_format = JPG +toolchange_gcode = +use_firmware_retraction = 0 +use_relative_e_distances = 0 +use_volumetric_e = 0 +variable_layer_height = 1 +wipe = 1 +z_offset = 0 +default_filament_profile = "Generic PLA+ @ANKER" +start_gcode = M104 S{first_layer_temperature[0]} ; set final nozzle temp\nM190 S{first_layer_bed_temperature[0]} ; set and wait for nozzle temp to stabilize\nM109 S{first_layer_temperature[0]} ; wait for nozzle temp to stabilize\nG28 ;Home\nG1 E10 F3600; push out retracted filament(fix for over retraction after prime) +end_gcode = M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG28 X0 Y0\nM84 + +[printer:*M5*] +inherits = *common* +bed_shape = 0x0,235-0,235x235,0x235 +max_print_height = 250 +printer_model = M5 +retract_length = 1.25 +retract_speed = 60 +deretract_speed = 60 +retract_before_travel = 1 +retract_before_wipe = 0% +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_ANKERMAKE\nPRINTER_MODEL_M5 + +[printer:AnkerMake M5 (0.4 mm nozzle)] +inherits = *M5* +nozzle_diameter = 0.4 +printer_variant = 0.4 +min_layer_height = 0.08 +max_layer_height = 0.32 +retract_lift_above = 0.2 +default_print_profile = "0.16 mm OPTIMAL (0.4 mm nozzle) @ANKER" \ No newline at end of file diff --git a/resources/profiles/Anker/M5-bed.stl b/resources/profiles/Anker/M5-bed.stl new file mode 100644 index 0000000000..72e12a23cd Binary files /dev/null and b/resources/profiles/Anker/M5-bed.stl differ diff --git a/resources/profiles/Anker/M5-texture.svg b/resources/profiles/Anker/M5-texture.svg new file mode 100644 index 0000000000..8b04342e3f --- /dev/null +++ b/resources/profiles/Anker/M5-texture.svg @@ -0,0 +1,1003 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/profiles/Anker/M5_thumbnail.png b/resources/profiles/Anker/M5_thumbnail.png new file mode 100644 index 0000000000..e137319201 Binary files /dev/null and b/resources/profiles/Anker/M5_thumbnail.png differ diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index 678c8fe20c..6dabace5b5 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -157,23 +157,25 @@ namespace ImGui const wchar_t InfoMarker = 0x2603; const wchar_t SliderFloatEditBtnIcon = 0x2604; const wchar_t SliderFloatEditBtnPressedIcon = 0x2605; - const wchar_t LegendTravel = 0x2606; - const wchar_t LegendWipe = 0x2607; - const wchar_t LegendRetract = 0x2608; - const wchar_t LegendDeretract = 0x2609; - const wchar_t LegendSeams = 0x2610; - const wchar_t LegendToolChanges = 0x2611; - const wchar_t LegendColorChanges = 0x2612; - const wchar_t LegendPausePrints = 0x2613; - const wchar_t LegendCustomGCodes = 0x2614; - const wchar_t LegendCOG = 0x2615; - const wchar_t LegendShells = 0x2616; - const wchar_t LegendToolMarker = 0x2617; - const wchar_t WarningMarkerSmall = 0x2618; - const wchar_t ExpandBtn = 0x2619; - const wchar_t CollapseBtn = 0x2620; - const wchar_t InfoMarkerSmall = 0x2621; + const wchar_t ClipboardBtnIcon = 0x2606; -// void MyFunction(const char* name, const MyMatrix44& v); + const wchar_t LegendTravel = 0x2701; + const wchar_t LegendWipe = 0x2702; + const wchar_t LegendRetract = 0x2703; + const wchar_t LegendDeretract = 0x2704; + const wchar_t LegendSeams = 0x2705; + const wchar_t LegendToolChanges = 0x2706; + const wchar_t LegendColorChanges = 0x2707; + const wchar_t LegendPausePrints = 0x2708; + const wchar_t LegendCustomGCodes = 0x2709; + const wchar_t LegendCOG = 0x2710; + const wchar_t LegendShells = 0x2711; + const wchar_t LegendToolMarker = 0x2712; + const wchar_t WarningMarkerSmall = 0x2713; + const wchar_t ExpandBtn = 0x2714; + const wchar_t CollapseBtn = 0x2715; + const wchar_t InfoMarkerSmall = 0x2716; + + // void MyFunction(const char* name, const MyMatrix44& v); } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 012e8cd3b1..96a6e4f26a 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -186,6 +186,9 @@ set(SLIC3R_SOURCES MultiMaterialSegmentation.hpp MeshNormals.hpp MeshNormals.cpp + Measure.hpp + Measure.cpp + MeasureUtils.hpp CustomGCode.cpp CustomGCode.hpp Arrange.hpp @@ -261,6 +264,7 @@ set(SLIC3R_SOURCES Surface.hpp SurfaceCollection.cpp SurfaceCollection.hpp + SurfaceMesh.hpp SVG.cpp SVG.hpp Technologies.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index e2bd6ef653..798f7759dd 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1502,27 +1502,27 @@ void GCode::process_layers( } }); const auto spiral_vase = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&spiral_vase = *this->m_spiral_vase](LayerResult in) -> LayerResult { + [spiral_vase = this->m_spiral_vase.get()](LayerResult in) -> LayerResult { if (in.nop_layer_result) return in; - spiral_vase.enable(in.spiral_vase_enable); - return { spiral_vase.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush}; + spiral_vase->enable(in.spiral_vase_enable); + return { spiral_vase->process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush}; }); const auto pressure_equalizer = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&pressure_equalizer = *this->m_pressure_equalizer](LayerResult in) -> LayerResult { - return pressure_equalizer.process_layer(std::move(in)); + [pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult { + return pressure_equalizer->process_layer(std::move(in)); }); const auto cooling = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&cooling_buffer = *this->m_cooling_buffer](LayerResult in) -> std::string { + [cooling_buffer = this->m_cooling_buffer.get()](LayerResult in) -> std::string { if (in.nop_layer_result) return in.gcode; - return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush); + return cooling_buffer->process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush); }); const auto find_replace = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&self = *this->m_find_replace](std::string s) -> std::string { - return self.process_layer(std::move(s)); + [find_replace = this->m_find_replace.get()](std::string s) -> std::string { + return find_replace->process_layer(std::move(s)); }); const auto output = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [&output_stream](std::string s) { output_stream.write(s); } @@ -1584,25 +1584,25 @@ void GCode::process_layers( } }); const auto spiral_vase = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&spiral_vase = *this->m_spiral_vase](LayerResult in)->LayerResult { + [spiral_vase = this->m_spiral_vase.get()](LayerResult in)->LayerResult { if (in.nop_layer_result) return in; - spiral_vase.enable(in.spiral_vase_enable); - return { spiral_vase.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; + spiral_vase->enable(in.spiral_vase_enable); + return { spiral_vase->process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; }); const auto pressure_equalizer = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&pressure_equalizer = *this->m_pressure_equalizer](LayerResult in) -> LayerResult { - return pressure_equalizer.process_layer(std::move(in)); + [pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult { + return pressure_equalizer->process_layer(std::move(in)); }); const auto cooling = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&cooling_buffer = *this->m_cooling_buffer](LayerResult in)->std::string { + [cooling_buffer = this->m_cooling_buffer.get()](LayerResult in)->std::string { if (in.nop_layer_result) return in.gcode; - return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush); + return cooling_buffer->process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush); }); const auto find_replace = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&self = *this->m_find_replace](std::string s) -> std::string { - return self.process_layer(std::move(s)); + [find_replace = this->m_find_replace.get()](std::string s) -> std::string { + return find_replace->process_layer(std::move(s)); }); const auto output = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [&output_stream](std::string s) { output_stream.write(s); } diff --git a/src/libslic3r/Measure.cpp b/src/libslic3r/Measure.cpp new file mode 100644 index 0000000000..fc27f0383e --- /dev/null +++ b/src/libslic3r/Measure.cpp @@ -0,0 +1,1190 @@ +#include "libslic3r/libslic3r.h" +#include "Measure.hpp" +#include "MeasureUtils.hpp" + +#include "libslic3r/Geometry/Circle.hpp" +#include "libslic3r/SurfaceMesh.hpp" + +namespace Slic3r { +namespace Measure { + + +constexpr double feature_hover_limit = 0.5; // how close to a feature the mouse must be to highlight it +constexpr double edge_endpoint_limit = 0.5; // how close to an edge endpoint the mouse ... + +static std::pair get_center_and_radius(const std::vector& border, int start_idx, int end_idx, const Transform3d& trafo) +{ + Vec2ds pts; + double z = 0.; + for (int i=start_idx; i<=end_idx; ++i) { + Vec3d pt_transformed = trafo * border[i]; + z = pt_transformed.z(); + pts.emplace_back(pt_transformed.x(), pt_transformed.y()); + } + + auto circle = Geometry::circle_ransac(pts, 20); // FIXME: iterations? + + return std::make_pair(trafo.inverse() * Vec3d(circle.center.x(), circle.center.y(), z), circle.radius); +} + + + + +class MeasuringImpl { +public: + explicit MeasuringImpl(const indexed_triangle_set& its); + struct PlaneData { + std::vector facets; + std::vector> borders; // FIXME: should be in fact local in update_planes() + std::vector surface_features; + Vec3d normal; + float area; + }; + + std::vector get_all_features() const; + std::optional get_feature(size_t face_idx, const Vec3d& point) const; + std::vector> get_planes_triangle_indices() const; + const std::vector& get_plane_features(unsigned int plane_id) const; + +private: + void update_planes(); + void extract_features(); + + std::vector m_planes; + std::vector m_face_to_plane; + const indexed_triangle_set& m_its; +}; + + + + + + +MeasuringImpl::MeasuringImpl(const indexed_triangle_set& its) +: m_its{its} +{ + update_planes(); + extract_features(); +} + + +void MeasuringImpl::update_planes() +{ + m_planes.clear(); + + // Now we'll go through all the facets and append Points of facets sharing the same normal. + // This part is still performed in mesh coordinate system. + const size_t num_of_facets = m_its.indices.size(); + m_face_to_plane.resize(num_of_facets, size_t(-1)); + const std::vector face_normals = its_face_normals(m_its); + const std::vector face_neighbors = its_face_neighbors(m_its); + std::vector facet_queue(num_of_facets, 0); + int facet_queue_cnt = 0; + const stl_normal* normal_ptr = nullptr; + size_t seed_facet_idx = 0; + + auto is_same_normal = [](const stl_normal& a, const stl_normal& b) -> bool { + return (std::abs(a(0) - b(0)) < 0.001 && std::abs(a(1) - b(1)) < 0.001 && std::abs(a(2) - b(2)) < 0.001); + }; + + // First go through all the triangles and fill in m_planes vector. For each "plane" + // detected on the model, it will contain list of facets that are part of it. + // We will also fill in m_face_to_plane, which contains index into m_planes + // for each of the source facets. + while (1) { + // Find next unvisited triangle: + for (; seed_facet_idx < num_of_facets; ++ seed_facet_idx) + if (m_face_to_plane[seed_facet_idx] == size_t(-1)) { + facet_queue[facet_queue_cnt ++] = seed_facet_idx; + normal_ptr = &face_normals[seed_facet_idx]; + m_face_to_plane[seed_facet_idx] = m_planes.size(); + m_planes.emplace_back(); + break; + } + if (seed_facet_idx == num_of_facets) + break; // Everything was visited already + + while (facet_queue_cnt > 0) { + int facet_idx = facet_queue[-- facet_queue_cnt]; + const stl_normal& this_normal = face_normals[facet_idx]; + if (is_same_normal(this_normal, *normal_ptr)) { +// const Vec3i& face = m_its.indices[facet_idx]; + + m_face_to_plane[facet_idx] = m_planes.size() - 1; + m_planes.back().facets.emplace_back(facet_idx); + for (int j = 0; j < 3; ++ j) + if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && m_face_to_plane[neighbor_idx] == size_t(-1)) + facet_queue[facet_queue_cnt ++] = neighbor_idx; + } + } + + m_planes.back().normal = normal_ptr->cast(); + std::sort(m_planes.back().facets.begin(), m_planes.back().facets.end()); + } + + // Check that each facet is part of one of the planes. + assert(std::none_of(m_face_to_plane.begin(), m_face_to_plane.end(), [](size_t val) { return val == size_t(-1); })); + + // Now we will walk around each of the planes and save vertices which form the border. + SurfaceMesh sm(m_its); + for (int plane_id=0; plane_id < int(m_planes.size()); ++plane_id) { + const auto& facets = m_planes[plane_id].facets; + m_planes[plane_id].borders.clear(); + std::vector> visited(facets.size(), {false, false, false}); + + + + for (int face_id=0; face_id& last_border = m_planes[plane_id].borders.back(); + last_border.emplace_back(sm.point(sm.source(he)).cast()); + //Vertex_index target = sm.target(he); + const Halfedge_index he_start = he; + + Face_index fi = he.face(); + auto face_it = std::lower_bound(facets.begin(), facets.end(), int(fi)); + assert(face_it != facets.end()); + assert(*face_it == int(fi)); + visited[face_it - facets.begin()][he.side()] = true; + + do { + const Halfedge_index he_orig = he; + he = sm.next_around_target(he); + if (he.is_invalid()) + goto PLANE_FAILURE; + while ( (int)m_face_to_plane[sm.face(he)] == plane_id && he != he_orig) { + he = sm.next_around_target(he); + if (he.is_invalid()) + goto PLANE_FAILURE; + } + he = sm.opposite(he); + if (he.is_invalid()) + goto PLANE_FAILURE; + + Face_index fi = he.face(); + auto face_it = std::lower_bound(facets.begin(), facets.end(), int(fi)); + if (face_it == facets.end() || *face_it != int(fi)) // This indicates a broken mesh. + goto PLANE_FAILURE; + + if (visited[face_it - facets.begin()][he.side()] && he != he_start) { + last_border.resize(1); + break; + } + visited[face_it - facets.begin()][he.side()] = true; + + last_border.emplace_back(sm.point(sm.source(he)).cast()); + } while (he != he_start); + + if (last_border.size() == 1) + m_planes[plane_id].borders.pop_back(); + else { + assert(last_border.front() == last_border.back()); + } + } + } + continue; // There was no failure. + + PLANE_FAILURE: + m_planes[plane_id].borders.clear(); + } +} + + + + + + +void MeasuringImpl::extract_features() +{ + std::vector angles; + std::vector lengths; + + + for (int i=0; i<(int)m_planes.size(); ++i) { + PlaneData& plane = m_planes[i]; + plane.surface_features.clear(); + const Vec3d& normal = plane.normal; + + Eigen::Quaterniond q; + q.setFromTwoVectors(plane.normal, Vec3d::UnitZ()); + Transform3d trafo = Transform3d::Identity(); + trafo.rotate(q); + + for (const std::vector& border : plane.borders) { + if (border.size() <= 1) + continue; + assert(border.front() == border.back()); + int start_idx = -1; + + std::vector edges; + + // First calculate angles at all the vertices. + angles.clear(); + lengths.clear(); + for (int i=0; i M_PI) + angle = 2*M_PI - angle; + + angles.push_back(angle); + lengths.push_back(v2.norm()); + } + assert(border.size() == angles.size()); + assert(border.size() == lengths.size()); + + + // First go around the border and pick what might be circular segments. + // Save pair of indices to where such potential segments start and end. + // Also remember the length of these segments. + bool circle = false; + std::vector circles; + std::vector> circles_idxs; + std::vector circles_lengths; + for (int i=1; i<(int)angles.size(); ++i) { + if (Slic3r::is_approx(lengths[i], lengths[i-1]) + && Slic3r::is_approx(angles[i], angles[i-1]) + && i != (int)angles.size()-1 ) { + // circle + if (! circle) { + circle = true; + start_idx = std::max(0, i-2); + } + } else { + if (circle) { + const auto& [center, radius] = get_center_and_radius(border, start_idx, i, trafo); + // Add the circle and remember indices into borders. + circles_idxs.emplace_back(start_idx, i); + circles.emplace_back(SurfaceFeature(SurfaceFeatureType::Circle, center, plane.normal, std::nullopt, radius)); + circles_lengths.emplace_back(std::accumulate(lengths.begin() + start_idx + 1, lengths.begin() + i + 1, 0.)); + circle = false; + } + } + } + + // At this point we might need to merge the first and last segment, if the starting + // point happened to be inside the segment. The discrimination of too small segments + // will follow, so we need a complete picture before that. + if (circles_idxs.size() > 1 + && circles_idxs.back().second == angles.size()-1 + && circles_idxs.front().first == 0) { + // Possibly the same circle. Check that the angle and length criterion holds along the combined segment. + bool same = true; + double last_len = -1.; + double last_angle = 0.; + for (int i=circles_idxs.back().first + 1; i != circles_idxs.front().second; ++i) { + if (i == angles.size()) + i = 1; + if (last_len == -1.) { + last_len = lengths[i]; + last_angle = angles[i]; + } else { + if (! Slic3r::is_approx(lengths[i], last_len) || ! Slic3r::is_approx(angles[i], last_angle)) { + same = false; + break; + } + } + } + if (same) { + // This seems to really be the same circle. Better apply ransac again. The parts can be small and inexact. + std::vector points(border.begin() + circles_idxs.back().first, border.end()); + points.insert(points.end(), border.begin(), border.begin() + circles_idxs.front().second+1); + auto [c, radius] = get_center_and_radius(points, 0, points.size()-1, trafo); + + // Now replace the first circle with the combined one, remove the last circle. + // First index of the first circle is saved negative - we are going to pick edges + // from the border later, we will need to know where the merged in segment was. + // The sign simplifies the algorithm that picks the remaining edges - see below. + circles.front() = SurfaceFeature(SurfaceFeatureType::Circle, c, plane.normal, std::nullopt, radius); + circles_idxs.front().first = - circles_idxs.back().first; + circles_lengths.front() += circles_lengths.back(); + circles.pop_back(); + circles_idxs.pop_back(); + circles_lengths.pop_back(); + } + } + + // Now throw away all circles that subtend less than 90 deg. + assert(circles.size() == circles_lengths.size()); + for (int i=0; i(circles[i].get_circle()); + if (circles_lengths[i] / r < 0.9*M_PI/2.) { + circles_lengths.erase(circles_lengths.begin() + i); + circles.erase(circles.begin() + i); + circles_idxs.erase(circles_idxs.begin() + i); + --i; + } + } + circles_lengths.clear(); // no longer needed, make it obvious + + // Some of the "circles" may actually be polygons (5-8 vertices). We want them + // detected as edges, but also to remember the center and save it into those edges. + // We will add all such edges manually and delete the detected circles, leaving it + // in circles_idxs so they are not picked again. + assert(circles.size() == circles_idxs.size()); + for (int i=circles.size()-1; i>=0; --i) { + if (circles_idxs[i].first == 0 && circles_idxs[i].second == border.size()-1) { + int N = circles_idxs[i].second - circles_idxs[i].first; + if (N <= 8) { + if (N >= 5) { // polygon = 5,6,7,8 vertices + const Vec3d center = std::get<0>(circles[i].get_circle()); + for (int j=(int)circles_idxs[i].first + 1; j<=(int)circles_idxs[i].second; ++j) + edges.emplace_back(SurfaceFeature(SurfaceFeatureType::Edge, + border[j - 1], border[j], std::make_optional(center))); + } else { + // This will be handled just like a regular edge (squares, triangles). + circles_idxs.erase(circles_idxs.begin() + i); + } + circles.erase(circles.begin() + i); + } + } + } + + // Anything under 5 vertices shall not be considered a circle. + assert(circles_idxs.size() == circles.size()); + for (int i=0; i= 0 + ? end - start + (start == 0 && end == border.size()-1 ? 0 : 1) // last point is the same as first + : end + (border.size() + start); + if (N < 5) { + circles.erase(circles.begin() + i); + circles_idxs.erase(circles_idxs.begin() + i); + --i; + } + } + + + // We have the circles. Now go around again and pick edges, while jumping over circles. + // If the first index of the first circle is negative, it means that it was merged + // with a segment that was originally at the back and is no longer there. Ressurect + // its pair of indices so that edges are not picked again. + if (! circles_idxs.empty() && circles_idxs.front().first < 0) + circles_idxs.emplace_back(-circles_idxs.front().first, int(border.size())); + int cidx = 0; // index of next circle to jump over + for (int i=1; i (int)circles_idxs[cidx].first) + i = circles_idxs[cidx++].second; + else + edges.emplace_back(SurfaceFeature(SurfaceFeatureType::Edge, border[i - 1], border[i])); + } + + // Merge adjacent edges where needed. + assert(std::all_of(edges.begin(), edges.end(), + [](const SurfaceFeature& f) { return f.get_type() == SurfaceFeatureType::Edge; })); + for (int i=edges.size()-1; i>=0; --i) { + const auto& [first_start, first_end] = edges[i==0 ? edges.size()-1 : i-1].get_edge(); + const auto& [second_start, second_end] = edges[i].get_edge(); + + if (Slic3r::is_approx(first_end, second_start) + && Slic3r::is_approx((first_end-first_start).normalized().dot((second_end-second_start).normalized()), 1.)) { + // The edges have the same direction and share a point. Merge them. + edges[i==0 ? edges.size()-1 : i-1] = SurfaceFeature(SurfaceFeatureType::Edge, first_start, second_end); + edges.erase(edges.begin() + i); + } + } + + // Now move the circles and edges into the feature list for the plane. + assert(std::all_of(circles.begin(), circles.end(), [](const SurfaceFeature& f) { + return f.get_type() == SurfaceFeatureType::Circle; + })); + assert(std::all_of(edges.begin(), edges.end(), [](const SurfaceFeature& f) { + return f.get_type() == SurfaceFeatureType::Edge; + })); + plane.surface_features.insert(plane.surface_features.end(), std::make_move_iterator(circles.begin()), + std::make_move_iterator(circles.end())); + plane.surface_features.insert(plane.surface_features.end(), std::make_move_iterator(edges.begin()), + std::make_move_iterator(edges.end())); + } + + // The last surface feature is the plane itself. + Vec3d cog = Vec3d::Zero(); + size_t counter = 0; + for (const std::vector& b : plane.borders) { + for (size_t i = 1; i < b.size(); ++i) { + cog += b[i]; + ++counter; + } + } + cog /= double(counter); + plane.surface_features.emplace_back(SurfaceFeature(SurfaceFeatureType::Plane, + plane.normal, cog, std::optional(), i + 0.0001)); + + plane.borders.clear(); + plane.borders.shrink_to_fit(); + } +} + + + +std::vector MeasuringImpl::get_all_features() const +{ + std::vector features; + //PlaneData& plane = m_planes[0]; + for (const PlaneData& plane : m_planes) + for (const SurfaceFeature& feature : plane.surface_features) + features.emplace_back(feature); + return features; +} + + + + + + +std::optional MeasuringImpl::get_feature(size_t face_idx, const Vec3d& point) const +{ + if (face_idx >= m_face_to_plane.size()) + return std::optional(); + + const PlaneData& plane = m_planes[m_face_to_plane[face_idx]]; + + size_t closest_feature_idx = size_t(-1); + double min_dist = std::numeric_limits::max(); + + MeasurementResult res; + SurfaceFeature point_sf(point); + + for (size_t i=0; idist; + if (dist < feature_hover_limit && dist < min_dist) { + min_dist = std::min(dist, min_dist); + closest_feature_idx = i; + } + } + } + + if (closest_feature_idx != size_t(-1)) { + const SurfaceFeature& f = plane.surface_features[closest_feature_idx]; + if (f.get_type() == SurfaceFeatureType::Edge) { + // If this is an edge, check if we are not close to the endpoint. If so, + // we will include the endpoint as well. + constexpr double limit_sq = edge_endpoint_limit * edge_endpoint_limit; + const auto& [sp, ep] = f.get_edge(); + if ((point-sp).squaredNorm() < limit_sq) + return std::make_optional(SurfaceFeature(sp)); + if ((point-ep).squaredNorm() < limit_sq) + return std::make_optional(SurfaceFeature(ep)); + } + return std::make_optional(f); + } + + // Nothing detected, return the plane as a whole. + assert(plane.surface_features.back().get_type() == SurfaceFeatureType::Plane); + return std::make_optional(plane.surface_features.back()); +} + + + + + +std::vector> MeasuringImpl::get_planes_triangle_indices() const +{ + std::vector> out; + for (const PlaneData& plane : m_planes) + out.emplace_back(plane.facets); + return out; +} + +const std::vector& MeasuringImpl::get_plane_features(unsigned int plane_id) const +{ + assert(plane_id < m_planes.size()); + return m_planes[plane_id].surface_features; +} + + + + + + + + + + + + +Measuring::Measuring(const indexed_triangle_set& its) +: priv{std::make_unique(its)} +{} + +Measuring::~Measuring() {} + + +std::vector Measuring::get_all_features() const +{ + return priv->get_all_features(); +} + + +std::optional Measuring::get_feature(size_t face_idx, const Vec3d& point) const +{ + return priv->get_feature(face_idx, point); +} + + + +std::vector> Measuring::get_planes_triangle_indices() const +{ + return priv->get_planes_triangle_indices(); +} + +const std::vector& Measuring::get_plane_features(unsigned int plane_id) const +{ + return priv->get_plane_features(plane_id); +} + +const AngleAndEdges AngleAndEdges::Dummy = { 0.0, Vec3d::Zero(), { Vec3d::Zero(), Vec3d::Zero() }, { Vec3d::Zero(), Vec3d::Zero() }, 0.0, true }; + +static AngleAndEdges angle_edge_edge(const std::pair& e1, const std::pair& e2) +{ + if (are_parallel(e1, e2)) + return AngleAndEdges::Dummy; + + Vec3d e1_unit = edge_direction(e1.first, e1.second); + Vec3d e2_unit = edge_direction(e2.first, e2.second); + + // project edges on the plane defined by them + Vec3d normal = e1_unit.cross(e2_unit).normalized(); + const Eigen::Hyperplane plane(normal, e1.first); + Vec3d e11_proj = plane.projection(e1.first); + Vec3d e12_proj = plane.projection(e1.second); + Vec3d e21_proj = plane.projection(e2.first); + Vec3d e22_proj = plane.projection(e2.second); + + const bool coplanar = (e2.first - e21_proj).norm() < EPSILON && (e2.second - e22_proj).norm() < EPSILON; + + // rotate the plane to become the XY plane + auto qp = Eigen::Quaternion::FromTwoVectors(normal, Vec3d::UnitZ()); + auto qp_inverse = qp.inverse(); + const Vec3d e11_rot = qp * e11_proj; + const Vec3d e12_rot = qp * e12_proj; + const Vec3d e21_rot = qp * e21_proj; + const Vec3d e22_rot = qp * e22_proj; + + // discard Z + const Vec2d e11_rot_2d = Vec2d(e11_rot.x(), e11_rot.y()); + const Vec2d e12_rot_2d = Vec2d(e12_rot.x(), e12_rot.y()); + const Vec2d e21_rot_2d = Vec2d(e21_rot.x(), e21_rot.y()); + const Vec2d e22_rot_2d = Vec2d(e22_rot.x(), e22_rot.y()); + + // find intersection (arc center) of edges in XY plane + const Eigen::Hyperplane e1_rot_2d_line = Eigen::Hyperplane::Through(e11_rot_2d, e12_rot_2d); + const Eigen::Hyperplane e2_rot_2d_line = Eigen::Hyperplane::Through(e21_rot_2d, e22_rot_2d); + const Vec2d center_rot_2d = e1_rot_2d_line.intersection(e2_rot_2d_line); + + // arc center in original coordinate + const Vec3d center = qp_inverse * Vec3d(center_rot_2d.x(), center_rot_2d.y(), e11_rot.z()); + + // ensure the edges are pointing away from the center + std::pair out_e1 = e1; + std::pair out_e2 = e2; + if ((center_rot_2d - e11_rot_2d).squaredNorm() > (center_rot_2d - e12_rot_2d).squaredNorm()) { + std::swap(e11_proj, e12_proj); + std::swap(out_e1.first, out_e1.second); + e1_unit = -e1_unit; + } + if ((center_rot_2d - e21_rot_2d).squaredNorm() > (center_rot_2d - e22_rot_2d).squaredNorm()) { + std::swap(e21_proj, e22_proj); + std::swap(out_e2.first, out_e2.second); + e2_unit = -e2_unit; + } + + // arc angle + const double angle = std::acos(std::clamp(e1_unit.dot(e2_unit), -1.0, 1.0)); + // arc radius + const Vec3d e1_proj_mid = 0.5 * (e11_proj + e12_proj); + const Vec3d e2_proj_mid = 0.5 * (e21_proj + e22_proj); + const double radius = std::min((center - e1_proj_mid).norm(), (center - e2_proj_mid).norm()); + + return { angle, center, out_e1, out_e2, radius, coplanar }; +} + +static AngleAndEdges angle_edge_plane(const std::pair& e, const std::tuple& p) +{ + const auto& [idx, normal, origin] = p; + const Vec3d e1e2_unit = edge_direction(e); + if (are_parallel(e1e2_unit, normal) || are_perpendicular(e1e2_unit, normal)) + return AngleAndEdges::Dummy; + + // ensure the edge is pointing away from the intersection + // 1st calculate instersection between edge and plane + const Eigen::Hyperplane plane(normal, origin); + const Eigen::ParametrizedLine line = Eigen::ParametrizedLine::Through(e.first, e.second); + const Vec3d inters = line.intersectionPoint(plane); + + // then verify edge direction and revert it, if needed + Vec3d e1 = e.first; + Vec3d e2 = e.second; + if ((e1 - inters).squaredNorm() > (e2 - inters).squaredNorm()) + std::swap(e1, e2); + + const Vec3d e1e2 = e2 - e1; + const double e1e2_len = e1e2.norm(); + + // calculate 2nd edge (on the plane) + const Vec3d temp = normal.cross(e1e2); + const Vec3d edge_on_plane_unit = normal.cross(temp).normalized(); + std::pair edge_on_plane = { origin, origin + e1e2_len * edge_on_plane_unit }; + + // ensure the 2nd edge is pointing in the correct direction + const Vec3d test_edge = (edge_on_plane.second - edge_on_plane.first).cross(e1e2); + if (test_edge.dot(temp) < 0.0) + edge_on_plane = { origin, origin - e1e2_len * edge_on_plane_unit }; + + AngleAndEdges ret = angle_edge_edge({ e1, e2 }, edge_on_plane); + ret.radius = (inters - 0.5 * (e1 + e2)).norm(); + return ret; +} + +static AngleAndEdges angle_plane_plane(const std::tuple& p1, const std::tuple& p2) +{ + const auto& [idx1, normal1, origin1] = p1; + const auto& [idx2, normal2, origin2] = p2; + + // are planes parallel ? + if (are_parallel(normal1, normal2)) + return AngleAndEdges::Dummy; + + auto intersection_plane_plane = [](const Vec3d& n1, const Vec3d& o1, const Vec3d& n2, const Vec3d& o2) { + Eigen::MatrixXd m(2, 3); + m << n1.x(), n1.y(), n1.z(), n2.x(), n2.y(), n2.z(); + Eigen::VectorXd b(2); + b << o1.dot(n1), o2.dot(n2); + Eigen::VectorXd x = m.colPivHouseholderQr().solve(b); + return std::make_pair(n1.cross(n2).normalized(), Vec3d(x(0), x(1), x(2))); + }; + + // Calculate intersection line between planes + const auto [intersection_line_direction, intersection_line_origin] = intersection_plane_plane(normal1, origin1, normal2, origin2); + + // Project planes' origin on intersection line + const Eigen::ParametrizedLine intersection_line = Eigen::ParametrizedLine(intersection_line_origin, intersection_line_direction); + const Vec3d origin1_proj = intersection_line.projection(origin1); + const Vec3d origin2_proj = intersection_line.projection(origin2); + + // Calculate edges on planes + const Vec3d edge_on_plane1_unit = (origin1 - origin1_proj).normalized(); + const Vec3d edge_on_plane2_unit = (origin2 - origin2_proj).normalized(); + const double radius = std::max(10.0, std::max((origin1 - origin1_proj).norm(), (origin2 - origin2_proj).norm())); + const std::pair edge_on_plane1 = { origin1_proj + radius * edge_on_plane1_unit, origin1_proj + 2.0 * radius * edge_on_plane1_unit }; + const std::pair edge_on_plane2 = { origin2_proj + radius * edge_on_plane2_unit, origin2_proj + 2.0 * radius * edge_on_plane2_unit }; + + AngleAndEdges ret = angle_edge_edge(edge_on_plane1, edge_on_plane2); + ret.radius = radius; + return ret; +} + + + + + + + +MeasurementResult get_measurement(const SurfaceFeature& a, const SurfaceFeature& b, const Measuring* measuring) +{ + assert(a.get_type() != SurfaceFeatureType::Undef && b.get_type() != SurfaceFeatureType::Undef); + + const bool swap = int(a.get_type()) > int(b.get_type()); + const SurfaceFeature& f1 = swap ? b : a; + const SurfaceFeature& f2 = swap ? a : b; + + MeasurementResult result; + + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + if (f1.get_type() == SurfaceFeatureType::Point) { + if (f2.get_type() == SurfaceFeatureType::Point) { + Vec3d diff = (f2.get_point() - f1.get_point()); + result.distance_strict = std::make_optional(DistAndPoints{diff.norm(), f1.get_point(), f2.get_point()}); + result.distance_xyz = diff.cwiseAbs(); + + /////////////////////////////////////////////////////////////////////////// + } else if (f2.get_type() == SurfaceFeatureType::Edge) { + const auto [s,e] = f2.get_edge(); + const Eigen::ParametrizedLine line(s, (e-s).normalized()); + const double dist_inf = line.distance(f1.get_point()); + const Vec3d proj = line.projection(f1.get_point()); + const double len_sq = (e-s).squaredNorm(); + const double dist_start_sq = (proj-s).squaredNorm(); + const double dist_end_sq = (proj-e).squaredNorm(); + if (dist_start_sq < len_sq && dist_end_sq < len_sq) { + // projection falls on the line - the strict distance is the same as infinite + result.distance_strict = std::make_optional(DistAndPoints{dist_inf, f1.get_point(), proj}); + } else { // the result is the closer of the endpoints + const bool s_is_closer = dist_start_sq < dist_end_sq; + result.distance_strict = std::make_optional(DistAndPoints{std::sqrt(std::min(dist_start_sq, dist_end_sq) + sqr(dist_inf)), f1.get_point(), s_is_closer ? s : e}); + } + result.distance_infinite = std::make_optional(DistAndPoints{dist_inf, f1.get_point(), proj}); + /////////////////////////////////////////////////////////////////////////// + } else if (f2.get_type() == SurfaceFeatureType::Circle) { + // Find a plane containing normal, center and the point. + const auto [c, radius, n] = f2.get_circle(); + const Eigen::Hyperplane circle_plane(n, c); + const Vec3d proj = circle_plane.projection(f1.get_point()); + if (proj.isApprox(c)) { + const Vec3d p_on_circle = c + radius * get_orthogonal(n, true); + result.distance_strict = std::make_optional(DistAndPoints{ radius, c, p_on_circle }); + } + else { + const Eigen::Hyperplane circle_plane(n, c); + const Vec3d proj = circle_plane.projection(f1.get_point()); + const double dist = std::sqrt(std::pow((proj - c).norm() - radius, 2.) + + (f1.get_point() - proj).squaredNorm()); + + const Vec3d p_on_circle = c + radius * (proj - c).normalized(); + result.distance_strict = std::make_optional(DistAndPoints{ dist, f1.get_point(), p_on_circle }); // TODO + } + /////////////////////////////////////////////////////////////////////////// + } else if (f2.get_type() == SurfaceFeatureType::Plane) { + const auto [idx, normal, pt] = f2.get_plane(); + Eigen::Hyperplane plane(normal, pt); + result.distance_infinite = std::make_optional(DistAndPoints{plane.absDistance(f1.get_point()), f1.get_point(), plane.projection(f1.get_point())}); // TODO + // TODO: result.distance_strict = + } + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + } else if (f1.get_type() == SurfaceFeatureType::Edge) { + if (f2.get_type() == SurfaceFeatureType::Edge) { + std::vector distances; + + auto add_point_edge_distance = [&distances](const Vec3d& v, const std::pair& e) { + const MeasurementResult res = get_measurement(SurfaceFeature(v), SurfaceFeature(SurfaceFeatureType::Edge, e.first, e.second)); + double distance = res.distance_strict->dist; + Vec3d v2 = res.distance_strict->to; + + const Vec3d e1e2 = e.second - e.first; + const Vec3d e1v2 = v2 - e.first; + if (e1v2.dot(e1e2) >= 0.0 && e1v2.norm() < e1e2.norm()) + distances.emplace_back(distance, v, v2); + }; + + std::pair e1 = f1.get_edge(); + std::pair e2 = f2.get_edge(); + + distances.emplace_back((e2.first - e1.first).norm(), e1.first, e2.first); + distances.emplace_back((e2.second - e1.first).norm(), e1.first, e2.second); + distances.emplace_back((e2.first - e1.second).norm(), e1.second, e2.first); + distances.emplace_back((e2.second - e1.second).norm(), e1.second, e2.second); + add_point_edge_distance(e1.first, e2); + add_point_edge_distance(e1.second, e2); + add_point_edge_distance(e2.first, e1); + add_point_edge_distance(e2.second, e1); + auto it = std::min_element(distances.begin(), distances.end(), + [](const DistAndPoints& item1, const DistAndPoints& item2) { + return item1.dist < item2.dist; + }); + result.distance_infinite = std::make_optional(*it); + + result.angle = angle_edge_edge(f1.get_edge(), f2.get_edge()); + /////////////////////////////////////////////////////////////////////////// + } else if (f2.get_type() == SurfaceFeatureType::Circle) { + const std::pair e = f1.get_edge(); + const auto& [center, radius, normal] = f2.get_circle(); + const Vec3d e1e2 = (e.second - e.first); + const Vec3d e1e2_unit = e1e2.normalized(); + + std::vector distances; + distances.emplace_back(*get_measurement(SurfaceFeature(e.first), f2).distance_strict); + distances.emplace_back(*get_measurement(SurfaceFeature(e.second), f2).distance_strict); + + const Eigen::Hyperplane plane(e1e2_unit, center); + const Eigen::ParametrizedLine line = Eigen::ParametrizedLine::Through(e.first, e.second); + const Vec3d inter = line.intersectionPoint(plane); + const Vec3d e1inter = inter - e.first; + if (e1inter.dot(e1e2) >= 0.0 && e1inter.norm() < e1e2.norm()) + distances.emplace_back(*get_measurement(SurfaceFeature(inter), f2).distance_strict); + + auto it = std::min_element(distances.begin(), distances.end(), + [](const DistAndPoints& item1, const DistAndPoints& item2) { + return item1.dist < item2.dist; + }); + result.distance_infinite = std::make_optional(DistAndPoints{it->dist, it->from, it->to}); + /////////////////////////////////////////////////////////////////////////// + } else if (f2.get_type() == SurfaceFeatureType::Plane) { + assert(measuring != nullptr); + + const auto [from, to] = f1.get_edge(); + const auto [idx, normal, origin] = f2.get_plane(); + + const Vec3d edge_unit = (to - from).normalized(); + if (are_perpendicular(edge_unit, normal)) { + std::vector distances; + const Eigen::Hyperplane plane(normal, origin); + distances.push_back(DistAndPoints{ plane.absDistance(from), from, plane.projection(from) }); + distances.push_back(DistAndPoints{ plane.absDistance(to), to, plane.projection(to) }); + auto it = std::min_element(distances.begin(), distances.end(), + [](const DistAndPoints& item1, const DistAndPoints& item2) { + return item1.dist < item2.dist; + }); + result.distance_infinite = std::make_optional(DistAndPoints{ it->dist, it->from, it->to }); + } + else { + const std::vector& plane_features = measuring->get_plane_features(idx); + std::vector distances; + for (const SurfaceFeature& sf : plane_features) { + if (sf.get_type() == SurfaceFeatureType::Edge) { + const auto m = get_measurement(sf, f1); + if (!m.distance_infinite.has_value()) { + distances.clear(); + break; + } + else + distances.push_back(*m.distance_infinite); + } + } + if (!distances.empty()) { + auto it = std::min_element(distances.begin(), distances.end(), + [](const DistAndPoints& item1, const DistAndPoints& item2) { + return item1.dist < item2.dist; + }); + result.distance_infinite = std::make_optional(DistAndPoints{ it->dist, it->from, it->to }); + } + } + result.angle = angle_edge_plane(f1.get_edge(), f2.get_plane()); + } + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + } else if (f1.get_type() == SurfaceFeatureType::Circle) { + if (f2.get_type() == SurfaceFeatureType::Circle) { + const auto [c0, r0, n0] = f1.get_circle(); + const auto [c1, r1, n1] = f2.get_circle(); + + // The following code is an adaptation of the algorithm found in: + // https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/DistCircle3Circle3.h + // and described in: + // https://www.geometrictools.com/Documentation/DistanceToCircle3.pdf + + struct ClosestInfo + { + double sqrDistance{ 0.0 }; + Vec3d circle0Closest{ Vec3d::Zero() }; + Vec3d circle1Closest{ Vec3d::Zero() }; + + inline bool operator < (const ClosestInfo& other) const { return sqrDistance < other.sqrDistance; } + }; + std::array candidates{}; + + const double zero = 0.0; + + const Vec3d D = c1 - c0; + + if (!are_parallel(n0, n1)) { + auto orthonormal_basis = [](const Vec3d& v) { + std::array ret; + ret[2] = v.normalized(); + int index; + ret[2].maxCoeff(&index); + switch (index) + { + case 0: { ret[0] = Vec3d(ret[2].y(), -ret[2].x(), 0.0).normalized(); break; } + case 1: { ret[0] = Vec3d(0.0, ret[2].z(), -ret[2].y()).normalized(); break; } + case 2: { ret[0] = Vec3d(-ret[2].z(), 0.0, ret[2].x()).normalized(); break; } + } + ret[1] = ret[2].cross(ret[0]).normalized(); + return ret; + }; + + // Get parameters for constructing the degree-8 polynomial phi. + const double one = 1.0; + const double two = 2.0; + const double r0sqr = sqr(r0); + const double r1sqr = sqr(r1); + + // Compute U1 and V1 for the plane of circle1. + const std::array basis = orthonormal_basis(n1); + const Vec3d U1 = basis[0]; + const Vec3d V1 = basis[1]; + + // Construct the polynomial phi(cos(theta)). + const Vec3d N0xD = n0.cross(D); + const Vec3d N0xU1 = n0.cross(U1); + const Vec3d N0xV1 = n0.cross(V1); + const double a0 = r1 * D.dot(U1); + const double a1 = r1 * D.dot(V1); + const double a2 = N0xD.dot(N0xD); + const double a3 = r1 * N0xD.dot(N0xU1); + const double a4 = r1 * N0xD.dot(N0xV1); + const double a5 = r1sqr * N0xU1.dot(N0xU1); + const double a6 = r1sqr * N0xU1.dot(N0xV1); + const double a7 = r1sqr * N0xV1.dot(N0xV1); + Polynomial1 p0{ a2 + a7, two * a3, a5 - a7 }; + Polynomial1 p1{ two * a4, two * a6 }; + Polynomial1 p2{ zero, a1 }; + Polynomial1 p3{ -a0 }; + Polynomial1 p4{ -a6, a4, two * a6 }; + Polynomial1 p5{ -a3, a7 - a5 }; + Polynomial1 tmp0{ one, zero, -one }; + Polynomial1 tmp1 = p2 * p2 + tmp0 * p3 * p3; + Polynomial1 tmp2 = two * p2 * p3; + Polynomial1 tmp3 = p4 * p4 + tmp0 * p5 * p5; + Polynomial1 tmp4 = two * p4 * p5; + Polynomial1 p6 = p0 * tmp1 + tmp0 * p1 * tmp2 - r0sqr * tmp3; + Polynomial1 p7 = p0 * tmp2 + p1 * tmp1 - r0sqr * tmp4; + + // Parameters for polynomial root finding. The roots[] array + // stores the roots. We need only the unique ones, which is + // the responsibility of the set uniqueRoots. The pairs[] + // array stores the (cosine,sine) information mentioned in the + // PDF. TODO: Choose the maximum number of iterations for root + // finding based on specific polynomial data? + const uint32_t maxIterations = 128; + int32_t degree = 0; + size_t numRoots = 0; + std::array roots{}; + std::set uniqueRoots{}; + size_t numPairs = 0; + std::array, 16> pairs{}; + double temp = zero; + double sn = zero; + + if (p7.GetDegree() > 0 || p7[0] != zero) { + // H(cs,sn) = p6(cs) + sn * p7(cs) + Polynomial1 phi = p6 * p6 - tmp0 * p7 * p7; + degree = static_cast(phi.GetDegree()); + assert(degree > 0); + numRoots = RootsPolynomial::Find(degree, &phi[0], maxIterations, roots.data()); + for (size_t i = 0; i < numRoots; ++i) { + uniqueRoots.insert(roots[i]); + } + + for (auto const& cs : uniqueRoots) { + if (std::fabs(cs) <= one) { + temp = p7(cs); + if (temp != zero) { + sn = -p6(cs) / temp; + pairs[numPairs++] = std::make_pair(cs, sn); + } + else { + temp = std::max(one - sqr(cs), zero); + sn = std::sqrt(temp); + pairs[numPairs++] = std::make_pair(cs, sn); + if (sn != zero) + pairs[numPairs++] = std::make_pair(cs, -sn); + } + } + } + } + else { + // H(cs,sn) = p6(cs) + degree = static_cast(p6.GetDegree()); + assert(degree > 0); + numRoots = RootsPolynomial::Find(degree, &p6[0], maxIterations, roots.data()); + for (size_t i = 0; i < numRoots; ++i) { + uniqueRoots.insert(roots[i]); + } + + for (auto const& cs : uniqueRoots) { + if (std::fabs(cs) <= one) { + temp = std::max(one - sqr(cs), zero); + sn = std::sqrt(temp); + pairs[numPairs++] = std::make_pair(cs, sn); + if (sn != zero) + pairs[numPairs++] = std::make_pair(cs, -sn); + } + } + } + + for (size_t i = 0; i < numPairs; ++i) { + ClosestInfo& info = candidates[i]; + Vec3d delta = D + r1 * (pairs[i].first * U1 + pairs[i].second * V1); + info.circle1Closest = c0 + delta; + const double N0dDelta = n0.dot(delta); + const double lenN0xDelta = n0.cross(delta).norm(); + if (lenN0xDelta > 0.0) { + const double diff = lenN0xDelta - r0; + info.sqrDistance = sqr(N0dDelta) + sqr(diff); + delta -= N0dDelta * n0; + delta.normalize(); + info.circle0Closest = c0 + r0 * delta; + } + else { + const Vec3d r0U0 = r0 * get_orthogonal(n0, true); + const Vec3d diff = delta - r0U0; + info.sqrDistance = diff.dot(diff); + info.circle0Closest = c0 + r0U0; + } + } + + std::sort(candidates.begin(), candidates.begin() + numPairs); + } + else { + ClosestInfo& info = candidates[0]; + + const double N0dD = n0.dot(D); + const Vec3d normProj = N0dD * n0; + const Vec3d compProj = D - normProj; + Vec3d U = compProj; + const double d = U.norm(); + U.normalize(); + + // The configuration is determined by the relative location of the + // intervals of projection of the circles on to the D-line. + // Circle0 projects to [-r0,r0] and circle1 projects to + // [d-r1,d+r1]. + const double dmr1 = d - r1; + double distance; + if (dmr1 >= r0) { + // d >= r0 + r1 + // The circles are separated (d > r0 + r1) or tangent with one + // outside the other (d = r0 + r1). + distance = dmr1 - r0; + info.circle0Closest = c0 + r0 * U; + info.circle1Closest = c1 - r1 * U; + } + else { + // d < r0 + r1 + // The cases implicitly use the knowledge that d >= 0. + const double dpr1 = d + r1; + if (dpr1 <= r0) { + // Circle1 is inside circle0. + distance = r0 - dpr1; + if (d > 0.0) { + info.circle0Closest = c0 + r0 * U; + info.circle1Closest = c1 + r1 * U; + } + else { + // The circles are concentric, so U = (0,0,0). + // Construct a vector perpendicular to N0 to use for + // closest points. + U = get_orthogonal(n0, true); + info.circle0Closest = c0 + r0 * U; + info.circle1Closest = c1 + r1 * U; + } + } + else if (dmr1 <= -r0) { + // Circle0 is inside circle1. + distance = -r0 - dmr1; + if (d > 0.0) { + info.circle0Closest = c0 - r0 * U; + info.circle1Closest = c1 - r1 * U; + } + else { + // The circles are concentric, so U = (0,0,0). + // Construct a vector perpendicular to N0 to use for + // closest points. + U = get_orthogonal(n0, true); + info.circle0Closest = c0 + r0 * U; + info.circle1Closest = c1 + r1 * U; + } + } + else { + distance = (c1 - c0).norm(); + info.circle0Closest = c0; + info.circle1Closest = c1; + } + } + + info.sqrDistance = distance * distance + N0dD * N0dD; + } + + result.distance_infinite = std::make_optional(DistAndPoints{ std::sqrt(candidates[0].sqrDistance), candidates[0].circle0Closest, candidates[0].circle1Closest }); // TODO + /////////////////////////////////////////////////////////////////////////// + } else if (f2.get_type() == SurfaceFeatureType::Plane) { + assert(measuring != nullptr); + + const auto [center, radius, normal1] = f1.get_circle(); + const auto [idx2, normal2, origin2] = f2.get_plane(); + + const bool coplanar = are_parallel(normal1, normal2) && Eigen::Hyperplane(normal1, center).absDistance(origin2) < EPSILON; + if (!coplanar) { + const std::vector& plane_features = measuring->get_plane_features(idx2); + std::vector distances; + for (const SurfaceFeature& sf : plane_features) { + if (sf.get_type() == SurfaceFeatureType::Edge) { + const auto m = get_measurement(sf, f1); + if (!m.distance_infinite.has_value()) { + distances.clear(); + break; + } + else + distances.push_back(*m.distance_infinite); + } + } + if (!distances.empty()) { + auto it = std::min_element(distances.begin(), distances.end(), + [](const DistAndPoints& item1, const DistAndPoints& item2) { + return item1.dist < item2.dist; + }); + result.distance_infinite = std::make_optional(DistAndPoints{ it->dist, it->from, it->to }); + } + } + } + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + } else if (f1.get_type() == SurfaceFeatureType::Plane) { + const auto [idx1, normal1, pt1] = f1.get_plane(); + const auto [idx2, normal2, pt2] = f2.get_plane(); + + if (are_parallel(normal1, normal2)) { + // The planes are parallel, calculate distance. + const Eigen::Hyperplane plane(normal1, pt1); + result.distance_infinite = std::make_optional(DistAndPoints{ plane.absDistance(pt2), pt2, plane.projection(pt2) }); // TODO + } + else + result.angle = angle_plane_plane(f1.get_plane(), f2.get_plane()); + } + + return result; +} + +void DistAndPoints::transform(const Transform3d& trafo) { + from = trafo * from; + to = trafo * to; + dist = (to - from).norm(); +} + +void AngleAndEdges::transform(const Transform3d& trafo) { + const Vec3d old_e1 = e1.second - e1.first; + const Vec3d old_e2 = e2.second - e2.first; + center = trafo * center; + e1.first = trafo * e1.first; + e1.second = trafo * e1.second; + e2.first = trafo * e2.first; + e2.second = trafo * e2.second; + angle = std::acos(std::clamp(Measure::edge_direction(e1).dot(Measure::edge_direction(e2)), -1.0, 1.0)); + const Vec3d new_e1 = e1.second - e1.first; + const Vec3d new_e2 = e2.second - e2.first; + const double average_scale = 0.5 * (new_e1.norm() / old_e1.norm() + new_e2.norm() / old_e2.norm()); + radius = average_scale * radius; +} + + + + + + + + +} // namespace Measure +} // namespace Slic3r + diff --git a/src/libslic3r/Measure.hpp b/src/libslic3r/Measure.hpp new file mode 100644 index 0000000000..ede8c634ee --- /dev/null +++ b/src/libslic3r/Measure.hpp @@ -0,0 +1,208 @@ +#ifndef Slic3r_Measure_hpp_ +#define Slic3r_Measure_hpp_ + +#include +#include + +#include "Point.hpp" + + +struct indexed_triangle_set; + + + +namespace Slic3r { +namespace Measure { + + +enum class SurfaceFeatureType : int { + Undef = 0, + Point = 1 << 0, + Edge = 1 << 1, + Circle = 1 << 2, + Plane = 1 << 3 +}; + +class SurfaceFeature { +public: + SurfaceFeature(SurfaceFeatureType type, const Vec3d& pt1, const Vec3d& pt2, std::optional pt3 = std::nullopt, double value = 0.0) + : m_type(type), m_pt1(pt1), m_pt2(pt2), m_pt3(pt3), m_value(value) {} + + explicit SurfaceFeature(const Vec3d& pt) + : m_type{SurfaceFeatureType::Point}, m_pt1{pt} {} + + // Get type of this feature. + SurfaceFeatureType get_type() const { return m_type; } + + // For points, return the point. + Vec3d get_point() const { assert(m_type == SurfaceFeatureType::Point); return m_pt1; } + + // For edges, return start and end. + std::pair get_edge() const { assert(m_type == SurfaceFeatureType::Edge); return std::make_pair(m_pt1, m_pt2); } + + // For circles, return center, radius and normal. + std::tuple get_circle() const { assert(m_type == SurfaceFeatureType::Circle); return std::make_tuple(m_pt1, m_value, m_pt2); } + + // For planes, return index into vector provided by Measuring::get_plane_triangle_indices, normal and point. + std::tuple get_plane() const { assert(m_type == SurfaceFeatureType::Plane); return std::make_tuple(int(m_value), m_pt1, m_pt2); } + + // For anything, return an extra point that should also be considered a part of this. + std::optional get_extra_point() const { assert(m_type != SurfaceFeatureType::Undef); return m_pt3; } + + bool operator == (const SurfaceFeature& other) const { + if (this->m_type != other.m_type) return false; + switch (this->m_type) + { + case SurfaceFeatureType::Undef: { break; } + case SurfaceFeatureType::Point: { return (this->m_pt1.isApprox(other.m_pt1)); } + case SurfaceFeatureType::Edge: { + return (this->m_pt1.isApprox(other.m_pt1) && this->m_pt2.isApprox(other.m_pt2)) || + (this->m_pt1.isApprox(other.m_pt2) && this->m_pt2.isApprox(other.m_pt1)); + } + case SurfaceFeatureType::Plane: + case SurfaceFeatureType::Circle: { + return (this->m_pt1.isApprox(other.m_pt1) && this->m_pt2.isApprox(other.m_pt2) && std::abs(this->m_value - other.m_value) < EPSILON); + } + } + + return false; + } + + bool operator != (const SurfaceFeature& other) const { + return !operator == (other); + } + +private: + SurfaceFeatureType m_type{ SurfaceFeatureType::Undef }; + Vec3d m_pt1{ Vec3d::Zero() }; + Vec3d m_pt2{ Vec3d::Zero() }; + std::optional m_pt3; + double m_value{ 0.0 }; +}; + + + +class MeasuringImpl; + + +class Measuring { +public: + // Construct the measurement object on a given its. The its must remain + // valid and unchanged during the whole lifetime of the object. + explicit Measuring(const indexed_triangle_set& its); + ~Measuring(); + + // Return a reference to a list of all features identified on the its. + // Use only for debugging. Expensive, do not call often. + std::vector get_all_features() const; + + // Given a face_idx where the mouse cursor points, return a feature that + // should be highlighted (if any). + std::optional get_feature(size_t face_idx, const Vec3d& point) const; + + // Returns a list of triangle indices for each identified plane. Each + // Plane object contains an index into this vector. Expensive, do not + // call too often. + std::vector> get_planes_triangle_indices() const; + + // Returns the surface features of the plane with the given index + const std::vector& get_plane_features(unsigned int plane_id) const; + +private: + std::unique_ptr priv; +}; + + +struct DistAndPoints { + DistAndPoints(double dist_, Vec3d from_, Vec3d to_) : dist(dist_), from(from_), to(to_) {} + double dist; + Vec3d from; + Vec3d to; + + void transform(const Transform3d& trafo); +}; + +struct AngleAndEdges { + AngleAndEdges(double angle_, const Vec3d& center_, const std::pair& e1_, const std::pair& e2_, double radius_, bool coplanar_) + : angle(angle_), center(center_), e1(e1_), e2(e2_), radius(radius_), coplanar(coplanar_) {} + double angle; + Vec3d center; + std::pair e1; + std::pair e2; + double radius; + bool coplanar; + + void transform(const Transform3d& trafo); + + static const AngleAndEdges Dummy; +}; + +struct MeasurementResult { + std::optional angle; + std::optional distance_infinite; + std::optional distance_strict; + std::optional distance_xyz; + + bool has_distance_data() const { + return distance_infinite.has_value() || distance_strict.has_value(); + } + + bool has_any_data() const { + return angle.has_value() || distance_infinite.has_value() || distance_strict.has_value() || distance_xyz.has_value(); + } + + void transform(const Transform3d& trafo) { + if (angle.has_value()) + angle->transform(trafo); + if (distance_infinite.has_value()) + distance_infinite->transform(trafo); + if (distance_strict.has_value()) { + distance_strict->transform(trafo); + distance_xyz = (distance_strict->to - distance_strict->from).cwiseAbs(); + } + } +}; + +// Returns distance/angle between two SurfaceFeatures. +MeasurementResult get_measurement(const SurfaceFeature& a, const SurfaceFeature& b, const Measuring* measuring = nullptr); + +inline Vec3d edge_direction(const Vec3d& from, const Vec3d& to) { return (to - from).normalized(); } +inline Vec3d edge_direction(const std::pair& e) { return edge_direction(e.first, e.second); } +inline Vec3d edge_direction(const SurfaceFeature& edge) { + assert(edge.get_type() == SurfaceFeatureType::Edge); + return edge_direction(edge.get_edge()); +} + +inline Vec3d plane_normal(const SurfaceFeature& plane) { + assert(plane.get_type() == SurfaceFeatureType::Plane); + return std::get<1>(plane.get_plane()); +} + +inline bool are_parallel(const Vec3d& v1, const Vec3d& v2) { return std::abs(std::abs(v1.dot(v2)) - 1.0) < EPSILON; } +inline bool are_perpendicular(const Vec3d& v1, const Vec3d& v2) { return std::abs(v1.dot(v2)) < EPSILON; } + +inline bool are_parallel(const std::pair& e1, const std::pair& e2) { + return are_parallel(e1.second - e1.first, e2.second - e2.first); +} +inline bool are_parallel(const SurfaceFeature& f1, const SurfaceFeature& f2) { + if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Edge) + return are_parallel(edge_direction(f1), edge_direction(f2)); + else if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Plane) + return are_perpendicular(edge_direction(f1), plane_normal(f2)); + else + return false; +} + +inline bool are_perpendicular(const SurfaceFeature& f1, const SurfaceFeature& f2) { + if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Edge) + return are_perpendicular(edge_direction(f1), edge_direction(f2)); + else if (f1.get_type() == SurfaceFeatureType::Edge && f2.get_type() == SurfaceFeatureType::Plane) + return are_parallel(edge_direction(f1), plane_normal(f2)); + else + return false; +} + +} // namespace Measure +} // namespace Slic3r + +#endif // Slic3r_Measure_hpp_ diff --git a/src/libslic3r/MeasureUtils.hpp b/src/libslic3r/MeasureUtils.hpp new file mode 100644 index 0000000000..0ab4ac121d --- /dev/null +++ b/src/libslic3r/MeasureUtils.hpp @@ -0,0 +1,386 @@ +#ifndef Slic3r_MeasureUtils_hpp_ +#define Slic3r_MeasureUtils_hpp_ + +#include + +namespace Slic3r { +namespace Measure { + +// Utility class used to calculate distance circle-circle +// Adaptation of code found in: +// https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/Polynomial1.h + +class Polynomial1 +{ +public: + Polynomial1(std::initializer_list values) + { + // C++ 11 will call the default constructor for + // Polynomial1 p{}, so it is guaranteed that + // values.size() > 0. + m_coefficient.resize(values.size()); + std::copy(values.begin(), values.end(), m_coefficient.begin()); + EliminateLeadingZeros(); + } + + // Construction and destruction. The first constructor creates a + // polynomial of the specified degree but sets all coefficients to + // zero (to ensure initialization). You are responsible for setting + // the coefficients, presumably with the degree-term set to a nonzero + // number. In the second constructor, the degree is the number of + // initializers plus 1, but then adjusted so that coefficient[degree] + // is not zero (unless all initializer values are zero). + explicit Polynomial1(uint32_t degree) + : m_coefficient(static_cast(degree) + 1, 0.0) + {} + + // Eliminate any leading zeros in the polynomial, except in the case + // the degree is 0 and the coefficient is 0. The elimination is + // necessary when arithmetic operations cause a decrease in the degree + // of the result. For example, (1 + x + x^2) + (1 + 2*x - x^2) = + // (2 + 3*x). The inputs both have degree 2, so the result is created + // with degree 2. After the addition we find that the degree is in + // fact 1 and resize the array of coefficients. This function is + // called internally by the arithmetic operators, but it is exposed in + // the public interface in case you need it for your own purposes. + void EliminateLeadingZeros() + { + const size_t size = m_coefficient.size(); + if (size > 1) { + const double zero = 0.0; + int32_t leading; + for (leading = static_cast(size) - 1; leading > 0; --leading) { + if (m_coefficient[leading] != zero) + break; + } + + m_coefficient.resize(++leading); + } + } + + // Set all coefficients to the specified value. + void SetCoefficients(double value) + { + std::fill(m_coefficient.begin(), m_coefficient.end(), value); + } + + inline uint32_t GetDegree() const + { + // By design, m_coefficient.size() > 0. + return static_cast(m_coefficient.size() - 1); + } + + inline const double& operator[](uint32_t i) const { return m_coefficient[i]; } + inline double& operator[](uint32_t i) { return m_coefficient[i]; } + + // Evaluate the polynomial. If the polynomial is invalid, the + // function returns zero. + double operator()(double t) const + { + int32_t i = static_cast(m_coefficient.size()); + double result = m_coefficient[--i]; + for (--i; i >= 0; --i) { + result *= t; + result += m_coefficient[i]; + } + return result; + } + +protected: + // The class is designed so that m_coefficient.size() >= 1. + std::vector m_coefficient; +}; + +inline Polynomial1 operator * (const Polynomial1& p0, const Polynomial1& p1) +{ + const uint32_t p0Degree = p0.GetDegree(); + const uint32_t p1Degree = p1.GetDegree(); + Polynomial1 result(p0Degree + p1Degree); + result.SetCoefficients(0.0); + for (uint32_t i0 = 0; i0 <= p0Degree; ++i0) { + for (uint32_t i1 = 0; i1 <= p1Degree; ++i1) { + result[i0 + i1] += p0[i0] * p1[i1]; + } + } + return result; +} + +inline Polynomial1 operator + (const Polynomial1& p0, const Polynomial1& p1) +{ + const uint32_t p0Degree = p0.GetDegree(); + const uint32_t p1Degree = p1.GetDegree(); + uint32_t i; + if (p0Degree >= p1Degree) { + Polynomial1 result(p0Degree); + for (i = 0; i <= p1Degree; ++i) { + result[i] = p0[i] + p1[i]; + } + for (/**/; i <= p0Degree; ++i) { + result[i] = p0[i]; + } + result.EliminateLeadingZeros(); + return result; + } + else { + Polynomial1 result(p1Degree); + for (i = 0; i <= p0Degree; ++i) { + result[i] = p0[i] + p1[i]; + } + for (/**/; i <= p1Degree; ++i) { + result[i] = p1[i]; + } + result.EliminateLeadingZeros(); + return result; + } +} + +inline Polynomial1 operator - (const Polynomial1& p0, const Polynomial1& p1) +{ + const uint32_t p0Degree = p0.GetDegree(); + const uint32_t p1Degree = p1.GetDegree(); + uint32_t i; + if (p0Degree >= p1Degree) { + Polynomial1 result(p0Degree); + for (i = 0; i <= p1Degree; ++i) { + result[i] = p0[i] - p1[i]; + } + for (/**/; i <= p0Degree; ++i) { + result[i] = p0[i]; + } + result.EliminateLeadingZeros(); + return result; + } + else { + Polynomial1 result(p1Degree); + for (i = 0; i <= p0Degree; ++i) { + result[i] = p0[i] - p1[i]; + } + for (/**/; i <= p1Degree; ++i) { + result[i] = -p1[i]; + } + result.EliminateLeadingZeros(); + return result; + } +} + +inline Polynomial1 operator * (double scalar, const Polynomial1& p) +{ + const uint32_t degree = p.GetDegree(); + Polynomial1 result(degree); + for (uint32_t i = 0; i <= degree; ++i) { + result[i] = scalar * p[i]; + } + return result; +} + +// Utility class used to calculate distance circle-circle +// Adaptation of code found in: +// https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/RootsPolynomial.h + +class RootsPolynomial +{ +public: + // General equations: sum_{i=0}^{d} c(i)*t^i = 0. The input array 'c' + // must have at least d+1 elements and the output array 'root' must + // have at least d elements. + + // Find the roots on (-infinity,+infinity). + static int32_t Find(int32_t degree, const double* c, uint32_t maxIterations, double* roots) + { + if (degree >= 0 && c != nullptr) { + const double zero = 0.0; + while (degree >= 0 && c[degree] == zero) { + --degree; + } + + if (degree > 0) { + // Compute the Cauchy bound. + const double one = 1.0; + const double invLeading = one / c[degree]; + double maxValue = zero; + for (int32_t i = 0; i < degree; ++i) { + const double value = std::fabs(c[i] * invLeading); + if (value > maxValue) + maxValue = value; + } + const double bound = one + maxValue; + + return FindRecursive(degree, c, -bound, bound, maxIterations, roots); + } + else if (degree == 0) + // The polynomial is a nonzero constant. + return 0; + else { + // The polynomial is identically zero. + roots[0] = zero; + return 1; + } + } + else + // Invalid degree or c. + return 0; + } + + // If you know that p(tmin) * p(tmax) <= 0, then there must be at + // least one root in [tmin, tmax]. Compute it using bisection. + static bool Find(int32_t degree, const double* c, double tmin, double tmax, uint32_t maxIterations, double& root) + { + const double zero = 0.0; + double pmin = Evaluate(degree, c, tmin); + if (pmin == zero) { + root = tmin; + return true; + } + double pmax = Evaluate(degree, c, tmax); + if (pmax == zero) { + root = tmax; + return true; + } + + if (pmin * pmax > zero) + // It is not known whether the interval bounds a root. + return false; + + if (tmin >= tmax) + // Invalid ordering of interval endpoitns. + return false; + + for (uint32_t i = 1; i <= maxIterations; ++i) { + root = 0.5 * (tmin + tmax); + + // This test is designed for 'float' or 'double' when tmin + // and tmax are consecutive floating-point numbers. + if (root == tmin || root == tmax) + break; + + const double p = Evaluate(degree, c, root); + const double product = p * pmin; + if (product < zero) { + tmax = root; + pmax = p; + } + else if (product > zero) { + tmin = root; + pmin = p; + } + else + break; + } + + return true; + } + + // Support for the Find functions. + static int32_t FindRecursive(int32_t degree, double const* c, double tmin, double tmax, uint32_t maxIterations, double* roots) + { + // The base of the recursion. + const double zero = 0.0; + double root = zero; + if (degree == 1) { + int32_t numRoots; + if (c[1] != zero) { + root = -c[0] / c[1]; + numRoots = 1; + } + else if (c[0] == zero) { + root = zero; + numRoots = 1; + } + else + numRoots = 0; + + if (numRoots > 0 && tmin <= root && root <= tmax) { + roots[0] = root; + return 1; + } + return 0; + } + + // Find the roots of the derivative polynomial scaled by 1/degree. + // The scaling avoids the factorial growth in the coefficients; + // for example, without the scaling, the high-order term x^d + // becomes (d!)*x through multiple differentiations. With the + // scaling we instead get x. This leads to better numerical + // behavior of the root finder. + const int32_t derivDegree = degree - 1; + std::vector derivCoeff(static_cast(derivDegree) + 1); + std::vector derivRoots(derivDegree); + for (int32_t i = 0, ip1 = 1; i <= derivDegree; ++i, ++ip1) { + derivCoeff[i] = c[ip1] * (double)(ip1) / (double)degree; + } + const int32_t numDerivRoots = FindRecursive(degree - 1, &derivCoeff[0], tmin, tmax, maxIterations, &derivRoots[0]); + + int32_t numRoots = 0; + if (numDerivRoots > 0) { + // Find root on [tmin,derivRoots[0]]. + if (Find(degree, c, tmin, derivRoots[0], maxIterations, root)) + roots[numRoots++] = root; + + // Find root on [derivRoots[i],derivRoots[i+1]]. + for (int32_t i = 0, ip1 = 1; i <= numDerivRoots - 2; ++i, ++ip1) { + if (Find(degree, c, derivRoots[i], derivRoots[ip1], maxIterations, root)) + roots[numRoots++] = root; + } + + // Find root on [derivRoots[numDerivRoots-1],tmax]. + if (Find(degree, c, derivRoots[static_cast(numDerivRoots) - 1], tmax, maxIterations, root)) + roots[numRoots++] = root; + } + else { + // The polynomial is monotone on [tmin,tmax], so has at most one root. + if (Find(degree, c, tmin, tmax, maxIterations, root)) + roots[numRoots++] = root; + } + return numRoots; + } + + static double Evaluate(int32_t degree, const double* c, double t) + { + int32_t i = degree; + double result = c[i]; + while (--i >= 0) { + result = t * result + c[i]; + } + return result; + } +}; + +// Adaptation of code found in: +// https://github.com/davideberly/GeometricTools/blob/master/GTE/Mathematics/Vector.h + +// Construct a single vector orthogonal to the nonzero input vector. If +// the maximum absolute component occurs at index i, then the orthogonal +// vector U has u[i] = v[i+1], u[i+1] = -v[i], and all other components +// zero. The index addition i+1 is computed modulo N. +inline Vec3d get_orthogonal(const Vec3d& v, bool unitLength) +{ + double cmax = std::fabs(v[0]); + int32_t imax = 0; + for (int32_t i = 1; i < 3; ++i) { + double c = std::fabs(v[i]); + if (c > cmax) { + cmax = c; + imax = i; + } + } + + Vec3d result = Vec3d::Zero(); + int32_t inext = imax + 1; + if (inext == 3) + inext = 0; + + result[imax] = v[inext]; + result[inext] = -v[imax]; + if (unitLength) { + const double sqrDistance = result[imax] * result[imax] + result[inext] * result[inext]; + const double invLength = 1.0 / std::sqrt(sqrDistance); + result[imax] *= invLength; + result[inext] *= invLength; + } + return result; +} + +} // namespace Slic3r +} // namespace Measure + +#endif // Slic3r_MeasureUtils_hpp_ diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 3c4e004544..52ddbea9b4 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1661,6 +1661,12 @@ void ModelObject::split(ModelObjectPtrs* new_objects) new_object->add_instance(*model_instance); ModelVolume* new_vol = new_object->add_volume(*volume, std::move(mesh)); + // Invalidate extruder value in volume's config, + // otherwise there will no way to change extruder for object after splitting, + // because volume's extruder value overrides object's extruder value. + if (new_vol->config.has("extruder")) + new_vol->config.set_key_value("extruder", new ConfigOptionInt(0)); + for (ModelInstance* model_instance : new_object->instances) { #if ENABLE_WORLD_COORDINATE Vec3d shift = model_instance->get_transformation().get_matrix_no_offset() * new_vol->get_offset(); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index c3792779c3..330ad533c5 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -988,6 +988,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ DynamicPrintConfig filament_overrides; t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides); t_config_option_keys full_config_diff = full_print_config_diffs(m_full_print_config, new_full_config); + // If just a physical printer was changed, but printer preset is the same, then there is no need to apply whole print + // see https://github.com/prusa3d/PrusaSlicer/issues/8800 + if (full_config_diff.size() == 1 && full_config_diff[0] == "physical_printer_settings_id") + full_config_diff.clear(); + // Collect changes to object and region configs. t_config_option_keys object_diff = m_default_object_config.diff(new_full_config); t_config_option_keys region_diff = m_default_region_config.diff(new_full_config); diff --git a/src/libslic3r/SurfaceMesh.hpp b/src/libslic3r/SurfaceMesh.hpp new file mode 100644 index 0000000000..9e547eec49 --- /dev/null +++ b/src/libslic3r/SurfaceMesh.hpp @@ -0,0 +1,154 @@ +#ifndef slic3r_SurfaceMesh_hpp_ +#define slic3r_SurfaceMesh_hpp_ + +#include +#include + +namespace Slic3r { + +class TriangleMesh; + + + +enum Face_index : int; + +class Halfedge_index { + friend class SurfaceMesh; + +public: + Halfedge_index() : m_face(Face_index(-1)), m_side(0) {} + Face_index face() const { return m_face; } + unsigned char side() const { return m_side; } + bool is_invalid() const { return int(m_face) < 0; } + bool operator!=(const Halfedge_index& rhs) const { return ! ((*this) == rhs); } + bool operator==(const Halfedge_index& rhs) const { return m_face == rhs.m_face && m_side == rhs.m_side; } + +private: + Halfedge_index(int face_idx, unsigned char side_idx) : m_face(Face_index(face_idx)), m_side(side_idx) {} + + Face_index m_face; + unsigned char m_side; +}; + + + +class Vertex_index { + friend class SurfaceMesh; + +public: + Vertex_index() : m_face(Face_index(-1)), m_vertex_idx(0) {} + bool is_invalid() const { return int(m_face) < 0; } + bool operator==(const Vertex_index& rhs) const = delete; // Use SurfaceMesh::is_same_vertex. + +private: + Vertex_index(int face_idx, unsigned char vertex_idx) : m_face(Face_index(face_idx)), m_vertex_idx(vertex_idx) {} + + Face_index m_face; + unsigned char m_vertex_idx; +}; + + + +class SurfaceMesh { +public: + explicit SurfaceMesh(const indexed_triangle_set& its) + : m_its(its), + m_face_neighbors(its_face_neighbors_par(its)) + {} + SurfaceMesh(const SurfaceMesh&) = delete; + SurfaceMesh& operator=(const SurfaceMesh&) = delete; + + Vertex_index source(Halfedge_index h) const { assert(! h.is_invalid()); return Vertex_index(h.m_face, h.m_side); } + Vertex_index target(Halfedge_index h) const { assert(! h.is_invalid()); return Vertex_index(h.m_face, h.m_side == 2 ? 0 : h.m_side + 1); } + Face_index face(Halfedge_index h) const { assert(! h.is_invalid()); return h.m_face; } + + Halfedge_index next(Halfedge_index h) const { assert(! h.is_invalid()); h.m_side = (h.m_side + 1) % 3; return h; } + Halfedge_index prev(Halfedge_index h) const { assert(! h.is_invalid()); h.m_side = (h.m_side == 0 ? 2 : h.m_side - 1); return h; } + Halfedge_index halfedge(Vertex_index v) const { return Halfedge_index(v.m_face, (v.m_vertex_idx == 0 ? 2 : v.m_vertex_idx - 1)); } + Halfedge_index halfedge(Face_index f) const { return Halfedge_index(f, 0); } + Halfedge_index opposite(Halfedge_index h) const { + if (h.is_invalid()) + return h; + + int face_idx = m_face_neighbors[h.m_face][h.m_side]; + Halfedge_index h_candidate = halfedge(Face_index(face_idx)); + + if (h_candidate.is_invalid()) + return Halfedge_index(); // invalid + + for (int i=0; i<3; ++i) { + if (is_same_vertex(source(h_candidate), target(h))) { + // Meshes in PrusaSlicer should be fixed enough for the following not to happen. + assert(is_same_vertex(target(h_candidate), source(h))); + return h_candidate; + } + h_candidate = next(h_candidate); + } + return Halfedge_index(); // invalid + } + + Halfedge_index next_around_target(Halfedge_index h) const { return opposite(next(h)); } + Halfedge_index prev_around_target(Halfedge_index h) const { Halfedge_index op = opposite(h); return (op.is_invalid() ? Halfedge_index() : prev(op)); } + Halfedge_index next_around_source(Halfedge_index h) const { Halfedge_index op = opposite(h); return (op.is_invalid() ? Halfedge_index() : next(op)); } + Halfedge_index prev_around_source(Halfedge_index h) const { return opposite(prev(h)); } + Halfedge_index halfedge(Vertex_index source, Vertex_index target) const + { + Halfedge_index hi(source.m_face, source.m_vertex_idx); + assert(! hi.is_invalid()); + + const Vertex_index orig_target = this->target(hi); + Vertex_index current_target = orig_target; + + while (! is_same_vertex(current_target, target)) { + hi = next_around_source(hi); + if (hi.is_invalid()) + break; + current_target = this->target(hi); + if (is_same_vertex(current_target, orig_target)) + return Halfedge_index(); // invalid + } + + return hi; + } + + const stl_vertex& point(Vertex_index v) const { return m_its.vertices[m_its.indices[v.m_face][v.m_vertex_idx]]; } + + size_t degree(Vertex_index v) const + { + Halfedge_index h_first = halfedge(v); + Halfedge_index h = next_around_target(h_first); + size_t degree = 2; + while (! h.is_invalid() && h != h_first) { + h = next_around_target(h); + ++degree; + } + return h.is_invalid() ? 0 : degree - 1; + } + + size_t degree(Face_index f) const { + size_t total = 0; + for (unsigned char i=0; i<3; ++i) { + size_t d = degree(Vertex_index(f, i)); + if (d == 0) + return 0; + total += d; + } + assert(total - 6 >= 0); + return total - 6; // we counted 3 halfedges from f, and one more for each neighbor + } + + bool is_border(Halfedge_index h) const { return m_face_neighbors[h.m_face][h.m_side] == -1; } + + bool is_same_vertex(const Vertex_index& a, const Vertex_index& b) const { return m_its.indices[a.m_face][a.m_vertex_idx] == m_its.indices[b.m_face][b.m_vertex_idx]; } + Vec3i get_face_neighbors(Face_index face_id) const { assert(int(face_id) < int(m_face_neighbors.size())); return m_face_neighbors[face_id]; } + + + +private: + const std::vector m_face_neighbors; + const indexed_triangle_set& m_its; +}; + +} //namespace Slic3r + +#endif // slic3r_SurfaceMesh_hpp_ diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index e8fc53260b..9bef1e4986 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -26,6 +26,8 @@ #define ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW 0 // Disable using instanced models to render options in gcode preview #define DISABLE_GCODEVIEWER_INSTANCED_MODELS 1 +// Enable Measure Gizmo debug window +#define ENABLE_MEASURE_GIZMO_DEBUG 0 // Enable rendering of objects using environment map diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index f96738acea..4fa51c4868 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -65,6 +65,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoSimplify.hpp GUI/Gizmos/GLGizmoMmuSegmentation.cpp GUI/Gizmos/GLGizmoMmuSegmentation.hpp + GUI/Gizmos/GLGizmoMeasure.cpp + GUI/Gizmos/GLGizmoMeasure.hpp GUI/GLSelectionRectangle.cpp GUI/GLSelectionRectangle.hpp GUI/GLModel.hpp diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 4319a28cf1..7572d4fc46 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -223,7 +223,10 @@ bool Bed3D::set_shape(const Pointfs& bed_shape, const double max_print_height, c #if ENABLE_LEGACY_OPENGL_REMOVAL m_contour = ExPolygon(Polygon::new_scale(bed_shape)); - m_polygon = offset(m_contour.contour, (float)m_contour.contour.bounding_box().radius() * 1.7f, jtRound, scale_(0.5)).front(); + const BoundingBox bbox = m_contour.contour.bounding_box(); + if (!bbox.defined) + throw RuntimeError(std::string("Invalid bed shape")); + m_polygon = offset(m_contour.contour, (float)bbox.radius() * 1.7f, jtRound, scale_(0.5)).front(); m_triangles.reset(); m_gridlines.reset(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 6dd4cf8a3f..fdbc977462 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1377,9 +1377,9 @@ void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject const GLGizmosManager& gm = get_gizmos_manager(); auto gizmo_type = gm.get_current_type(); if ( (gizmo_type == GLGizmosManager::FdmSupports - || gizmo_type == GLGizmosManager::Seam - || gizmo_type == GLGizmosManager::Cut) - && ! vol->is_modifier) { + || gizmo_type == GLGizmosManager::Seam + || gizmo_type == GLGizmosManager::Cut) + && !vol->is_modifier) { vol->force_neutral_color = true; if (gizmo_type == GLGizmosManager::Cut) vol->color.a(0.95f); @@ -2438,8 +2438,10 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re // refresh volume raycasters for picking m_scene_raycaster.remove_raycasters(SceneRaycaster::EType::Volume); for (size_t i = 0; i < m_volumes.volumes.size(); ++i) { - assert(m_volumes.volumes[i]->mesh_raycaster != nullptr); - add_raycaster_for_picking(SceneRaycaster::EType::Volume, i, *m_volumes.volumes[i]->mesh_raycaster, m_volumes.volumes[i]->world_matrix()); + const GLVolume* v = m_volumes.volumes[i]; + assert(v->mesh_raycaster != nullptr); + std::shared_ptr raycaster = add_raycaster_for_picking(SceneRaycaster::EType::Volume, i, *v->mesh_raycaster, v->world_matrix()); + raycaster->set_active(v->is_active); } // refresh gizmo elements raycasters for picking @@ -3377,9 +3379,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) show_sinking_contours(); } } - else if (evt.LeftUp() && - m_gizmos.get_current_type() == GLGizmosManager::EType::Scale && - m_gizmos.get_current()->get_state() == GLGizmoBase::EState::On) { + else if (evt.LeftUp() && + m_gizmos.get_current_type() == GLGizmosManager::EType::Scale && + m_gizmos.get_current()->get_state() == GLGizmoBase::EState::On) { wxGetApp().obj_list()->selection_changed(); } @@ -3458,6 +3460,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_gizmos.get_current_type() != GLGizmosManager::FdmSupports && m_gizmos.get_current_type() != GLGizmosManager::Seam && m_gizmos.get_current_type() != GLGizmosManager::Cut && + m_gizmos.get_current_type() != GLGizmosManager::Measure && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) { m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); m_dirty = true; @@ -3693,7 +3696,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) // if right clicking on volume, propagate event through callback (shows context menu) int volume_idx = get_first_hover_volume_idx(); if (!m_volumes.volumes[volume_idx]->is_wipe_tower // no context menu for the wipe tower - && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports) // disable context menu when the gizmo is open + && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports && m_gizmos.get_current_type() != GLGizmosManager::Measure)) // disable context menu when the gizmo is open { // forces the selection of the volume /* m_selection.add(volume_idx); // #et_FIXME_if_needed @@ -3717,7 +3720,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.dragging) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower - const bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; + const bool post_right_click_event = (m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower) && + m_gizmos.get_current_type() != GLGizmosManager::Measure; if (post_right_click_event) post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() })); } @@ -5467,10 +5471,9 @@ void GLCanvas3D::_picking_pass() 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)) { + if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL)) m_hover_volume_idxs.emplace_back(hit.raycaster_id); - m_gizmos.set_hover_id(-1); - } + m_gizmos.set_hover_id(-1); } } else @@ -5481,8 +5484,8 @@ void GLCanvas3D::_picking_pass() case SceneRaycaster::EType::Gizmo: { const Size& cnv_size = get_canvas_size(); - bool inside = 0 <= m_mouse.position.x() && m_mouse.position.x() < cnv_size.get_width() && - 0 <= m_mouse.position.y() && m_mouse.position.y() < cnv_size.get_height(); + const bool inside = 0 <= m_mouse.position.x() && m_mouse.position.x() < cnv_size.get_width() && + 0 <= m_mouse.position.y() && m_mouse.position.y() < cnv_size.get_height(); m_gizmos.set_hover_id(inside ? hit.raycaster_id : -1); break; } diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 8cb0b0e2e6..a519a26a19 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -19,6 +19,7 @@ #if ENABLE_RAYCAST_PICKING #include "SceneRaycaster.hpp" #endif // ENABLE_RAYCAST_PICKING +#include "GUI_Utils.hpp" #include "libslic3r/Slicing.hpp" @@ -135,16 +136,6 @@ private: wxTimer* m_timer; }; -class KeyAutoRepeatFilter -{ - size_t m_count{ 0 }; - -public: - void increase_count() { ++m_count; } - void reset_count() { m_count = 0; } - bool is_first() const { return m_count == 0; } -}; - wxDECLARE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); using Vec2dEvent = Event; @@ -680,7 +671,7 @@ public: #if ENABLE_RAYCAST_PICKING std::shared_ptr add_raycaster_for_picking(SceneRaycaster::EType type, int id, const MeshRaycaster& raycaster, - const Transform3d& trafo, bool use_back_faces = false) { + const Transform3d& trafo = Transform3d::Identity(), bool use_back_faces = false) { return m_scene_raycaster.add_raycaster(type, id, raycaster, trafo, use_back_faces); } void remove_raycasters_for_picking(SceneRaycaster::EType type, int id) { diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 2d6a66d43c..2d4d25f8c8 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -257,6 +257,21 @@ void GLModel::Geometry::remove_vertex(size_t id) } } +indexed_triangle_set GLModel::Geometry::get_as_indexed_triangle_set() const +{ + indexed_triangle_set its; + its.vertices.reserve(vertices_count()); + for (size_t i = 0; i < vertices_count(); ++i) { + its.vertices.emplace_back(extract_position_3(i)); + } + its.indices.reserve(indices_count() / 3); + for (size_t i = 0; i < indices_count() / 3; ++i) { + const size_t tri_id = i * 3; + its.indices.emplace_back(extract_index(tri_id), extract_index(tri_id + 1), extract_index(tri_id + 2)); + } + return its; +} + size_t GLModel::Geometry::vertex_stride_floats(const Format& format) { switch (format.vertex_layout) @@ -2253,6 +2268,128 @@ GLModel::Geometry smooth_sphere(unsigned int resolution, float radius) return data; } + +GLModel::Geometry smooth_cylinder(unsigned int resolution, float radius, float height) +{ + resolution = std::max(4, resolution); + + const unsigned int sectorCount = resolution; + const float sectorStep = 2.0f * float(M_PI) / float(sectorCount); + + GLModel::Geometry data; + data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + data.reserve_vertices(sectorCount * 4 + 2); + data.reserve_indices(sectorCount * 4 * 3); + + auto generate_vertices_on_circle = [sectorCount, sectorStep](float radius) { + std::vector ret; + ret.reserve(sectorCount); + for (unsigned int i = 0; i < sectorCount; ++i) { + // from 0 to 2pi + const float sectorAngle = sectorStep * i; + ret.emplace_back(radius * std::cos(sectorAngle), radius * std::sin(sectorAngle), 0.0f); + } + return ret; + }; + + const std::vector base_vertices = generate_vertices_on_circle(radius); + const Vec3f h = height * Vec3f::UnitZ(); + + // stem vertices + for (unsigned int i = 0; i < sectorCount; ++i) { + const Vec3f& v = base_vertices[i]; + const Vec3f n = v.normalized(); + data.add_vertex(v, n); + data.add_vertex(v + h, n); + } + + // stem triangles + for (unsigned int i = 0; i < sectorCount; ++i) { + unsigned int v1 = i * 2; + unsigned int v2 = (i < sectorCount - 1) ? v1 + 2 : 0; + unsigned int v3 = v2 + 1; + unsigned int v4 = v1 + 1; + data.add_triangle(v1, v2, v3); + data.add_triangle(v1, v3, v4); + } + + // bottom cap vertices + Vec3f cap_center = Vec3f::Zero(); + unsigned int cap_center_id = data.vertices_count(); + Vec3f normal = -Vec3f::UnitZ(); + + data.add_vertex(cap_center, normal); + for (unsigned int i = 0; i < sectorCount; ++i) { + data.add_vertex(base_vertices[i], normal); + } + + // bottom cap triangles + for (unsigned int i = 0; i < sectorCount; ++i) { + data.add_triangle(cap_center_id, (i < sectorCount - 1) ? cap_center_id + i + 2 : cap_center_id + 1, cap_center_id + i + 1); + } + + // top cap vertices + cap_center += h; + cap_center_id = data.vertices_count(); + normal = -normal; + + data.add_vertex(cap_center, normal); + for (unsigned int i = 0; i < sectorCount; ++i) { + data.add_vertex(base_vertices[i] + h, normal); + } + + // top cap triangles + for (unsigned int i = 0; i < sectorCount; ++i) { + data.add_triangle(cap_center_id, cap_center_id + i + 1, (i < sectorCount - 1) ? cap_center_id + i + 2 : cap_center_id + 1); + } + + return data; +} + +GLModel::Geometry smooth_torus(unsigned int primary_resolution, unsigned int secondary_resolution, float radius, float thickness) +{ + const unsigned int torus_sector_count = std::max(4, primary_resolution); + const float torus_sector_step = 2.0f * float(M_PI) / float(torus_sector_count); + const unsigned int section_sector_count = std::max(4, secondary_resolution); + const float section_sector_step = 2.0f * float(M_PI) / float(section_sector_count); + + GLModel::Geometry data; + data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + data.reserve_vertices(torus_sector_count * section_sector_count); + data.reserve_indices(torus_sector_count * section_sector_count * 2 * 3); + + // vertices + for (unsigned int i = 0; i < torus_sector_count; ++i) { + const float section_angle = torus_sector_step * i; + const float csa = std::cos(section_angle); + const float ssa = std::sin(section_angle); + const Vec3f section_center(radius * csa, radius * ssa, 0.0f); + for (unsigned int j = 0; j < section_sector_count; ++j) { + const float circle_angle = section_sector_step * j; + const float thickness_xy = thickness * std::cos(circle_angle); + const float thickness_z = thickness * std::sin(circle_angle); + const Vec3f v(thickness_xy * csa, thickness_xy * ssa, thickness_z); + data.add_vertex(section_center + v, (Vec3f)v.normalized()); + } + } + + // triangles + for (unsigned int i = 0; i < torus_sector_count; ++i) { + const unsigned int ii = i * section_sector_count; + const unsigned int ii_next = ((i + 1) % torus_sector_count) * section_sector_count; + for (unsigned int j = 0; j < section_sector_count; ++j) { + const unsigned int j_next = (j + 1) % section_sector_count; + const unsigned int i0 = ii + j; + const unsigned int i1 = ii_next + j; + const unsigned int i2 = ii_next + j_next; + const unsigned int i3 = ii + j_next; + data.add_triangle(i0, i1, i2); + data.add_triangle(i0, i2, i3); + } + } + + return data; +} #endif // ENABLE_LEGACY_OPENGL_REMOVAL } // namespace GUI diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index c2845c8850..2d1e352c48 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -140,6 +140,8 @@ namespace GUI { size_t vertices_size_bytes() const { return vertices_size_floats() * sizeof(float); } size_t indices_size_bytes() const { return indices.size() * index_stride_bytes(*this); } + indexed_triangle_set get_as_indexed_triangle_set() const; + static size_t vertex_stride_floats(const Format& format); static size_t vertex_stride_bytes(const Format& format) { return vertex_stride_floats(format) * sizeof(float); } @@ -374,9 +376,17 @@ namespace GUI { GLModel::Geometry diamond(unsigned int resolution); #if ENABLE_LEGACY_OPENGL_REMOVAL - // create a sphere with the given resolution and smooth normals + // create a sphere with smooth normals // the origin of the sphere is in its center GLModel::Geometry smooth_sphere(unsigned int resolution, float radius); + // create a cylinder with smooth normals + // the axis of the cylinder is the Z axis + // the origin of the cylinder is the center of its bottom cap face + GLModel::Geometry smooth_cylinder(unsigned int resolution, float radius, float height); + // create a torus with smooth normals + // the axis of the torus is the Z axis + // the origin of the torus is in its center + GLModel::Geometry smooth_torus(unsigned int primary_resolution, unsigned int secondary_resolution, float radius, float thickness); #endif // ENABLE_LEGACY_OPENGL_REMOVAL } // namespace GUI diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index c05ddabe4a..4953de11c8 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1713,11 +1713,16 @@ void ObjectList::load_generic_subobject(const std::string& type_name, const Mode new_volume->set_transformation(Geometry::Transformation::volume_to_bed_transformation(v->get_instance_transformation(), mesh_bb)); #endif // ENABLE_WORLD_COORDINATE // Set the modifier position. - auto offset = (type_name == "Slab") ? - // Slab: Lift to print bed - Vec3d(0., 0., 0.5 * mesh_bb.size().z() + instance_bb.min.z() - v->get_instance_offset().z()) : + Vec3d offset; + if (type_name == "Slab") { + Vec3d inst_center = instance_bb.center() - v->get_instance_offset(); + // Slab: Lift to print bed and and push to the center of instance + offset = Vec3d(inst_center.x(), inst_center.y(), 0.5 * mesh_bb.size().z() + instance_bb.min.z() - v->get_instance_offset().z()); + } + else { // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. - Vec3d(instance_bb.max.x(), instance_bb.min.y(), instance_bb.min.z()) + 0.5 * mesh_bb.size() - v->get_instance_offset(); + offset = Vec3d(instance_bb.max.x(), instance_bb.min.y(), instance_bb.min.z()) + 0.5 * mesh_bb.size() - v->get_instance_offset(); + } #if ENABLE_WORLD_COORDINATE new_volume->set_offset(v->get_instance_transformation().get_matrix_no_offset().inverse() * offset); #else @@ -2904,6 +2909,9 @@ static bool can_add_volumes_to_object(const ModelObject* object) wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, std::function add_to_selection/* = nullptr*/) { + const bool is_prevent_list_events = m_prevent_list_events; + m_prevent_list_events = true; + wxDataViewItem object_item = m_objects_model->GetItemById(int(obj_idx)); m_objects_model->DeleteVolumeChildren(object_item); @@ -2932,6 +2940,7 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st Expand(object_item); } + m_prevent_list_events = is_prevent_list_events; return items; } diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index d7d3529fee..55ca432480 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -416,6 +416,16 @@ public: ~TaskTimer(); }; +class KeyAutoRepeatFilter +{ + size_t m_count{ 0 }; + +public: + void increase_count() { ++m_count; } + void reset_count() { m_count = 0; } + bool is_first() const { return m_count == 0; } +}; + }} #endif diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 14b31d7c50..818872bb41 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -864,7 +864,10 @@ void GLGizmoCut3D::on_set_state() m_parent.request_extra_frame(); } else { - m_c->object_clipper()->release(); + if (auto oc = m_c->object_clipper()) { + oc->set_behavior(true, true, 0.); + oc->release(); + } m_selected.clear(); } force_update_clipper_on_render = m_state == On; @@ -1000,7 +1003,7 @@ bool GLGizmoCut3D::on_is_activable() const { const Selection& selection = m_parent.get_selection(); const int object_idx = selection.get_object_idx(); - if (object_idx < 0) + if (object_idx < 0 || selection.is_wipe_tower()) return false; bool is_dowel_object = false; @@ -1391,7 +1394,7 @@ void GLGizmoCut3D::on_render() void GLGizmoCut3D::render_debug_input_window() { m_imgui->begin(wxString("DEBUG")); - +/* static bool hide_clipped = false; static bool fill_cut = false; static float contour_width = 0.4f; @@ -1405,7 +1408,7 @@ void GLGizmoCut3D::render_debug_input_window() oc->set_behavior(hide_clipped || m_connectors_editing, fill_cut || m_connectors_editing, double(contour_width)); ImGui::Separator(); - +*/ if (m_imgui->checkbox(_L("Render cut plane as circle"), m_cut_plane_as_circle)) m_plane.reset(); @@ -1527,10 +1530,18 @@ void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) ImGui::Separator(); if (m_imgui->button(_L("Confirm connectors"))) { - m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); + m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); unselect_all_connectors(); set_connectors_editing(false); } + + ImGui::SameLine(2.75f * m_label_width); + + if (m_imgui->button(_L("Cancel"))) { + m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); + reset_connectors(); + set_connectors_editing(false); + } } void GLGizmoCut3D::render_build_size() @@ -1573,6 +1584,8 @@ void GLGizmoCut3D::set_connectors_editing(bool connectors_editing) m_connectors_editing = connectors_editing; update_raycasters_for_picking(); + m_c->object_clipper()->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); + m_parent.request_extra_frame(); } @@ -1603,6 +1616,13 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) reset_cut_plane(); m_imgui->disabled_end(); + ImGui::SameLine(2.25f * m_label_width); + ImGui::PushItemWidth(0.75f * m_label_width); + m_is_contour_changed = m_imgui->slider_float("contour width", &m_contour_width, 0.f, 3.f); + + if (auto oc = m_c->object_clipper(); oc && m_is_contour_changed) + oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); + if (wxGetApp().plater()->printer_technology() == ptFFF) { m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower); if (m_imgui->button(_L("Add/Edit connectors"))) @@ -1641,7 +1661,7 @@ void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) ImGui::Separator(); - m_imgui->disabled_begin(!can_perform_cut()); + m_imgui->disabled_begin(!m_is_contour_changed && !can_perform_cut()); if(m_imgui->button(_L("Perform cut"))) perform_cut(m_parent.get_selection()); m_imgui->disabled_end(); @@ -1729,6 +1749,8 @@ void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) void GLGizmoCut3D::render_input_window_warning() const { + if (m_is_contour_changed) + return; if (wxGetApp().plater()->printer_technology() == ptFFF && m_has_invalid_connector) { wxString out = wxString(ImGui::WarningMarkerSmall) + _L("Invalid connectors detected") + ":"; if (m_info_stats.outside_cut_contour > size_t(0)) @@ -1829,7 +1851,8 @@ void GLGizmoCut3D::render_connectors() { ::glEnable(GL_DEPTH_TEST); - if (cut_line_processing() || m_connector_mode == CutConnectorMode::Auto || !m_c->selection_info()) + if (m_is_contour_changed || cut_line_processing() || + m_connector_mode == CutConnectorMode::Auto || !m_c->selection_info()) return; const ModelObject* mo = m_c->selection_info()->model_object(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp index 35b6a92cec..8a3ee6d074 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.hpp @@ -96,7 +96,7 @@ class GLGizmoCut3D : public GLGizmoBase bool m_hide_cut_plane{ false }; bool m_connectors_editing{ false }; - bool m_cut_plane_as_circle{ false }; + bool m_cut_plane_as_circle{ true }; float m_connector_depth_ratio{ 3.f }; float m_connector_size{ 2.5f }; @@ -109,6 +109,9 @@ class GLGizmoCut3D : public GLGizmoBase bool m_imperial_units{ false }; bool force_update_clipper_on_render{false}; + float m_contour_width{ 0.4f }; + bool m_is_contour_changed{ false }; + mutable std::vector m_selected; // which pins are currently selected int m_selected_count{ 0 }; @@ -170,7 +173,6 @@ public: void put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset); void update_clipper(); void update_clipper_on_render(); - void set_connectors_editing() { m_connectors_editing = true; } void invalidate_cut_plane(); BoundingBoxf3 bounding_box() const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp index f1813c4bd0..b4e47d3985 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp @@ -25,6 +25,8 @@ class GLGizmoFlatten : public GLGizmoBase private: + GLModel arrow; + struct PlaneData { std::vector vertices; // should be in fact local in update_planes() #if ENABLE_LEGACY_OPENGL_REMOVAL diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index d21005e4c2..1ce118d71d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -114,7 +114,7 @@ void GLGizmoHollow::on_register_raycasters_for_picking() if (info != nullptr && !info->model_object()->sla_drain_holes.empty()) { const sla::DrainHoles& drain_holes = info->model_object()->sla_drain_holes; for (int i = 0; i < (int)drain_holes.size(); ++i) { - m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cylinder.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cylinder.mesh_raycaster)); } update_raycasters_for_picking_transform(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp new file mode 100644 index 0000000000..3fa8d8d6f3 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp @@ -0,0 +1,1697 @@ +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoMeasure.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" + +#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/MeasureUtils.hpp" + +#include + +#include + +#include + +#include + +namespace Slic3r { +namespace GUI { + +static const Slic3r::ColorRGBA SELECTED_1ST_COLOR = { 0.25f, 0.75f, 0.75f, 1.0f }; +static const Slic3r::ColorRGBA SELECTED_2ND_COLOR = { 0.75f, 0.25f, 0.75f, 1.0f }; + +static const int POINT_ID = 100; +static const int EDGE_ID = 200; +static const int CIRCLE_ID = 300; +static const int PLANE_ID = 400; +static const int SELECTION_1_ID = 501; +static const int SELECTION_2_ID = 502; + +static const float TRIANGLE_BASE = 10.0f; +static const float TRIANGLE_HEIGHT = TRIANGLE_BASE * 1.618033f; + +static const std::string CTRL_STR = +#ifdef __APPLE__ +"⌘" +#else +"Ctrl" +#endif //__APPLE__ +; + +static std::string format_double(double value) +{ + char buf[1024]; + sprintf(buf, "%.3f", value); + return std::string(buf); +} + +static std::string format_vec3(const Vec3d& v) +{ + char buf[1024]; + sprintf(buf, "X: %.3f, Y: %.3f, Z: %.3f", v.x(), v.y(), v.z()); + return std::string(buf); +} + +static std::string surface_feature_type_as_string(Measure::SurfaceFeatureType type) +{ + switch (type) + { + default: + case Measure::SurfaceFeatureType::Undef: { return _u8L("No feature"); } + case Measure::SurfaceFeatureType::Point: { return _u8L("Vertex"); } + case Measure::SurfaceFeatureType::Edge: { return _u8L("Edge"); } + case Measure::SurfaceFeatureType::Circle: { return _u8L("Circle"); } + case Measure::SurfaceFeatureType::Plane: { return _u8L("Plane"); } + } +} + +static std::string point_on_feature_type_as_string(Measure::SurfaceFeatureType type, int hover_id) +{ + std::string ret; + switch (type) { + case Measure::SurfaceFeatureType::Point: { ret = _u8L("Vertex"); break; } + case Measure::SurfaceFeatureType::Edge: { ret = (hover_id == POINT_ID) ? _u8L("Center of edge") : _u8L("Point on edge"); break; } + case Measure::SurfaceFeatureType::Circle: { ret = (hover_id == POINT_ID) ? _u8L("Center of circle") : _u8L("Point on circle"); break; } + case Measure::SurfaceFeatureType::Plane: { ret = _u8L("Point on plane"); break; } + default: { assert(false); break; } + } + return ret; +} + +static GLModel::Geometry init_plane_data(const indexed_triangle_set& its, const std::vector>& planes_triangles, int idx) +{ + assert(0 <= idx && idx < (int)planes_triangles.size()); + const std::vector& triangle_indices = planes_triangles[idx]; + + GLModel::Geometry init_data; + init_data.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + unsigned int i = 0; + for (int idx : triangle_indices) { + const Vec3f& v0 = its.vertices[its.indices[idx][0]]; + const Vec3f& v1 = its.vertices[its.indices[idx][1]]; + const Vec3f& v2 = its.vertices[its.indices[idx][2]]; + + const Vec3f n = (v1 - v0).cross(v2 - v0).normalized(); + init_data.add_vertex(v0, n); + init_data.add_vertex(v1, n); + init_data.add_vertex(v2, n); + init_data.add_triangle(i, i + 1, i + 2); + i += 3; + } + + return init_data; +} + +static GLModel::Geometry init_torus_data(unsigned int primary_resolution, unsigned int secondary_resolution, const Vec3f& center, + float radius, float thickness, const Vec3f& model_axis, const Transform3f& world_trafo) +{ + const unsigned int torus_sector_count = std::max(4, primary_resolution); + const unsigned int section_sector_count = std::max(4, secondary_resolution); + const float torus_sector_step = 2.0f * float(M_PI) / float(torus_sector_count); + const float section_sector_step = 2.0f * float(M_PI) / float(section_sector_count); + + GLModel::Geometry data; + data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; + data.reserve_vertices(torus_sector_count * section_sector_count); + data.reserve_indices(torus_sector_count * section_sector_count * 2 * 3); + + // vertices + const Transform3f local_to_world_matrix = world_trafo * Geometry::translation_transform(center.cast()).cast() * + Eigen::Quaternion::FromTwoVectors(Vec3f::UnitZ(), model_axis); + for (unsigned int i = 0; i < torus_sector_count; ++i) { + const float section_angle = torus_sector_step * i; + const Vec3f radius_dir(std::cos(section_angle), std::sin(section_angle), 0.0f); + const Vec3f local_section_center = radius * radius_dir; + const Vec3f world_section_center = local_to_world_matrix * local_section_center; + const Vec3f local_section_normal = local_section_center.normalized().cross(Vec3f::UnitZ()).normalized(); + const Vec3f world_section_normal = (Vec3f)(local_to_world_matrix.matrix().block(0, 0, 3, 3) * local_section_normal).normalized(); + const Vec3f base_v = thickness * radius_dir; + for (unsigned int j = 0; j < section_sector_count; ++j) { + const Vec3f v = Eigen::AngleAxisf(section_sector_step * j, world_section_normal) * base_v; + data.add_vertex(world_section_center + v, (Vec3f)v.normalized()); + } + } + + // triangles + for (unsigned int i = 0; i < torus_sector_count; ++i) { + const unsigned int ii = i * section_sector_count; + const unsigned int ii_next = ((i + 1) % torus_sector_count) * section_sector_count; + for (unsigned int j = 0; j < section_sector_count; ++j) { + const unsigned int j_next = (j + 1) % section_sector_count; + const unsigned int i0 = ii + j; + const unsigned int i1 = ii_next + j; + const unsigned int i2 = ii_next + j_next; + const unsigned int i3 = ii + j_next; + data.add_triangle(i0, i1, i2); + data.add_triangle(i0, i2, i3); + } + } + + return data; +} + +class TransformHelper +{ + struct Cache + { + std::array viewport; + Matrix4d ndc_to_ss_matrix; + Transform3d ndc_to_ss_matrix_inverse; + }; + + static Cache s_cache; + +public: + static Vec3d model_to_world(const Vec3d& model, const Transform3d& world_matrix) { + return world_matrix * model; + } + + static Vec4d world_to_clip(const Vec3d& world, const Matrix4d& projection_view_matrix) { + return projection_view_matrix * Vec4d(world.x(), world.y(), world.z(), 1.0); + } + + static Vec3d clip_to_ndc(const Vec4d& clip) { + return Vec3d(clip.x(), clip.y(), clip.z()) / clip.w(); + } + + static Vec2d ndc_to_ss(const Vec3d& ndc, const std::array& viewport) { + const double half_w = 0.5 * double(viewport[2]); + const double half_h = 0.5 * double(viewport[3]); + return { half_w * ndc.x() + double(viewport[0]) + half_w, half_h * ndc.y() + double(viewport[1]) + half_h }; + }; + + static Vec4d model_to_clip(const Vec3d& model, const Transform3d& world_matrix, const Matrix4d& projection_view_matrix) { + return world_to_clip(model_to_world(model, world_matrix), projection_view_matrix); + } + + static Vec3d model_to_ndc(const Vec3d& model, const Transform3d& world_matrix, const Matrix4d& projection_view_matrix) { + return clip_to_ndc(world_to_clip(model_to_world(model, world_matrix), projection_view_matrix)); + } + + static Vec2d model_to_ss(const Vec3d& model, const Transform3d& world_matrix, const Matrix4d& projection_view_matrix, const std::array& viewport) { + return ndc_to_ss(clip_to_ndc(world_to_clip(model_to_world(model, world_matrix), projection_view_matrix)), viewport); + } + + static Vec2d world_to_ss(const Vec3d& world, const Matrix4d& projection_view_matrix, const std::array& viewport) { + return ndc_to_ss(clip_to_ndc(world_to_clip(world, projection_view_matrix)), viewport); + } + + static const Matrix4d& ndc_to_ss_matrix(const std::array& viewport) { + update(viewport); + return s_cache.ndc_to_ss_matrix; + } + + static const Transform3d ndc_to_ss_matrix_inverse(const std::array& viewport) { + update(viewport); + return s_cache.ndc_to_ss_matrix_inverse; + } + +private: + static void update(const std::array& viewport) { + if (s_cache.viewport == viewport) + return; + + const double half_w = 0.5 * double(viewport[2]); + const double half_h = 0.5 * double(viewport[3]); + s_cache.ndc_to_ss_matrix << half_w, 0.0, 0.0, double(viewport[0]) + half_w, + 0.0, half_h, 0.0, double(viewport[1]) + half_h, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0; + + s_cache.ndc_to_ss_matrix_inverse = s_cache.ndc_to_ss_matrix.inverse(); + s_cache.viewport = viewport; + } +}; + +TransformHelper::Cache TransformHelper::s_cache = { { 0, 0, 0, 0 }, Matrix4d::Identity(), Transform3d::Identity() }; + +GLGizmoMeasure::GLGizmoMeasure(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) +{ + GLModel::Geometry sphere_geometry = smooth_sphere(16, 7.5f); + m_sphere.mesh_raycaster = std::make_unique(std::make_shared(sphere_geometry.get_as_indexed_triangle_set())); + m_sphere.model.init_from(std::move(sphere_geometry)); + + GLModel::Geometry cylinder_geometry = smooth_cylinder(16, 5.0f, 1.0f); + m_cylinder.mesh_raycaster = std::make_unique(std::make_shared(cylinder_geometry.get_as_indexed_triangle_set())); + m_cylinder.model.init_from(std::move(cylinder_geometry)); +} + +bool GLGizmoMeasure::on_mouse(const wxMouseEvent &mouse_event) +{ + m_mouse_pos = { double(mouse_event.GetX()), double(mouse_event.GetY()) }; + + if (mouse_event.Moving()) { + // only for sure + m_mouse_left_down = false; + return false; + } + else if (mouse_event.LeftDown()) { + if (m_hover_id != -1) { + SelectedFeatures selected_features_old = m_selected_features; + m_mouse_left_down = true; + + auto item_from_feature = [this]() { + SelectedFeatures::Item item; + if (m_hover_id == SELECTION_1_ID && m_selected_features.first.feature.has_value()) + item = m_selected_features.first; + else if (m_hover_id == SELECTION_2_ID && m_selected_features.second.feature.has_value()) + item = m_selected_features.second; + else { + item = { + (m_mode == EMode::ExtendedSelection) ? point_on_feature_type_as_string(m_curr_feature->get_type(), m_hover_id) : surface_feature_type_as_string(m_curr_feature->get_type()), + (m_mode == EMode::ExtendedSelection) ? Measure::SurfaceFeature(*m_curr_point_on_feature_position) : m_curr_feature + }; + } + return item; + }; + + if (m_selected_features.first.feature.has_value()) { + auto it = std::find_if(m_selection_raycasters.begin(), m_selection_raycasters.end(), + [](std::shared_ptr item) { return SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, item->get_id()) == SELECTION_2_ID; }); + if (it != m_selection_raycasters.end()) + m_selection_raycasters.erase(it); + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, SELECTION_2_ID); + + const SelectedFeatures::Item item = item_from_feature(); + if (m_selected_features.first != item) { + if (m_selected_features.second == item) + m_selected_features.second.reset(); + else { + m_selected_features.second = item; + if (m_mode == EMode::ExtendedSelection) + m_selection_raycasters.push_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, SELECTION_2_ID, *m_sphere.mesh_raycaster)); + } + } + else { + if (!m_selected_features.second.feature.has_value()) + m_selected_features.first.reset(); + } + } + else { + const SelectedFeatures::Item item = item_from_feature(); + m_selected_features.first = item; + if (m_mode == EMode::ExtendedSelection) + m_selection_raycasters.push_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, SELECTION_1_ID, *m_sphere.mesh_raycaster)); + } + + if (m_selected_features != selected_features_old && m_selected_features.second.feature.has_value()) { + m_measurement_result = Measure::get_measurement(*m_selected_features.first.feature, *m_selected_features.second.feature, m_measuring.get()); + // transform to world coordinates + m_measurement_result.transform(m_volume_matrix); + } + + m_imgui->set_requires_extra_frame(); + + return true; + } + + // fix: prevent restart gizmo when reselect object + // take responsibility for left up + if (m_parent.get_first_hover_volume_idx() >= 0) + m_mouse_left_down = true; + } + else if (mouse_event.LeftUp()) { + if (m_mouse_left_down) { + // responsible for mouse left up after selecting plane + m_mouse_left_down = false; + return true; + } + if (m_hover_id == -1 && !m_parent.is_mouse_dragging()) + // avoid closing the gizmo if the user clicks outside of any volume + return true; + } + else if (mouse_event.RightDown() && mouse_event.CmdDown()) { + m_selected_features.reset(); + m_selection_raycasters.clear(); + m_parent.request_extra_frame(); + } + else if (mouse_event.Leaving()) + m_mouse_left_down = false; + + return false; +} + +void GLGizmoMeasure::data_changed() +{ + const Selection& selection = m_parent.get_selection(); + const ModelObject* model_object = nullptr; + const ModelVolume* model_volume = nullptr; + if (selection.is_single_full_instance() || + selection.is_from_single_object() ) { + model_object = selection.get_model()->objects[selection.get_object_idx()]; + model_volume = model_object->volumes[selection.get_first_volume()->volume_idx()]; + } + if (model_object != m_old_model_object || model_volume != m_old_model_volume) + update_if_needed(); + + m_last_inv_zoom = 0.0f; + m_last_plane_idx = -1; + if (m_pending_scale) { + m_measurement_result = Measure::get_measurement(*m_selected_features.first.feature, *m_selected_features.second.feature, m_measuring.get()); + // transform to world coordinates + m_measurement_result.transform(m_volume_matrix); + m_pending_scale = false; + } + else + m_selected_features.reset(); + m_selection_raycasters.clear(); + m_editing_distance = false; + m_is_editing_distance_first_frame = true; +} + +bool GLGizmoMeasure::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + if (action == SLAGizmoEventType::CtrlDown) { + if (m_ctrl_kar_filter.is_first()) { + if (m_curr_feature.has_value()) { + m_mode = EMode::ExtendedSelection; + disable_scene_raycasters(); + } + } + + m_ctrl_kar_filter.increase_count(); + } + else if (action == SLAGizmoEventType::CtrlUp) { + m_ctrl_kar_filter.reset_count(); + m_mode = EMode::BasicSelection; + restore_scene_raycasters_state(); + } + + return true; +} + +bool GLGizmoMeasure::on_init() +{ + m_shortcut_key = WXK_CONTROL_U; + return true; +} + +void GLGizmoMeasure::on_set_state() +{ + if (m_state == Off) { + m_ctrl_kar_filter.reset_count(); + m_curr_feature.reset(); + m_curr_point_on_feature_position.reset(); + restore_scene_raycasters_state(); + m_editing_distance = false; + m_is_editing_distance_first_frame = true; + } + else { + m_mode = EMode::BasicSelection; + // store current state of scene raycaster for later use + m_scene_raycasters.clear(); + auto scene_raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); + if (scene_raycasters != nullptr) { + m_scene_raycasters.reserve(scene_raycasters->size()); + for (auto r : *scene_raycasters) { + SceneRaycasterState state = { r, r->is_active() }; + m_scene_raycasters.emplace_back(state); + } + } + } +} + +CommonGizmosDataID GLGizmoMeasure::on_get_requirements() const +{ + return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::Raycaster)); +} + +std::string GLGizmoMeasure::on_get_name() const +{ + return _u8L("Measure"); +} + +bool GLGizmoMeasure::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + bool res = (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) ? + selection.is_single_full_instance() : + selection.is_single_volume() || selection.is_single_volume_instance(); + if (res) + res &= !selection.get_first_volume()->is_sinking(); + + return res; +} + +void GLGizmoMeasure::on_render() +{ +#if ENABLE_MEASURE_GIZMO_DEBUG + render_debug_dialog(); +#endif // ENABLE_MEASURE_GIZMO_DEBUG + +// // do not render if the user is panning/rotating the 3d scene +// if (m_parent.is_mouse_dragging()) +// return; + + const Selection& selection = m_parent.get_selection(); + + if ((wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA && selection.is_single_full_instance()) || + (selection.is_single_volume() || selection.is_single_volume_instance())) { + update_if_needed(); + + const Camera& camera = wxGetApp().plater()->get_camera(); + const float inv_zoom = (float)camera.get_inv_zoom(); + + Vec3f position_on_model; + Vec3f normal_on_model; + size_t model_facet_idx; + const bool mouse_on_object = m_c->raycaster()->raycasters().front()->unproject_on_mesh(m_mouse_pos, m_volume_matrix, camera, position_on_model, normal_on_model, nullptr, &model_facet_idx); + const bool is_hovering_on_locked_feature = m_mode == EMode::ExtendedSelection && m_hover_id != -1; + + auto update_circle = [this, inv_zoom]() { + if (m_last_inv_zoom != inv_zoom || m_last_circle != m_curr_feature) { + m_last_inv_zoom = inv_zoom; + m_last_circle = m_curr_feature; + m_circle.reset(); + const auto [center, radius, normal] = m_curr_feature->get_circle(); + GLModel::Geometry circle_geometry = init_torus_data(64, 16, center.cast(), float(radius), 5.0f * inv_zoom, normal.cast(), m_volume_matrix.cast()); + m_circle.mesh_raycaster = std::make_unique(std::make_shared(circle_geometry.get_as_indexed_triangle_set())); + m_circle.model.init_from(std::move(circle_geometry)); + return true; + } + return false; + }; + + if (m_mode == EMode::BasicSelection) { + std::optional curr_feature = mouse_on_object ? m_measuring->get_feature(model_facet_idx, position_on_model.cast()) : std::nullopt; + m_curr_point_on_feature_position.reset(); + if (m_curr_feature != curr_feature || + (curr_feature.has_value() && curr_feature->get_type() == Measure::SurfaceFeatureType::Circle && (m_curr_feature != curr_feature || m_last_inv_zoom != inv_zoom))) { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID); + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, EDGE_ID); + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, PLANE_ID); + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID); + m_raycasters.clear(); + m_curr_feature = curr_feature; + if (!m_curr_feature.has_value()) + return; + + switch (m_curr_feature->get_type()) { + default: { assert(false); break; } + case Measure::SurfaceFeatureType::Point: + { + m_raycasters.insert({ POINT_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID, *m_sphere.mesh_raycaster) }); + break; + } + case Measure::SurfaceFeatureType::Edge: + { + m_raycasters.insert({ EDGE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, EDGE_ID, *m_cylinder.mesh_raycaster) }); + if (m_curr_feature->get_extra_point().has_value()) + m_raycasters.insert({ POINT_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID, *m_sphere.mesh_raycaster) }); + break; + } + case Measure::SurfaceFeatureType::Circle: + { + update_circle(); + m_raycasters.insert({ CIRCLE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID, *m_circle.mesh_raycaster) }); + m_raycasters.insert({ POINT_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, POINT_ID, *m_sphere.mesh_raycaster) }); + break; + } + case Measure::SurfaceFeatureType::Plane: + { + const auto [idx, normal, point] = m_curr_feature->get_plane(); + if (m_last_plane_idx != idx) { + m_last_plane_idx = idx; + const indexed_triangle_set its = (m_old_model_volume != nullptr) ? m_old_model_volume->mesh().its : m_old_model_object->volumes.front()->mesh().its; + const std::vector> planes_triangles = m_measuring->get_planes_triangle_indices(); + GLModel::Geometry init_data = init_plane_data(its, planes_triangles, idx); + m_plane.reset(); + m_plane.mesh_raycaster = std::make_unique(std::make_shared(init_data.get_as_indexed_triangle_set())); + m_plane.model.init_from(std::move(init_data)); + } + + m_raycasters.insert({ PLANE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, PLANE_ID, *m_plane.mesh_raycaster) }); + break; + } + } + } + } + else if (is_hovering_on_locked_feature) { + auto position_on_feature = [this](int feature_type_id, const Camera& camera, std::function callback = nullptr) -> Vec3d { + auto it = m_raycasters.find(feature_type_id); + if (it != m_raycasters.end() && it->second != nullptr) { + Vec3f p; + Vec3f n; + const Transform3d& trafo = it->second->get_transform(); + bool res = it->second->get_raycaster()->closest_hit(m_mouse_pos, trafo, camera, p, n); + if (res) { + if (callback) + p = callback(p); + return trafo * p.cast(); + } + } + return Vec3d::Zero(); + }; + + switch (m_curr_feature->get_type()) + { + default: { assert(false); break; } + case Measure::SurfaceFeatureType::Point: + { + m_curr_point_on_feature_position = m_curr_feature->get_point(); + break; + } + case Measure::SurfaceFeatureType::Edge: + { + const std::optional extra = m_curr_feature->get_extra_point(); + if (extra.has_value() && m_hover_id == POINT_ID) + m_curr_point_on_feature_position = *extra; + else + m_curr_point_on_feature_position = m_volume_matrix.inverse() * position_on_feature(EDGE_ID, camera, [](const Vec3f& v) { return Vec3f(0.0f, 0.0f, v.z()); }); + break; + } + case Measure::SurfaceFeatureType::Plane: + { + m_curr_point_on_feature_position = m_volume_matrix.inverse() * position_on_feature(PLANE_ID, camera); + break; + } + case Measure::SurfaceFeatureType::Circle: + { + const auto [center, radius, normal] = m_curr_feature->get_circle(); + if (m_hover_id == POINT_ID) + m_curr_point_on_feature_position = center; + else { + const Vec3d world_pof = position_on_feature(CIRCLE_ID, camera, [](const Vec3f& v) { return v; }); + const Eigen::Hyperplane plane(m_volume_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()* normal, m_volume_matrix * center); + const Transform3d local_to_model_matrix = Geometry::translation_transform(center) * Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), normal); + const Vec3d local_proj = local_to_model_matrix.inverse() * m_volume_matrix.inverse() * plane.projection(world_pof); + double angle = std::atan2(local_proj.y(), local_proj.x()); + if (angle < 0.0) + angle += 2.0 * double(M_PI); + + const Vec3d local_pos = radius * Vec3d(std::cos(angle), std::sin(angle), 0.0); + m_curr_point_on_feature_position = local_to_model_matrix * local_pos; + } + break; + } + } + } + else { + if (m_curr_feature.has_value() && m_curr_feature->get_type() == Measure::SurfaceFeatureType::Circle) { + if (update_circle()) { + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID); + auto it = m_raycasters.find(CIRCLE_ID); + if (it != m_raycasters.end()) + m_raycasters.erase(it); + m_raycasters.insert({ CIRCLE_ID, m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CIRCLE_ID, *m_circle.mesh_raycaster) }); + } + } + } + + if (!m_curr_feature.has_value() && !m_selected_features.first.feature.has_value()) + return; + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + shader->start_using(); + shader->set_uniform("emission_factor", 0.25f); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + glsafe(::glEnable(GL_DEPTH_TEST)); + const bool old_cullface = ::glIsEnabled(GL_CULL_FACE); + glsafe(::glDisable(GL_CULL_FACE)); + + const Transform3d& view_matrix = camera.get_view_matrix(); + + auto set_matrix_uniforms = [shader, &view_matrix](const Transform3d& model_matrix) { + const Transform3d view_model_matrix = view_matrix * model_matrix; + shader->set_uniform("view_model_matrix", view_model_matrix); + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + }; + + auto render_feature = [this, set_matrix_uniforms](const Measure::SurfaceFeature& feature, const std::vector& colors, + float inv_zoom, bool update_raycasters_transform) { + switch (feature.get_type()) + { + default: { assert(false); break; } + case Measure::SurfaceFeatureType::Point: + { + const Vec3d position = TransformHelper::model_to_world(feature.get_point(), m_volume_matrix); + const Transform3d feature_matrix = Geometry::translation_transform(position) * Geometry::scale_transform(inv_zoom); + set_matrix_uniforms(feature_matrix); + m_sphere.model.set_color(colors.front()); + m_sphere.model.render(); + if (update_raycasters_transform) { + auto it = m_raycasters.find(POINT_ID); + if (it != m_raycasters.end() && it->second != nullptr) + it->second->set_transform(feature_matrix); + } + break; + } + case Measure::SurfaceFeatureType::Circle: + { + const auto& [center, radius, normal] = feature.get_circle(); + // render center + const Vec3d center_world = TransformHelper::model_to_world(center, m_volume_matrix); + const Transform3d center_matrix = Geometry::translation_transform(center_world) * Geometry::scale_transform(inv_zoom); + set_matrix_uniforms(center_matrix); + m_sphere.model.set_color(colors.front()); + m_sphere.model.render(); + if (update_raycasters_transform) { + auto it = m_raycasters.find(POINT_ID); + if (it != m_raycasters.end() && it->second != nullptr) + it->second->set_transform(center_matrix); + } + // render circle + const Transform3d circle_matrix = Transform3d::Identity(); + set_matrix_uniforms(circle_matrix); + if (update_raycasters_transform) { + m_circle.model.set_color(colors.back()); + m_circle.model.render(); + auto it = m_raycasters.find(CIRCLE_ID); + if (it != m_raycasters.end() && it->second != nullptr) + it->second->set_transform(circle_matrix); + } + else { + GLModel circle; + GLModel::Geometry circle_geometry = init_torus_data(64, 16, center.cast(), float(radius), 5.0f * inv_zoom, normal.cast(), m_volume_matrix.cast()); + circle.init_from(std::move(circle_geometry)); + circle.set_color(colors.back()); + circle.render(); + } + break; + } + case Measure::SurfaceFeatureType::Edge: + { + const auto& [from, to] = feature.get_edge(); + // render extra point + const std::optional extra = feature.get_extra_point(); + if (extra.has_value()) { + const Vec3d extra_world = TransformHelper::model_to_world(*extra, m_volume_matrix); + const Transform3d point_matrix = Geometry::translation_transform(extra_world) * Geometry::scale_transform(inv_zoom); + set_matrix_uniforms(point_matrix); + m_sphere.model.set_color(colors.front()); + m_sphere.model.render(); + if (update_raycasters_transform) { + auto it = m_raycasters.find(POINT_ID); + if (it != m_raycasters.end() && it->second != nullptr) + it->second->set_transform(point_matrix); + } + } + // render edge + const Vec3d from_world = TransformHelper::model_to_world(from, m_volume_matrix); + const Vec3d to_world = TransformHelper::model_to_world(to, m_volume_matrix); + const Transform3d edge_matrix = Geometry::translation_transform(from_world) * + Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), to_world - from_world) * + Geometry::scale_transform({ (double)inv_zoom, (double)inv_zoom, (to_world - from_world).norm() }); + set_matrix_uniforms(edge_matrix); + m_cylinder.model.set_color(colors.back()); + m_cylinder.model.render(); + if (update_raycasters_transform) { + auto it = m_raycasters.find(EDGE_ID); + if (it != m_raycasters.end() && it->second != nullptr) + it->second->set_transform(edge_matrix); + } + break; + } + case Measure::SurfaceFeatureType::Plane: + { + const auto& [idx, normal, pt] = feature.get_plane(); + assert(idx < m_plane_models_cache.size()); + set_matrix_uniforms(m_volume_matrix); + m_plane_models_cache[idx].set_color(colors.front()); + m_plane_models_cache[idx].render(); + if (update_raycasters_transform) { + auto it = m_raycasters.find(PLANE_ID); + if (it != m_raycasters.end() && it->second != nullptr) + it->second->set_transform(m_volume_matrix); + } + break; + } + } + }; + + auto hover_selection_color = [this]() { + return saturate(!m_selected_features.first.feature.has_value() ? SELECTED_1ST_COLOR : SELECTED_2ND_COLOR, 1.5f); + }; + + auto hovering_color = [this, hover_selection_color, &selection]() { + return (m_mode == EMode::ExtendedSelection) ? selection.get_first_volume()->render_color : hover_selection_color(); + }; + + if (m_curr_feature.has_value()) { + std::vector colors; + if (m_selected_features.first.feature.has_value() && *m_curr_feature == *m_selected_features.first.feature) + colors.emplace_back(SELECTED_1ST_COLOR); + else if (m_selected_features.second.feature.has_value() && *m_curr_feature == *m_selected_features.second.feature) + colors.emplace_back(SELECTED_2ND_COLOR); + else { + switch (m_curr_feature->get_type()) + { + default: { assert(false); break; } + case Measure::SurfaceFeatureType::Point: + { + colors.emplace_back(hover_selection_color()); + break; + } + case Measure::SurfaceFeatureType::Edge: + case Measure::SurfaceFeatureType::Circle: + { + colors.emplace_back((m_hover_id == POINT_ID) ? hover_selection_color() : hovering_color()); + colors.emplace_back(hovering_color()); + break; + } + case Measure::SurfaceFeatureType::Plane: + { + colors.emplace_back(hovering_color()); + break; + } + } + } + + render_feature(*m_curr_feature, colors, inv_zoom, true); + } + + if (m_selected_features.first.feature.has_value() && (!m_curr_feature.has_value() || *m_curr_feature != *m_selected_features.first.feature)) { + const std::vector colors = { SELECTED_1ST_COLOR }; + render_feature(*m_selected_features.first.feature, colors, inv_zoom, false); + if (m_selected_features.first.feature->get_type() == Measure::SurfaceFeatureType::Point) { + auto it = std::find_if(m_selection_raycasters.begin(), m_selection_raycasters.end(), + [](std::shared_ptr item) { return SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, item->get_id()) == SELECTION_1_ID; }); + if (it != m_selection_raycasters.end()) + (*it)->set_transform(m_volume_matrix * Geometry::translation_transform(m_selected_features.first.feature->get_point()) * Geometry::scale_transform(inv_zoom)); + } + } + if (m_selected_features.second.feature.has_value() && (!m_curr_feature.has_value() || *m_curr_feature != *m_selected_features.second.feature)) { + const std::vector colors = { SELECTED_2ND_COLOR }; + render_feature(*m_selected_features.second.feature, colors, inv_zoom, false); + if (m_selected_features.second.feature->get_type() == Measure::SurfaceFeatureType::Point) { + auto it = std::find_if(m_selection_raycasters.begin(), m_selection_raycasters.end(), + [](std::shared_ptr item) { return SceneRaycaster::decode_id(SceneRaycaster::EType::Gizmo, item->get_id()) == SELECTION_2_ID; }); + if (it != m_selection_raycasters.end()) + (*it)->set_transform(m_volume_matrix * Geometry::translation_transform(m_selected_features.second.feature->get_point()) * Geometry::scale_transform(inv_zoom)); + } + } + + if (is_hovering_on_locked_feature && m_curr_point_on_feature_position.has_value()) { + if (m_hover_id != POINT_ID) { + const Vec3d position = TransformHelper::model_to_world(*m_curr_point_on_feature_position, m_volume_matrix); + const Transform3d matrix = Geometry::translation_transform(position) * Geometry::scale_transform(inv_zoom); + set_matrix_uniforms(matrix); + m_sphere.model.set_color(hover_selection_color()); + m_sphere.model.render(); + } + } + + shader->stop_using(); + + if (old_cullface) + glsafe(::glEnable(GL_CULL_FACE)); + } + + render_dimensioning(); +} + +void GLGizmoMeasure::update_if_needed() +{ + auto update_plane_models_cache = [this](const indexed_triangle_set& its) { + m_plane_models_cache.clear(); + const std::vector> planes_triangles = m_measuring->get_planes_triangle_indices(); + for (int idx = 0; idx < (int)planes_triangles.size(); ++idx) { + m_plane_models_cache.emplace_back(GLModel()); + GLModel::Geometry init_data = init_plane_data(its, planes_triangles, idx); + m_plane_models_cache.back().init_from(std::move(init_data)); + } + }; + + auto do_update = [this, update_plane_models_cache](const ModelObject* object, const ModelVolume* volume) { + const indexed_triangle_set& its = (volume != nullptr) ? volume->mesh().its : object->volumes.front()->mesh().its; + m_measuring.reset(new Measure::Measuring(its)); + + update_plane_models_cache(its); + + // Let's save what we calculated it from: + m_volumes_matrices.clear(); + m_volumes_types.clear(); + m_first_instance_scale = Vec3d::Ones(); + m_first_instance_mirror = Vec3d::Ones(); + if (object != nullptr) { + for (const ModelVolume* vol : object->volumes) { + m_volumes_matrices.push_back(vol->get_matrix()); + m_volumes_types.push_back(vol->type()); + } + m_first_instance_scale = object->instances.front()->get_scaling_factor(); + m_first_instance_mirror = object->instances.front()->get_mirror(); + } + m_old_model_object = object; + m_old_model_volume = volume; + }; + + const Selection& selection = m_parent.get_selection(); + if (selection.is_empty()) + return; + + m_volume_matrix = selection.get_first_volume()->world_matrix(); + + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelVolume* mv = m_c->selection_info()->model_volume(); + if (m_state != On || (mo == nullptr && mv == nullptr)) + return; + + if (mo == nullptr) + mo = mv->get_object(); + + if (mo->instances.empty()) + return; + + if (!m_measuring || mo != m_old_model_object || mv != m_old_model_volume || mo->volumes.size() != m_volumes_matrices.size()) + do_update(mo, mv); + + // We want to recalculate when the scale changes - some planes could (dis)appear. + if (!mo->instances.front()->get_scaling_factor().isApprox(m_first_instance_scale) || + !mo->instances.front()->get_mirror().isApprox(m_first_instance_mirror)) + do_update(mo, mv); + + for (unsigned int i = 0; i < mo->volumes.size(); ++i) { + if (!mo->volumes[i]->get_matrix().isApprox(m_volumes_matrices[i]) || + mo->volumes[i]->type() != m_volumes_types[i]) { + do_update(mo, mv); + break; + } + } +} + +void GLGizmoMeasure::disable_scene_raycasters() +{ + for (auto r : m_scene_raycasters) { + r.raycaster->set_active(false); + } +} + +void GLGizmoMeasure::restore_scene_raycasters_state() +{ + for (auto r : m_scene_raycasters) { + r.raycaster->set_active(r.state); + } +} + +void GLGizmoMeasure::render_dimensioning() +{ + static SelectedFeatures last_selected_features; + + if (!m_selected_features.first.feature.has_value() || !m_selected_features.second.feature.has_value()) + return; + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + auto point_point = [this, shader](const Vec3d& v1, const Vec3d& v2, float distance) { + if ((v2 - v1).squaredNorm() < 0.000001 || distance < 0.001f) + return; + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Matrix4d projection_view_matrix = camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix(); + const std::array& viewport = camera.get_viewport(); + + // screen coordinates + const Vec2d v1ss = TransformHelper::world_to_ss(v1, projection_view_matrix, viewport); + const Vec2d v2ss = TransformHelper::world_to_ss(v2, projection_view_matrix, viewport); + + if (v1ss.isApprox(v2ss)) + return; + + const Vec2d v12ss = v2ss - v1ss; + const double v12ss_len = v12ss.norm(); + + const bool overlap = v12ss_len - 2.0 * TRIANGLE_HEIGHT < 0.0; + + const auto q12ss = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Vec3d(v12ss.x(), v12ss.y(), 0.0)); + const auto q21ss = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Vec3d(-v12ss.x(), -v12ss.y(), 0.0)); + + shader->set_uniform("projection_matrix", Transform3d::Identity()); + + const Vec3d v1ss_3 = { v1ss.x(), v1ss.y(), 0.0 }; + const Vec3d v2ss_3 = { v2ss.x(), v2ss.y(), 0.0 }; + + const Transform3d ss_to_ndc_matrix = TransformHelper::ndc_to_ss_matrix_inverse(viewport); + + // stem + shader->set_uniform("view_model_matrix", overlap ? + ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q12ss * Geometry::translation_transform(-2.0 * TRIANGLE_HEIGHT * Vec3d::UnitX()) * Geometry::scale_transform({ v12ss_len + 4.0 * TRIANGLE_HEIGHT, 1.0f, 1.0f }) : + ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q12ss * Geometry::scale_transform({ v12ss_len, 1.0f, 1.0f })); + m_dimensioning.line.set_color(ColorRGBA::WHITE()); + m_dimensioning.line.render(); + + // arrow 1 + shader->set_uniform("view_model_matrix", overlap ? + ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q12ss : + ss_to_ndc_matrix * Geometry::translation_transform(v1ss_3) * q21ss); + m_dimensioning.triangle.render(); + + // arrow 2 + shader->set_uniform("view_model_matrix", overlap ? + ss_to_ndc_matrix * Geometry::translation_transform(v2ss_3) * q21ss : + ss_to_ndc_matrix * Geometry::translation_transform(v2ss_3) * q12ss); + m_dimensioning.triangle.render(); + + const bool use_inches = wxGetApp().app_config->get("use_inches") == "1"; + const double curr_value = use_inches ? ObjectManipulation::mm_to_in * distance : distance; + const std::string curr_value_str = format_double(curr_value); + const std::string units = use_inches ? _u8L("in") : _u8L("mm"); + const float value_str_width = 20.0f + ImGui::CalcTextSize(curr_value_str.c_str()).x; + static double edit_value = 0.0; + + const Vec2d label_position = 0.5 * (v1ss + v2ss); + m_imgui->set_next_window_pos(label_position.x(), viewport[3] - label_position.y(), ImGuiCond_Always, 0.0f, 1.0f); + m_imgui->set_next_window_bg_alpha(0.0f); + + if (!m_editing_distance) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 1.0f, 1.0f }); + m_imgui->begin(std::string("distance"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration); + ImGui::AlignTextToFramePadding(); + m_imgui->text(curr_value_str + " " + units); + ImGui::SameLine(); + if (m_imgui->image_button(ImGui::SliderFloatEditBtnIcon, _L("Edit to scale"))) { + m_editing_distance = true; + edit_value = curr_value; + m_imgui->requires_extra_frame(); + } + m_imgui->end(); + ImGui::PopStyleVar(3); + } + + if (m_editing_distance && !ImGui::IsPopupOpen("distance_popup")) + ImGui::OpenPopup("distance_popup"); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 1.0f, 1.0f }); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 4.0f, 0.0f }); + if (ImGui::BeginPopupModal("distance_popup", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration)) { + auto perform_scale = [this](double new_value, double old_value) { + if (new_value == old_value || new_value <= 0.0) + return; + + const double ratio = new_value / old_value; + wxGetApp().plater()->take_snapshot(_L("Scale")); + + TransformationType type; + type.set_world(); + type.set_relative(); + type.set_joint(); + + // apply scale + Selection& selection = m_parent.get_selection(); + selection.setup_cache(); + selection.scale(ratio * Vec3d::Ones(), type); + wxGetApp().plater()->canvas3D()->do_scale(""); // avoid storing another snapshot + wxGetApp().obj_manipul()->set_dirty(); + + // update measure on next call to data_changed() + m_pending_scale = true; + }; + auto action_exit = [this]() { + m_editing_distance = false; + m_is_editing_distance_first_frame = true; + ImGui::CloseCurrentPopup(); + }; + auto action_scale = [perform_scale, action_exit](double new_value, double old_value) { + perform_scale(new_value, old_value); + action_exit(); + }; + + m_imgui->disable_background_fadeout_animation(); + ImGui::PushItemWidth(value_str_width); + if (ImGui::InputDouble("##distance", &edit_value, 0.0f, 0.0f, "%.3f")) { + } + + // trick to auto-select text in the input widgets on 1st frame + if (m_is_editing_distance_first_frame) { + ImGui::SetKeyboardFocusHere(0); + m_is_editing_distance_first_frame = false; + m_imgui->set_requires_extra_frame(); + } + + // handle keys input + if (ImGui::IsKeyPressedMap(ImGuiKey_Enter) || ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) + action_scale(edit_value, curr_value); + else if (ImGui::IsKeyPressedMap(ImGuiKey_Escape)) + action_exit(); + + ImGui::SameLine(); + if (m_imgui->button(_u8L("Scale"))) + action_scale(edit_value, curr_value); + ImGui::SameLine(); + if (m_imgui->button(_u8L("Cancel"))) + action_exit(); + ImGui::EndPopup(); + } + ImGui::PopStyleVar(4); + }; + + auto point_edge = [this, shader](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2) { + assert(f1.get_type() == Measure::SurfaceFeatureType::Point && f2.get_type() == Measure::SurfaceFeatureType::Edge); + std::pair e = f2.get_edge(); + // Transform to world coordinates + e.first = TransformHelper::model_to_world(e.first, m_volume_matrix); + e.second = TransformHelper::model_to_world(e.second, m_volume_matrix); + + const Vec3d v_proj = m_measurement_result.distance_infinite->to; + + const Vec3d e1e2 = e.second - e.first; + const Vec3d v_proje1 = v_proj - e.first; + const bool on_e1_side = v_proje1.dot(e1e2) < -EPSILON; + const bool on_e2_side = !on_e1_side && v_proje1.norm() > e1e2.norm(); + if (on_e1_side || on_e2_side) { + const Camera& camera = wxGetApp().plater()->get_camera(); + const Matrix4d projection_view_matrix = camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix(); + const std::array& viewport = camera.get_viewport(); + const Transform3d ss_to_ndc_matrix = TransformHelper::ndc_to_ss_matrix_inverse(viewport); + + const Vec2d v_projss = TransformHelper::world_to_ss(v_proj, projection_view_matrix, viewport); + auto render_extension = [this, &v_projss, &projection_view_matrix, &viewport, &ss_to_ndc_matrix, shader](const Vec3d& p) { + const Vec2d pss = TransformHelper::world_to_ss(p, projection_view_matrix, viewport); + if (!pss.isApprox(v_projss)) { + const Vec2d pv_projss = v_projss - pss; + const double pv_projss_len = pv_projss.norm(); + + const auto q = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Vec3d(pv_projss.x(), pv_projss.y(), 0.0)); + + shader->set_uniform("projection_matrix", Transform3d::Identity()); + shader->set_uniform("view_model_matrix", ss_to_ndc_matrix * Geometry::translation_transform({ pss.x(), pss.y(), 0.0 }) * q * + Geometry::scale_transform({ pv_projss_len, 1.0f, 1.0f })); + m_dimensioning.line.set_color(ColorRGBA::LIGHT_GRAY()); + m_dimensioning.line.render(); + } + }; + + render_extension(on_e1_side ? e.first : e.second); + } + }; + + auto arc_edge_edge = [this, shader](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2, double radius = 0.0) { + assert(f1.get_type() == Measure::SurfaceFeatureType::Edge && f2.get_type() == Measure::SurfaceFeatureType::Edge); + if (!m_measurement_result.angle.has_value()) + return; + + const double angle = m_measurement_result.angle->angle; + const Vec3d center = m_measurement_result.angle->center; + const std::pair e1 = m_measurement_result.angle->e1; + const std::pair e2 = m_measurement_result.angle->e2; + const double calc_radius = m_measurement_result.angle->radius; + const bool coplanar = m_measurement_result.angle->coplanar; + + if (std::abs(angle) < EPSILON || std::abs(calc_radius) < EPSILON) + return; + + const double draw_radius = (radius > 0.0) ? radius : calc_radius; + + const Vec3d e1_unit = Measure::edge_direction(e1); + const Vec3d e2_unit = Measure::edge_direction(e2); + + const unsigned int resolution = std::max(2, 64 * angle / double(PI)); + const double step = angle / double(resolution); + const Vec3d normal = e1_unit.cross(e2_unit).normalized(); + + if (!m_dimensioning.arc.is_initialized()) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = ColorRGBA::WHITE(); + init_data.reserve_vertices(resolution + 1); + init_data.reserve_indices(resolution + 1); + + // vertices + indices + for (unsigned int i = 0; i <= resolution; ++i) { + const double a = step * double(i); + const Vec3d v = draw_radius * (Eigen::Quaternion(Eigen::AngleAxisd(a, normal)) * e1_unit); + init_data.add_vertex((Vec3f)v.cast()); + init_data.add_index(i); + } + + m_dimensioning.arc.init_from(std::move(init_data)); + } + + // arc + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::translation_transform(center)); + m_dimensioning.arc.render(); + + // arrows + auto render_arrow = [this, shader, &camera, &normal, ¢er, &e1_unit, draw_radius, step, resolution](unsigned int endpoint_id) { + const double angle = (endpoint_id == 1) ? 0.0 : step * double(resolution); + const Vec3d position_model = Geometry::translation_transform(center) * (draw_radius * (Eigen::Quaternion(Eigen::AngleAxisd(angle, normal)) * e1_unit)); + const Vec3d direction_model = (endpoint_id == 1) ? -normal.cross(position_model - center).normalized() : normal.cross(position_model - center).normalized(); + const auto qz = Eigen::Quaternion::FromTwoVectors(Vec3d::UnitZ(), normal); + const auto qx = Eigen::Quaternion::FromTwoVectors(qz * Vec3d::UnitX(), direction_model); + const Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::translation_transform(position_model) * + qx * qz * Geometry::scale_transform(camera.get_inv_zoom()); + shader->set_uniform("view_model_matrix", view_model_matrix); + m_dimensioning.triangle.render(); + }; + + glsafe(::glDisable(GL_CULL_FACE)); + render_arrow(1); + render_arrow(2); + glsafe(::glEnable(GL_CULL_FACE)); + + // edge 1 extension + const Vec3d e11e12 = e1.second - e1.first; + const Vec3d e11center = center - e1.first; + const double e11center_len = e11center.norm(); + if (e11center_len > EPSILON && e11center.dot(e11e12) < 0.0) { + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::translation_transform(center) * + Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Measure::edge_direction(e1.first, e1.second)) * + Geometry::scale_transform({ e11center_len, 1.0f, 1.0f })); + m_dimensioning.line.set_color(ColorRGBA::LIGHT_GRAY()); + m_dimensioning.line.render(); + } + + // edge 2 extension + const Vec3d e21center = center - e2.first; + const double e21center_len = e21center.norm(); + if (e21center_len > EPSILON) { + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::translation_transform(center) * + Eigen::Quaternion::FromTwoVectors(Vec3d::UnitX(), Measure::edge_direction(e2.first, e2.second)) * + Geometry::scale_transform({ (coplanar && radius > 0.0) ? e21center_len : draw_radius, 1.0f, 1.0f })); + m_dimensioning.line.set_color(ColorRGBA::LIGHT_GRAY()); + m_dimensioning.line.render(); + } + + // label + // label world coordinates + const Vec3d label_position_world = Geometry::translation_transform(center) * (draw_radius * (Eigen::Quaternion(Eigen::AngleAxisd(step * 0.5 * double(resolution), normal)) * e1_unit)); + + // label screen coordinates + const std::array& viewport = camera.get_viewport(); + const Vec2d label_position_ss = TransformHelper::world_to_ss(label_position_world, + camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix(), viewport); + + m_imgui->set_next_window_pos(label_position_ss.x(), viewport[3] - label_position_ss.y(), ImGuiCond_Always, 0.0f, 1.0f); + m_imgui->set_next_window_bg_alpha(0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + m_imgui->begin(_L("##angle"), ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + m_imgui->text(format_double(Geometry::rad2deg(angle)) + "°"); + m_imgui->end(); + ImGui::PopStyleVar(); + }; + + auto arc_edge_plane = [this, arc_edge_edge](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2) { + assert(f1.get_type() == Measure::SurfaceFeatureType::Edge && f2.get_type() == Measure::SurfaceFeatureType::Plane); + if (!m_measurement_result.angle.has_value()) + return; + + const std::pair e1 = m_measurement_result.angle->e1; + const std::pair e2 = m_measurement_result.angle->e2; + const double calc_radius = m_measurement_result.angle->radius; + + if (calc_radius == 0.0) + return; + + arc_edge_edge(Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e1.first, e1.second), + Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e2.first, e2.second), calc_radius); + }; + + auto arc_plane_plane = [this, arc_edge_edge](const Measure::SurfaceFeature& f1, const Measure::SurfaceFeature& f2) { + assert(f1.get_type() == Measure::SurfaceFeatureType::Plane && f2.get_type() == Measure::SurfaceFeatureType::Plane); + if (!m_measurement_result.angle.has_value()) + return; + + const std::pair e1 = m_measurement_result.angle->e1; + const std::pair e2 = m_measurement_result.angle->e2; + const double calc_radius = m_measurement_result.angle->radius; + + if (calc_radius == 0.0) + return; + + arc_edge_edge(Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e1.first, e1.second), + Measure::SurfaceFeature(Measure::SurfaceFeatureType::Edge, e2.first, e2.second), calc_radius); + }; + + shader->start_using(); + + if (!m_dimensioning.line.is_initialized()) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = ColorRGBA::WHITE(); + init_data.reserve_vertices(2); + init_data.reserve_indices(2); + + // vertices + init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f)); + init_data.add_vertex(Vec3f(1.0f, 0.0f, 0.0f)); + + // indices + init_data.add_line(0, 1); + + m_dimensioning.line.init_from(std::move(init_data)); + } + + if (!m_dimensioning.triangle.is_initialized()) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = ColorRGBA::WHITE(); + init_data.reserve_vertices(3); + init_data.reserve_indices(3); + + // vertices + init_data.add_vertex(Vec3f(0.0f, 0.0f, 0.0f)); + init_data.add_vertex(Vec3f(-TRIANGLE_HEIGHT, 0.5f * TRIANGLE_BASE, 0.0f)); + init_data.add_vertex(Vec3f(-TRIANGLE_HEIGHT, -0.5f * TRIANGLE_BASE, 0.0f)); + + // indices + init_data.add_triangle(0, 1, 2); + + m_dimensioning.triangle.init_from(std::move(init_data)); + } + + if (last_selected_features != m_selected_features) + m_dimensioning.arc.reset(); + + glsafe(::glDisable(GL_DEPTH_TEST)); + + if (m_selected_features.second.feature.has_value()) { + const bool has_distance = m_measurement_result.has_distance_data(); + + const Measure::SurfaceFeature* f1 = &(*m_selected_features.first.feature); + const Measure::SurfaceFeature* f2 = &(*m_selected_features.second.feature); + Measure::SurfaceFeatureType ft1 = f1->get_type(); + Measure::SurfaceFeatureType ft2 = f2->get_type(); + + // Order features by type so following conditions are simple. + if (ft1 > ft2) { + std::swap(ft1, ft2); + std::swap(f1, f2); + } + + // If there is an angle to show, draw the arc: + if (ft1 == Measure::SurfaceFeatureType::Edge && ft2 == Measure::SurfaceFeatureType::Edge) + arc_edge_edge(*f1, *f2); + else if (ft1 == Measure::SurfaceFeatureType::Edge && ft2 == Measure::SurfaceFeatureType::Plane) + arc_edge_plane(*f1, *f2); + else if (ft1 == Measure::SurfaceFeatureType::Plane && ft2 == Measure::SurfaceFeatureType::Plane) + arc_plane_plane(*f1, *f2); + + if (has_distance){ + // Where needed, draw the extension of the edge to where the dist is measured: + if (ft1 == Measure::SurfaceFeatureType::Point && ft2 == Measure::SurfaceFeatureType::Edge) + point_edge(*f1, *f2); + + // Render the arrow between the points that the backend passed: + const Measure::DistAndPoints& dap = m_measurement_result.distance_infinite.has_value() + ? *m_measurement_result.distance_infinite + : *m_measurement_result.distance_strict; + point_point(dap.from, dap.to, dap.dist); + } + } + + glsafe(::glEnable(GL_DEPTH_TEST)); + + shader->stop_using(); +} + +static void add_row_to_table(std::function col_1 = nullptr, std::function col_2 = nullptr) +{ + assert(col_1 != nullptr && col_2 != nullptr); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + col_1(); + ImGui::TableSetColumnIndex(1); + col_2(); +} + +static void add_strings_row_to_table(ImGuiWrapper& imgui, const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color) +{ + add_row_to_table([&]() { imgui.text_colored(col_1_color, col_1); }, [&]() { imgui.text_colored(col_2_color, col_2); }); +}; + +#if ENABLE_MEASURE_GIZMO_DEBUG +void GLGizmoMeasure::render_debug_dialog() +{ + auto add_feature_data = [this](const SelectedFeatures::Item& item) { + add_strings_row_to_table(*m_imgui, "Type", ImGuiWrapper::COL_ORANGE_LIGHT, item.source, ImGui::GetStyleColorVec4(ImGuiCol_Text)); + switch (item.feature->get_type()) + { + case Measure::SurfaceFeatureType::Point: + { + add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(item.feature->get_point()), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + break; + } + case Measure::SurfaceFeatureType::Edge: + { + auto [from, to] = item.feature->get_edge(); + add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(from), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + add_strings_row_to_table(*m_imgui, "m_pt2", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(to), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + break; + } + case Measure::SurfaceFeatureType::Plane: + { + auto [idx, normal, origin] = item.feature->get_plane(); + add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(normal), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + add_strings_row_to_table(*m_imgui, "m_pt2", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(origin), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + add_strings_row_to_table(*m_imgui, "m_value", ImGuiWrapper::COL_ORANGE_LIGHT, format_double(idx), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + break; + } + case Measure::SurfaceFeatureType::Circle: + { + auto [center, radius, normal] = item.feature->get_circle(); + add_strings_row_to_table(*m_imgui, "m_pt1", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(center), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + add_strings_row_to_table(*m_imgui, "m_pt2", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(normal), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + add_strings_row_to_table(*m_imgui, "m_value", ImGuiWrapper::COL_ORANGE_LIGHT, format_double(radius), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + break; + } + } + std::optional extra_point = item.feature->get_extra_point(); + if (extra_point.has_value()) + add_strings_row_to_table(*m_imgui, "m_pt3", ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(*extra_point), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + }; + + m_imgui->begin(_L("Measure tool debug"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + if (!m_selected_features.first.feature.has_value() && !m_selected_features.second.feature.has_value()) + m_imgui->text("Empty selection"); + else { + const ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersH; + if (m_selected_features.first.feature.has_value()) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Selection 1"); + if (ImGui::BeginTable("Selection 1", 2, flags)) { + add_feature_data(m_selected_features.first); + ImGui::EndTable(); + } + } + if (m_selected_features.second.feature.has_value()) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, "Selection 2"); + if (ImGui::BeginTable("Selection 2", 2, flags)) { + add_feature_data(m_selected_features.second); + ImGui::EndTable(); + } + } + } + m_imgui->end(); +} +#endif // ENABLE_MEASURE_GIZMO_DEBUG + +void GLGizmoMeasure::on_render_input_window(float x, float y, float bottom_limit) +{ + static std::optional last_feature; + static EMode last_mode = EMode::BasicSelection; + static SelectedFeatures last_selected_features; + + static float last_y = 0.0f; + static float last_h = 0.0f; + + if (m_editing_distance) + return; + + m_imgui->begin(get_name(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + const float win_h = ImGui::GetWindowHeight(); + y = std::min(y, bottom_limit - win_h); + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + if (last_h != win_h || last_y != y) { + // ask canvas for another frame to render the window in the correct position + m_imgui->set_requires_extra_frame(); + if (last_h != win_h) + last_h = win_h; + if (last_y != y) + last_y = y; + } + + if (ImGui::BeginTable("Commands", 2)) { + unsigned int row_count = 1; + add_row_to_table( + [this]() { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Left mouse button")); + }, + [this]() { + std::string text; + ColorRGBA color; + if (m_selected_features.second.feature.has_value()) { + if (m_selected_features.second.feature == m_curr_feature && m_mode == EMode::BasicSelection) + text = _u8L("Unselect feature"); + else if (m_hover_id == SELECTION_2_ID) + text = _u8L("Unselect point"); + else + text = (m_mode == EMode::BasicSelection) ? _u8L("Select feature") : _u8L("Select point"); + color = SELECTED_2ND_COLOR; + } + else { + if (m_selected_features.first.feature.has_value()) { + if (m_selected_features.first.feature == m_curr_feature) + text = _u8L("Unselect feature"); + else if (m_hover_id == SELECTION_1_ID) + text = _u8L("Unselect point"); + color = SELECTED_1ST_COLOR; + } + if (text.empty()) { + text = (m_mode == EMode::BasicSelection) ? _u8L("Select feature") : _u8L("Select point"); + color = m_selected_features.first.feature.has_value() ? SELECTED_2ND_COLOR : SELECTED_1ST_COLOR; + } + } + + m_imgui->text_colored(ImGui::GetStyleColorVec4(ImGuiCol_Text), text); + ImGui::SameLine(); + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const float rect_size = ImGui::GetTextLineHeight(); + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(pos.x + 1.0f, pos.y + 1.0f), ImVec2(pos.x + rect_size, pos.y + rect_size), ImGuiWrapper::to_ImU32(color)); + ImGui::Dummy(ImVec2(rect_size, rect_size)); + } + ); + + if (m_selected_features.first.feature.has_value()) { + add_strings_row_to_table(*m_imgui, CTRL_STR + "+" + _u8L("Right mouse button"), ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Restart selection"), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ++row_count; + } + + if (m_mode == EMode::BasicSelection && m_hover_id != -1) { + add_strings_row_to_table(*m_imgui, CTRL_STR, ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Enable point selection"), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ++row_count; + } + + // add dummy rows to keep dialog size fixed + for (unsigned int i = row_count; i < 3; ++i) { + add_strings_row_to_table(*m_imgui, " ", ImGuiWrapper::COL_ORANGE_LIGHT, " ", ImGui::GetStyleColorVec4(ImGuiCol_Text)); + } + + ImGui::EndTable(); + } + + const bool use_inches = wxGetApp().app_config->get("use_inches") == "1"; + const std::string units = use_inches ? " " + _u8L("in") : " " + _u8L("mm"); + + //const Measure::SurfaceFeatureType feature_type = m_curr_feature.has_value() ? m_curr_feature->get_type() : Measure::SurfaceFeatureType::Undef; + //bool data_text_set = false; + //ImGui::Separator(); + //if (feature_type != Measure::SurfaceFeatureType::Undef) { + // if (m_mode == EMode::BasicSelection) { + // m_imgui->text(surface_feature_type_as_string(feature_type)); + // data_text_set = true; + // } + // else if (m_mode == EMode::ExtendedSelection) { + // if (m_hover_id != -1 && m_curr_point_on_feature_position.has_value()) { + // m_imgui->text(point_on_feature_type_as_string(feature_type, m_hover_id)); + // data_text_set = true; + // } + // } + //} + //if (!data_text_set) + // m_imgui->text(_u8L("No feature")); + + //const unsigned int max_data_row_count = 3; + //unsigned int data_row_count = 0; + //if (ImGui::BeginTable("Data", 2)) { + // if (m_mode == EMode::BasicSelection) { + // switch (feature_type) + // { + // default: { break; } + // case Measure::SurfaceFeatureType::Point: + // { + // Vec3d position = m_volume_matrix * m_curr_feature->get_point(); + // if (use_inches) + // position = ObjectManipulation::mm_to_in * position; + // add_strings_row_to_table(*m_imgui, _u8L("Position"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(position), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // data_row_count = 1; + // break; + // } + // case Measure::SurfaceFeatureType::Edge: + // { + // auto [from, to] = m_curr_feature->get_edge(); + // from = m_volume_matrix * from; + // to = m_volume_matrix * to; + // if (use_inches) { + // from = ObjectManipulation::mm_to_in * from; + // to = ObjectManipulation::mm_to_in * to; + // } + // add_strings_row_to_table(*m_imgui, _u8L("From"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(from), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // add_strings_row_to_table(*m_imgui, _u8L("To"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(to), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // add_strings_row_to_table(*m_imgui, _u8L("Length"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double((to - from).norm()) + units, ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // data_row_count = 3; + // break; + // } + // case Measure::SurfaceFeatureType::Circle: + // { + // auto [center, radius, normal] = m_curr_feature->get_circle(); + // // generic point on circle, used to recalculate radius after transformation + // const Vec3d on_circle = m_volume_matrix * (center + radius * Measure::get_orthogonal(normal, true)); + // center = m_volume_matrix * center; + // normal = (m_volume_matrix.matrix().block(0, 0, 3, 3).inverse().transpose() * normal).normalized(); + // radius = (on_circle - center).norm(); + // if (use_inches) { + // center = ObjectManipulation::mm_to_in * center; + // radius = ObjectManipulation::mm_to_in * radius; + // } + // add_strings_row_to_table(*m_imgui, _u8L("Center"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(center), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // add_strings_row_to_table(*m_imgui, _u8L("Radius"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(radius) + units, ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // add_strings_row_to_table(*m_imgui, _u8L("Normal"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(normal), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // data_row_count = 3; + // break; + // } + // case Measure::SurfaceFeatureType::Plane: + // { + // auto [idx, normal, origin] = m_curr_feature->get_plane(); + // origin = m_volume_matrix * origin; + // normal = m_volume_matrix.matrix().block(0, 0, 3, 3).inverse().transpose() * normal; + // if (use_inches) + // origin = ObjectManipulation::mm_to_in * origin; + // add_strings_row_to_table(*m_imgui, _u8L("Origin"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(origin), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // add_strings_row_to_table(*m_imgui, _u8L("Normal"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(normal), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // data_row_count = 2; + // break; + // } + // } + // } + // else { + // if (m_hover_id != -1 && m_curr_point_on_feature_position.has_value()) { + // Vec3d position = m_volume_matrix * *m_curr_point_on_feature_position; + // if (use_inches) + // position = ObjectManipulation::mm_to_in * position; + // add_strings_row_to_table(*m_imgui, _u8L("Position"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(position), ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // data_row_count = 1; + // } + // } + + // // add dummy rows to keep dialog size fixed + // for (unsigned int i = data_row_count; i < max_data_row_count; ++i) { + // add_strings_row_to_table(*m_imgui, " ", ImGuiWrapper::COL_ORANGE_LIGHT, " ", ImGui::GetStyleColorVec4(ImGuiCol_Text)); + // } + // ImGui::EndTable(); + //} + + ImGui::Separator(); + const ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersH; + if (ImGui::BeginTable("Selection", 2, flags)) { + add_strings_row_to_table(*m_imgui, _u8L("Selection") + " 1:", ImGuiWrapper::to_ImVec4(SELECTED_1ST_COLOR), m_selected_features.first.feature.has_value() ? + m_selected_features.first.source : _u8L("None"), ImGuiWrapper::to_ImVec4(SELECTED_1ST_COLOR)); + add_strings_row_to_table(*m_imgui, _u8L("Selection") + " 2:", ImGuiWrapper::to_ImVec4(SELECTED_2ND_COLOR), m_selected_features.second.feature.has_value() ? + m_selected_features.second.source : _u8L("None"), ImGuiWrapper::to_ImVec4(SELECTED_2ND_COLOR)); + ImGui::EndTable(); + } + + //if (m_selected_features.first.feature.has_value()) { + // if (m_imgui->button(_u8L("Restart"))) { + // m_selected_features.reset(); + // m_selection_raycasters.clear(); + // m_imgui->set_requires_extra_frame(); + // } + //} + + auto add_measure_row_to_table = [this](const std::string& col_1, const ImVec4& col_1_color, const std::string& col_2, const ImVec4& col_2_color) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + m_imgui->text_colored(col_1_color, col_1); + ImGui::TableSetColumnIndex(1); + m_imgui->text_colored(col_2_color, col_2); + ImGui::TableSetColumnIndex(2); + if (m_imgui->image_button(ImGui::ClipboardBtnIcon, _L("Copy to clipboard"))) { + wxTheClipboard->Open(); + wxTheClipboard->SetData(new wxTextDataObject(col_1 + ": " + col_2)); + wxTheClipboard->Close(); + } + }; + + ImGui::Separator(); + m_imgui->text(_u8L("Measure")); + + const unsigned int max_measure_row_count = 2; + unsigned int measure_row_count = 0; + if (ImGui::BeginTable("Measure", 4)) { + if (m_selected_features.second.feature.has_value()) { + const Measure::MeasurementResult& measure = m_measurement_result; + if (measure.angle.has_value()) { + ImGui::PushID("ClipboardAngle"); + add_measure_row_to_table(_u8L("Angle"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(Geometry::rad2deg(measure.angle->angle)) + "°", + ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ++measure_row_count; + ImGui::PopID(); + } + if (measure.distance_infinite.has_value()) { + double distance = measure.distance_infinite->dist; + if (use_inches) + distance = ObjectManipulation::mm_to_in * distance; + ImGui::PushID("ClipboardDistanceInfinite"); + add_measure_row_to_table(_u8L("Distance Infinite"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(distance) + units, + ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ++measure_row_count; + ImGui::PopID(); + } + if (measure.distance_strict.has_value() && + (!measure.distance_infinite.has_value() || std::abs(measure.distance_strict->dist - measure.distance_infinite->dist) > EPSILON)) { + double distance = measure.distance_strict->dist; + if (use_inches) + distance = ObjectManipulation::mm_to_in * distance; + ImGui::PushID("ClipboardDistanceStrict"); + add_measure_row_to_table(_u8L("Distance Strict"), ImGuiWrapper::COL_ORANGE_LIGHT, format_double(distance) + units, + ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ++measure_row_count; + ImGui::PopID(); + } + if (measure.distance_xyz.has_value() && measure.distance_xyz->norm() > EPSILON) { + Vec3d distance = *measure.distance_xyz; + if (use_inches) + distance = ObjectManipulation::mm_to_in * distance; + ImGui::PushID("ClipboardDistanceXYZ"); + add_measure_row_to_table(_u8L("Distance XYZ"), ImGuiWrapper::COL_ORANGE_LIGHT, format_vec3(distance), + ImGui::GetStyleColorVec4(ImGuiCol_Text)); + ++measure_row_count; + ImGui::PopID(); + } + } + + // add dummy rows to keep dialog size fixed + for (unsigned int i = measure_row_count; i < max_measure_row_count; ++i) { + add_strings_row_to_table(*m_imgui, " ", ImGuiWrapper::COL_ORANGE_LIGHT, " ", ImGui::GetStyleColorVec4(ImGuiCol_Text)); + } + ImGui::EndTable(); + } + + if (last_feature != m_curr_feature || last_mode != m_mode || last_selected_features != m_selected_features) { + // the dialog may have changed its size, ask for an extra frame to render it properly + last_feature = m_curr_feature; + last_mode = m_mode; + last_selected_features = m_selected_features; + m_imgui->set_requires_extra_frame(); + } + + m_imgui->end(); +} + +void GLGizmoMeasure::on_register_raycasters_for_picking() +{ + // the features are rendered on top of the scene, so the raytraced picker should take it into account + m_parent.set_raycaster_gizmos_on_top(true); +} + +void GLGizmoMeasure::on_unregister_raycasters_for_picking() +{ + m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); + m_parent.set_raycaster_gizmos_on_top(false); + m_raycasters.clear(); + m_selection_raycasters.clear(); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp new file mode 100644 index 0000000000..4426457ff8 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp @@ -0,0 +1,164 @@ +#ifndef slic3r_GLGizmoMeasure_hpp_ +#define slic3r_GLGizmoMeasure_hpp_ + +#include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLModel.hpp" +#include "slic3r/GUI/GUI_Utils.hpp" +#include "libslic3r/Measure.hpp" + +namespace Slic3r { + +class ModelVolume; + +enum class ModelVolumeType : int; + +namespace Measure { class Measuring; } + + +namespace GUI { + +enum class SLAGizmoEventType : unsigned char; + +class GLGizmoMeasure : public GLGizmoBase +{ + enum class EMode : unsigned char + { + BasicSelection, + ExtendedSelection + }; + + struct SelectedFeatures + { + struct Item + { + std::string source; + std::optional feature; + + bool operator == (const Item& other) const { + if (this->source != other.source) return false; + return this->feature == other.feature; + } + + bool operator != (const Item& other) const { + return !operator == (other); + } + + void reset() { + source.clear(); + feature.reset(); + } + }; + + Item first; + Item second; + + void reset() { + first.reset(); + second.reset(); + } + + bool operator == (const SelectedFeatures & other) const { + if (this->first != other.first) return false; + return this->second == other.second; + } + + bool operator != (const SelectedFeatures & other) const { + return !operator == (other); + } + }; + + EMode m_mode{ EMode::BasicSelection }; + Measure::MeasurementResult m_measurement_result; + + std::unique_ptr m_measuring; // PIMPL + + PickingModel m_sphere; + PickingModel m_cylinder; + PickingModel m_circle; + PickingModel m_plane; + struct Dimensioning + { + GLModel line; + GLModel triangle; + GLModel arc; + }; + Dimensioning m_dimensioning; + + Transform3d m_volume_matrix{ Transform3d::Identity() }; + std::vector m_plane_models_cache; + std::map> m_raycasters; + std::vector> m_selection_raycasters; + std::optional m_curr_feature; + std::optional m_curr_point_on_feature_position; + struct SceneRaycasterState + { + std::shared_ptr raycaster{ nullptr }; + bool state{true}; + + }; + std::vector m_scene_raycasters; + + // These hold information to decide whether recalculation is necessary: + std::vector m_volumes_matrices; + std::vector m_volumes_types; + Vec3d m_first_instance_scale{ Vec3d::Ones() }; + Vec3d m_first_instance_mirror{ Vec3d::Ones() }; + float m_last_inv_zoom{ 0.0f }; + std::optional m_last_circle; + int m_last_plane_idx{ -1 }; + + bool m_mouse_left_down{ false }; // for detection left_up of this gizmo + const ModelObject* m_old_model_object{ nullptr }; + const ModelVolume* m_old_model_volume{ nullptr }; + + Vec2d m_mouse_pos{ Vec2d::Zero() }; + + KeyAutoRepeatFilter m_ctrl_kar_filter; + + SelectedFeatures m_selected_features; + bool m_pending_scale{ false }; + bool m_editing_distance{ false }; + bool m_is_editing_distance_first_frame{ true }; + + void update_if_needed(); + + void disable_scene_raycasters(); + void restore_scene_raycasters_state(); + + void render_dimensioning(); + +#if ENABLE_MEASURE_GIZMO_DEBUG + void render_debug_dialog(); +#endif // ENABLE_MEASURE_GIZMO_DEBUG + +public: + GLGizmoMeasure(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + + /// + /// Apply rotation on select plane + /// + /// Keep information about mouse click + /// Return True when use the information otherwise False. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + void data_changed() override; + + bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + +protected: + bool on_init() override; + std::string on_get_name() const override; + bool on_is_activable() const override; + void on_render() override; + void on_set_state() override; + CommonGizmosDataID on_get_requirements() const override; + + virtual void on_render_input_window(float x, float y, float bottom_limit) override; + virtual void on_register_raycasters_for_picking() override; + virtual void on_unregister_raycasters_for_picking() override; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoMeasure_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 579cbbe675..caceefff0f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -148,8 +148,8 @@ void GLGizmoSlaSupports::on_register_raycasters_for_picking() if (m_editing_mode && !m_editing_cache.empty()) { for (size_t i = 0; i < m_editing_cache.size(); ++i) { - m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_sphere.mesh_raycaster, Transform3d::Identity()), - m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cone.mesh_raycaster, Transform3d::Identity())); + m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_sphere.mesh_raycaster), + m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i, *m_cone.mesh_raycaster)); } update_raycasters_for_picking_transform(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index 9734f1b77f..394e879b7c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -119,21 +119,25 @@ void SelectionInfo::on_update() const Selection& selection = get_pool()->get_canvas()->get_selection(); if (selection.is_single_full_instance()) { m_model_object = selection.get_model()->objects[selection.get_object_idx()]; + m_model_volume = nullptr; m_z_shift = selection.get_first_volume()->get_sla_shift_z(); } - else + else { m_model_object = nullptr; + if (selection.is_single_volume()) + m_model_volume = selection.get_model()->objects[selection.get_object_idx()]->volumes[selection.get_first_volume()->volume_idx()]; + } } void SelectionInfo::on_release() { m_model_object = nullptr; + m_model_volume = nullptr; } int SelectionInfo::get_active_instance() const { - const Selection& selection = get_pool()->get_canvas()->get_selection(); - return selection.get_instance_idx(); + return get_pool()->get_canvas()->get_selection().get_instance_idx(); } @@ -329,12 +333,18 @@ void Raycaster::on_update() { wxBusyCursor wait; const ModelObject* mo = get_pool()->selection_info()->model_object(); + const ModelVolume* mv = get_pool()->selection_info()->model_volume(); - if (! mo) + if (mo == nullptr && mv == nullptr) return; + std::vector mvs; + if (mv != nullptr) + mvs.push_back(const_cast(mv)); + else + mvs = mo->volumes; + std::vector meshes; - const std::vector& mvs = mo->volumes; if (mvs.size() == 1) { assert(mvs.front()->is_model_part()); const HollowedMesh* hollowed_mesh_tracker = get_pool()->hollowed_mesh(); @@ -342,9 +352,9 @@ void Raycaster::on_update() meshes.push_back(hollowed_mesh_tracker->get_hollowed_mesh()); } if (meshes.empty()) { - for (const ModelVolume* mv : mvs) { - if (mv->is_model_part()) - meshes.push_back(&mv->mesh()); + for (const ModelVolume* v : mvs) { + if (v->is_model_part()) + meshes.push_back(&v->mesh()); } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index f8ead27f9e..7579402261 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -10,7 +10,7 @@ namespace Slic3r { class ModelObject; - +class ModelVolume; namespace GUI { @@ -24,6 +24,8 @@ enum class SLAGizmoEventType : unsigned char { Dragging, Delete, SelectAll, + CtrlDown, + CtrlUp, ShiftUp, AltUp, ApplyChanges, @@ -153,7 +155,10 @@ public: explicit SelectionInfo(CommonGizmosDataPool* cgdp) : CommonGizmosDataBase(cgdp) {} + // Returns a non-null pointer if the selection is a single full instance ModelObject* model_object() const { return m_model_object; } + // Returns a non-null pointer if the selection is a single volume + ModelVolume* model_volume() const { return m_model_volume; } int get_active_instance() const; float get_sla_shift() const { return m_z_shift; } @@ -163,6 +168,7 @@ protected: private: ModelObject* m_model_object = nullptr; + ModelVolume* m_model_volume = nullptr; // int m_active_inst = -1; float m_z_shift = 0.f; }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index c66191d102..3da12ee87a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -22,6 +22,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp" #include "libslic3r/format.hpp" #include "libslic3r/Model.hpp" @@ -106,6 +107,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, "fdm_supports.svg", 7)); m_gizmos.emplace_back(new GLGizmoSeam(m_parent, "seam.svg", 8)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 9)); + m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 10)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent)); m_gizmos.emplace_back(new GLGizmoSimplify(m_parent)); @@ -290,6 +292,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[Seam].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MmuSegmentation) return dynamic_cast(m_gizmos[MmuSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == Measure) + return dynamic_cast(m_gizmos[Measure].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Cut) return dynamic_cast(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else @@ -497,8 +501,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) bool processed = false; - if ((evt.GetModifiers() & ctrlMask) != 0) - { + if ((evt.GetModifiers() & ctrlMask) != 0) { switch (keyCode) { #ifdef __APPLE__ @@ -516,15 +519,13 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) } } } - else if (!evt.HasModifiers()) - { + else if (!evt.HasModifiers()) { switch (keyCode) { // key ESC case WXK_ESCAPE: { - if (m_current != Undefined) - { + if (m_current != Undefined) { if ((m_current != SlaSupports) || !gizmo_event(SLAGizmoEventType::DiscardChanges)) reset_all_states(); @@ -561,8 +562,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case 'A': case 'a': { - if (m_current == SlaSupports) - { + if (m_current == SlaSupports) { gizmo_event(SLAGizmoEventType::AutomaticGeneration); // set as processed no matter what's returned by gizmo_event() to avoid the calling canvas to process 'A' as arrange processed = true; @@ -580,8 +580,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case 'F': case 'f': { - if (m_current == Scale) - { + if (m_current == Scale) { if (!is_dragging()) wxGetApp().plater()->scale_selection_to_fit_print_volume(); @@ -593,8 +592,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) } } - if (!processed && !evt.HasModifiers()) - { + if (!processed && !evt.HasModifiers()) { if (handle_shortcut(keyCode)) processed = true; } @@ -618,19 +616,20 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) const bool is_editing = m_current == Hollow ? true : gizmo->is_in_editing_mode(); const bool is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); - if (keyCode == WXK_SHIFT) - { + if (keyCode == WXK_SHIFT) { // shift has been just released - SLA gizmo might want to close rectangular selection. if (gizmo_event(SLAGizmoEventType::ShiftUp) || (is_editing && is_rectangle_dragging)) processed = true; } - else if (keyCode == WXK_ALT) - { + else if (keyCode == WXK_ALT) { // alt has been just released - SLA gizmo might want to close rectangular selection. if (gizmo_event(SLAGizmoEventType::AltUp) || (is_editing && is_rectangle_dragging)) processed = true; } } + else if (m_current == Measure && keyCode == WXK_CONTROL) { + gizmo_event(SLAGizmoEventType::CtrlUp, Vec2d::Zero(), false); + } // if (processed) // m_parent.set_cursor(GLCanvas3D::Standard); @@ -643,8 +642,7 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) // m_parent.set_cursor(GLCanvas3D::Cross); processed = true; } - else if (m_current == Cut) - { + else if (m_current == Cut) { auto do_move = [this, &processed](double delta_z) { GLGizmoCut3D* cut = dynamic_cast(get_current()); cut->shift_cut_z(delta_z); @@ -660,11 +658,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) } default: { break; } } - } else if (m_current == Simplify && keyCode == WXK_ESCAPE) { + } + else if (m_current == Simplify && keyCode == WXK_ESCAPE) { GLGizmoSimplify *simplify = dynamic_cast(get_current()); if (simplify != nullptr) processed = simplify->on_esc_key_down(); } + else if (m_current == Measure && keyCode == WXK_CONTROL) { + gizmo_event(SLAGizmoEventType::CtrlDown, Vec2d::Zero(), true); + } } if (processed) diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 95f090f22b..b7eb6da95b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -81,6 +81,7 @@ public: MmuSegmentation, Emboss, Simplify, + Measure, Undefined }; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index ad2f2712c9..729357c97a 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -59,6 +59,7 @@ static const std::map font_icons = { {ImGui::PreferencesHoverButton, "notification_preferences_hover"}, {ImGui::SliderFloatEditBtnIcon, "edit_button" }, {ImGui::SliderFloatEditBtnPressedIcon, "edit_button_pressed" }, + {ImGui::ClipboardBtnIcon , "copy_menu" }, {ImGui::ExpandBtn , "expand_btn" }, {ImGui::CollapseBtn , "collapse_btn" }, {ImGui::RevertButton , "undo" }, @@ -460,36 +461,6 @@ bool ImGuiWrapper::draw_radio_button(const std::string& name, float size, bool a return pressed; } -bool ImGuiWrapper::input_double(const std::string &label, const double &value, const std::string &format) -{ - return ImGui::InputDouble(label.c_str(), const_cast(&value), 0.0f, 0.0f, format.c_str(), ImGuiInputTextFlags_CharsDecimal); -} - -bool ImGuiWrapper::input_double(const wxString &label, const double &value, const std::string &format) -{ - auto label_utf8 = into_u8(label); - return input_double(label_utf8, value, format); -} - -bool ImGuiWrapper::input_vec3(const std::string &label, const Vec3d &value, float width, const std::string &format) -{ - bool value_changed = false; - - ImGui::BeginGroup(); - - for (int i = 0; i < 3; ++i) - { - std::string item_label = (i == 0) ? "X" : ((i == 1) ? "Y" : "Z"); - ImGui::PushID(i); - ImGui::PushItemWidth(width); - value_changed |= ImGui::InputDouble(item_label.c_str(), const_cast(&value(i)), 0.0f, 0.0f, format.c_str()); - ImGui::PopID(); - } - ImGui::EndGroup(); - - return value_changed; -} - bool ImGuiWrapper::checkbox(const wxString &label, bool &value) { auto label_utf8 = into_u8(label); @@ -548,20 +519,20 @@ void ImGuiWrapper::text_wrapped(const wxString &label, float wrap_width) void ImGuiWrapper::tooltip(const char *label, float wrap_width) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 8.0f, 8.0f }); ImGui::BeginTooltip(); ImGui::PushTextWrapPos(wrap_width); ImGui::TextUnformatted(label); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); + ImGui::PopStyleVar(3); } void ImGuiWrapper::tooltip(const wxString &label, float wrap_width) { - ImGui::BeginTooltip(); - ImGui::PushTextWrapPos(wrap_width); - ImGui::TextUnformatted(label.ToUTF8().data()); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); + tooltip(label.ToUTF8().data(), wrap_width); } ImVec2 ImGuiWrapper::get_slider_icon_size() const @@ -703,6 +674,29 @@ bool ImGuiWrapper::image_button(ImTextureID user_texture_id, const ImVec2& size, return image_button_ex(id, user_texture_id, size, uv0, uv1, padding, bg_col, tint_col, flags); } +bool ImGuiWrapper::image_button(const wchar_t icon, const wxString& tooltip) +{ + const ImGuiIO& io = ImGui::GetIO(); + const ImTextureID tex_id = io.Fonts->TexID; + assert(io.Fonts->TexWidth > 0 && io.Fonts->TexHeight > 0); + const float inv_tex_w = 1.0f / float(io.Fonts->TexWidth); + const float inv_tex_h = 1.0f / float(io.Fonts->TexHeight); + const ImFontAtlasCustomRect* const rect = GetTextureCustomRect(icon); + const ImVec2 size = { float(rect->Width), float(rect->Height) }; + const ImVec2 uv0 = ImVec2(float(rect->X) * inv_tex_w, float(rect->Y) * inv_tex_h); + const ImVec2 uv1 = ImVec2(float(rect->X + rect->Width) * inv_tex_w, float(rect->Y + rect->Height) * inv_tex_h); + ImGui::PushStyleColor(ImGuiCol_Button, { 0.25f, 0.25f, 0.25f, 0.0f }); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.4f, 0.4f, 0.4f, 1.0f }); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.25f, 0.25f, 0.25f, 1.0f }); + const bool res = image_button(tex_id, size, uv0, uv1); + ImGui::PopStyleColor(3); + + if (!tooltip.empty() && ImGui::IsItemHovered()) + this->tooltip(tooltip, ImGui::GetFontSize() * 20.0f); + + return res; +} + bool ImGuiWrapper::combo(const wxString& label, const std::vector& options, int& selection, ImGuiComboFlags flags) { // this is to force the label to the left of the widget: @@ -1165,6 +1159,11 @@ ImFontAtlasCustomRect* ImGuiWrapper::GetTextureCustomRect(const wchar_t& tex_id) return (item != m_custom_glyph_rects_ids.end()) ? ImGui::GetIO().Fonts->GetCustomRectByIndex(m_custom_glyph_rects_ids[tex_id]) : nullptr; } +void ImGuiWrapper::disable_background_fadeout_animation() +{ + GImGui->DimBgRatio = 1.0f; +} + ImU32 ImGuiWrapper::to_ImU32(const ColorRGBA& color) { return ImGui::GetColorU32({ color.r(), color.g(), color.b(), color.a() }); diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index 02536ed40a..3365954bab 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -96,9 +96,6 @@ public: bool button(const wxString& label, const ImVec2 &size, bool enable); // default size = ImVec2(0.f, 0.f) bool radio_button(const wxString &label, bool active); bool draw_radio_button(const std::string& name, float size, bool active, std::function draw_callback); - bool input_double(const std::string &label, const double &value, const std::string &format = "%.3f"); - bool input_double(const wxString &label, const double &value, const std::string &format = "%.3f"); - bool input_vec3(const std::string &label, const Vec3d &value, float width, const std::string &format = "%.3f"); bool checkbox(const wxString &label, bool &value); static void text(const char *label); static void text(const std::string &label); @@ -119,6 +116,7 @@ public: bool slider_float(const wxString& label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f, bool clamp = true, const wxString& tooltip = {}, bool show_edit_btn = true); bool image_button(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0.0, 0.0), const ImVec2& uv1 = ImVec2(1.0, 1.0), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0.0, 0.0, 0.0, 0.0), const ImVec4& tint_col = ImVec4(1.0, 1.0, 1.0, 1.0), ImGuiButtonFlags flags = 0); + bool image_button(const wchar_t icon, const wxString& tooltip = L""); // Use selection = -1 to not mark any option as selected bool combo(const wxString& label, const std::vector& options, int& selection, ImGuiComboFlags flags = 0); @@ -210,6 +208,8 @@ public: void set_requires_extra_frame() { m_requires_extra_frame = true; } void reset_requires_extra_frame() { m_requires_extra_frame = false; } + void disable_background_fadeout_animation(); + static ImU32 to_ImU32(const ColorRGBA& color); static ImVec4 to_ImVec4(const ColorRGBA& color); static ColorRGBA from_ImU32(const ImU32& color); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 96959f33b0..5764b283c6 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -538,7 +538,7 @@ void MainFrame::update_layout() case ESettingsLayout::GCodeViewer: { m_main_sizer->Add(m_plater, 1, wxEXPAND); - m_plater->set_bed_shape({ { 0.0, 0.0 }, { 200.0, 0.0 }, { 200.0, 200.0 }, { 0.0, 200.0 } }, 0.0, {}, {}, true); + m_plater->set_default_bed_shape(); m_plater->get_collapse_toolbar().set_enabled(false); m_plater->collapse_sidebar(true); m_plater->Show(); diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 5dea9f334b..cfc78f9f80 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -19,6 +19,14 @@ wxDEFINE_EVENT(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED, wxCommandEvent); BitmapCache* m_bitmap_cache = nullptr; +wxBitmapBundle* find_bndl(const std::string& bmp_name) +{ + if (!m_bitmap_cache) + m_bitmap_cache = new BitmapCache; + + return m_bitmap_cache->find_bndl(bmp_name); +} + // ***************************************************************************** // ---------------------------------------------------------------------------- // ObjectDataViewModelNode @@ -188,7 +196,7 @@ void ObjectDataViewModelNode::update_settings_digest_bitmaps() std::string scaled_bitmap_name = m_name.ToUTF8().data(); scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : ""); - wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); + wxBitmapBundle *bmp = find_bndl(scaled_bitmap_name); if (bmp == nullptr) { std::vector bmps; for (auto& category : m_opt_categories) @@ -323,8 +331,6 @@ static int get_root_idx(ObjectDataViewModelNode *parent_node, const ItemType roo ObjectDataViewModel::ObjectDataViewModel() { - m_bitmap_cache = new Slic3r::GUI::BitmapCache; - m_volume_bmps = MenuFactory::get_volume_bitmaps(); m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); @@ -362,7 +368,7 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) scaled_bitmap_name += std::to_string(vol_type); scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : "-lm"); - wxBitmapBundle* bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); + wxBitmapBundle* bmp = find_bndl(scaled_bitmap_name); if (!bmp) { std::vector bmps; if (node->has_warning_icon()) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index baacafe435..7abca7c272 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3034,6 +3034,7 @@ void Plater::priv::delete_all_objects_from_model() gcode_result.reset(); view3D->get_canvas3d()->reset_sequential_print_clearance(); + view3D->get_canvas3d()->reset_all_gizmos(); m_worker.cancel_all(); @@ -4180,19 +4181,14 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) combo->update(); } else if (select_preset) { - if (preset_type == Preset::TYPE_PRINTER) { - PhysicalPrinterCollection& physical_printers = wxGetApp().preset_bundle->physical_printers; - if(combo->is_selected_physical_printer()) - preset_name = physical_printers.get_selected_printer_preset_name(); - else - physical_printers.unselect_printer(); - } wxWindowUpdateLocker noUpdates(sidebar->presets_panel()); wxGetApp().get_tab(preset_type)->select_preset(preset_name); } - // update plater with new config - q->on_config_change(wxGetApp().preset_bundle->full_config()); + if (preset_type != Preset::TYPE_PRINTER || select_preset) { + // update plater with new config + q->on_config_change(wxGetApp().preset_bundle->full_config()); + } if (preset_type == Preset::TYPE_PRINTER) { /* Settings list can be changed after printer preset changing, so * update all settings items for all item had it. @@ -4948,9 +4944,13 @@ bool Plater::priv::can_increase_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; + // Disallow arrange and add instance when emboss gizmo is opend + // Prevent strobo effect during editing emboss parameters. if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss) return false; - return !sidebar->obj_list()->has_selected_cut_object(); + const int obj_idx = get_selected_object_idx(); + return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && + !sidebar->obj_list()->has_selected_cut_object(); } bool Plater::priv::can_decrease_instances() const @@ -4959,7 +4959,10 @@ bool Plater::priv::can_decrease_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; - return !sidebar->obj_list()->has_selected_cut_object(); + const int obj_idx = get_selected_object_idx(); + return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && + (model.objects[obj_idx]->instances.size() > 1) && + !sidebar->obj_list()->has_selected_cut_object(); } bool Plater::priv::can_split_to_objects() const @@ -5490,10 +5493,27 @@ void Plater::load_gcode(const wxString& filename) p->gcode_result = std::move(processor.extract_result()); // show results - p->preview->reload_print(false); + try + { + p->preview->reload_print(false); + } + catch (const std::exception&) + { + wxEndBusyCursor(); + p->gcode_result.reset(); + reset_gcode_toolpaths(); + set_default_bed_shape(); + p->preview->reload_print(false); + p->get_current_canvas3D()->render(); + MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."), + wxString(GCODEVIEWER_APP_NAME) + " - " + _L("Error while loading .gcode file"), wxOK | wxICON_WARNING | wxCENTRE).ShowModal(); + set_project_filename(wxEmptyString); + return; + } p->preview->get_canvas3d()->zoom_to_gcode(); if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) { + wxEndBusyCursor(); //wxMessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."), MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("does not contain valid gcode."), wxString(GCODEVIEWER_APP_NAME) + " - " + _L("Error while loading .gcode file"), wxOK | wxICON_WARNING | wxCENTRE).ShowModal(); @@ -6659,6 +6679,11 @@ void Plater::set_bed_shape(const Pointfs& shape, const double max_print_height, p->set_bed_shape(shape, max_print_height, custom_texture, custom_model, force_as_custom); } +void Plater::set_default_bed_shape() const +{ + set_bed_shape({ { 0.0, 0.0 }, { 200.0, 0.0 }, { 200.0, 200.0 }, { 0.0, 200.0 } }, 0.0, {}, {}, true); +} + void Plater::force_filament_colors_update() { bool update_scheduled = false; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 4f9e1ef53e..77a401f3f1 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -399,6 +399,7 @@ public: void set_bed_shape() const; void set_bed_shape(const Pointfs& shape, const double max_print_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false) const; + void set_default_bed_shape() const; NotificationManager * get_notification_manager(); const NotificationManager * get_notification_manager() const; diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 65469e6a03..200cd8a30c 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -515,12 +515,23 @@ bool PresetComboBox::is_selected_physical_printer() bool PresetComboBox::selection_is_changed_according_to_physical_printers() { - if (m_type != Preset::TYPE_PRINTER || !is_selected_physical_printer()) + if (m_type != Preset::TYPE_PRINTER) return false; - PhysicalPrinterCollection& physical_printers = m_preset_bundle->physical_printers; + const std::string selected_string = into_u8(this->GetString(this->GetSelection())); + PhysicalPrinterCollection& physical_printers = m_preset_bundle->physical_printers; + Tab* tab = wxGetApp().get_tab(Preset::TYPE_PRINTER); - std::string selected_string = this->GetString(this->GetSelection()).ToUTF8().data(); + if (!is_selected_physical_printer()) { + if (!physical_printers.has_selection()) + return false; + + const bool is_changed = selected_string == physical_printers.get_selected_printer_preset_name(); + physical_printers.unselect_printer(); + if (is_changed) + tab->select_preset(selected_string); + return is_changed; + } std::string old_printer_full_name, old_printer_preset; if (physical_printers.has_selection()) { @@ -535,20 +546,21 @@ bool PresetComboBox::selection_is_changed_according_to_physical_printers() // if new preset wasn't selected, there is no need to call update preset selection if (old_printer_preset == preset_name) { + tab->update_preset_choice(); + wxGetApp().plater()->show_action_buttons(false); + // we need just to update according Plater<->Tab PresetComboBox if (dynamic_cast(this)!=nullptr) { - wxGetApp().get_tab(m_type)->update_preset_choice(); // Synchronize config.ini with the current selections. m_preset_bundle->export_selections(*wxGetApp().app_config); + this->update(); } else if (dynamic_cast(this)!=nullptr) wxGetApp().sidebar().update_presets(m_type); - this->update(); return true; } - Tab* tab = wxGetApp().get_tab(Preset::TYPE_PRINTER); if (tab) tab->select_preset(preset_name, false, old_printer_full_name); return true; diff --git a/src/slic3r/GUI/SceneRaycaster.cpp b/src/slic3r/GUI/SceneRaycaster.cpp index 6d255ccdb4..96ae536ca7 100644 --- a/src/slic3r/GUI/SceneRaycaster.cpp +++ b/src/slic3r/GUI/SceneRaycaster.cpp @@ -37,10 +37,10 @@ std::shared_ptr SceneRaycaster::add_raycaster(EType type, in const Transform3d& trafo, bool use_back_faces) { switch (type) { - case EType::Bed: { return m_bed.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } + case EType::Bed: { return m_bed.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } case EType::Volume: { return m_volumes.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } - case EType::Gizmo: { return m_gizmos.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } - default: { assert(false); return nullptr; } + case EType::Gizmo: { return m_gizmos.emplace_back(std::make_shared(encode_id(type, id), raycaster, trafo, use_back_faces)); } + default: { assert(false); return nullptr; } }; } @@ -173,6 +173,31 @@ void SceneRaycaster::render_hit(const Camera& camera) shader->stop_using(); } + +size_t SceneRaycaster::active_beds_count() const { + size_t count = 0; + for (const auto& b : m_bed) { + if (b->is_active()) + ++count; + } + return count; +} +size_t SceneRaycaster::active_volumes_count() const { + size_t count = 0; + for (const auto& v : m_volumes) { + if (v->is_active()) + ++count; + } + return count; +} +size_t SceneRaycaster::active_gizmos_count() const { + size_t count = 0; + for (const auto& g : m_gizmos) { + if (g->is_active()) + ++count; + } + return count; +} #endif // ENABLE_RAYCAST_PICKING_DEBUG std::vector>* SceneRaycaster::get_raycasters(EType type) diff --git a/src/slic3r/GUI/SceneRaycaster.hpp b/src/slic3r/GUI/SceneRaycaster.hpp index 7541badbb4..2254a2022d 100644 --- a/src/slic3r/GUI/SceneRaycaster.hpp +++ b/src/slic3r/GUI/SceneRaycaster.hpp @@ -105,9 +105,10 @@ public: size_t active_gizmos_count() const; #endif // ENABLE_RAYCAST_PICKING_DEBUG + static int decode_id(EType type, int id); + private: static int encode_id(EType type, int id); - static int decode_id(EType type, int id); static int base_id(EType type); }; diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 52e58193c7..88d3e13763 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -594,8 +594,6 @@ void SearchDialog::ProcessSelection(wxDataViewItem selection) void SearchDialog::OnInputText(wxCommandEvent&) { - search_line->SetInsertionPointEnd(); - wxString input_string = search_line->GetValue(); if (input_string == default_string) input_string.Clear(); diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index 969bc48d9c..34b88f1606 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -330,6 +330,7 @@ public: #if ENABLE_WORLD_COORDINATE bool is_single_volume_or_modifier() const { return is_single_volume() || is_single_modifier(); } #endif // ENABLE_WORLD_COORDINATE + bool is_single_volume_instance() const { return is_single_full_instance() && m_list.size() == 1; } bool is_single_text() const; bool contains_volume(unsigned int volume_idx) const { return m_list.find(volume_idx) != m_list.end(); } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8f38a1525e..7e441342f8 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -215,26 +215,30 @@ void Tab::create_preset_tab() sizer->Add(m_hsizer, 0, wxEXPAND | wxBOTTOM, 3); m_hsizer->Add(m_presets_choice, 0, wxLEFT | wxRIGHT | wxTOP | wxALIGN_CENTER_VERTICAL, 3); m_hsizer->AddSpacer(int(4*scale_factor)); - m_hsizer->Add(m_btn_save_preset, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->AddSpacer(int(4*scale_factor)); - m_hsizer->Add(m_btn_rename_preset, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->AddSpacer(int(4 * scale_factor)); - m_hsizer->Add(m_btn_delete_preset, 0, wxALIGN_CENTER_VERTICAL); + + m_h_buttons_sizer = new wxBoxSizer(wxHORIZONTAL); + m_h_buttons_sizer->Add(m_btn_save_preset, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(4*scale_factor)); + m_h_buttons_sizer->Add(m_btn_rename_preset, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(4 * scale_factor)); + m_h_buttons_sizer->Add(m_btn_delete_preset, 0, wxALIGN_CENTER_VERTICAL); if (m_btn_edit_ph_printer) { - m_hsizer->AddSpacer(int(4 * scale_factor)); - m_hsizer->Add(m_btn_edit_ph_printer, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(4 * scale_factor)); + m_h_buttons_sizer->Add(m_btn_edit_ph_printer, 0, wxALIGN_CENTER_VERTICAL); } - m_hsizer->AddSpacer(int(/*16*/8 * scale_factor)); - m_hsizer->Add(m_btn_hide_incompatible_presets, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->AddSpacer(int(8 * scale_factor)); - m_hsizer->Add(m_question_btn, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->AddSpacer(int(32 * scale_factor)); - m_hsizer->Add(m_undo_to_sys_btn, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->Add(m_undo_btn, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->AddSpacer(int(32 * scale_factor)); - m_hsizer->Add(m_search_btn, 0, wxALIGN_CENTER_VERTICAL); - m_hsizer->AddSpacer(int(8*scale_factor)); - m_hsizer->Add(m_btn_compare_preset, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(/*16*/8 * scale_factor)); + m_h_buttons_sizer->Add(m_btn_hide_incompatible_presets, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(8 * scale_factor)); + m_h_buttons_sizer->Add(m_question_btn, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(32 * scale_factor)); + m_h_buttons_sizer->Add(m_undo_to_sys_btn, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->Add(m_undo_btn, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(32 * scale_factor)); + m_h_buttons_sizer->Add(m_search_btn, 0, wxALIGN_CENTER_VERTICAL); + m_h_buttons_sizer->AddSpacer(int(8*scale_factor)); + m_h_buttons_sizer->Add(m_btn_compare_preset, 0, wxALIGN_CENTER_VERTICAL); + + m_hsizer->Add(m_h_buttons_sizer, 1, wxEXPAND); m_hsizer->AddSpacer(int(16*scale_factor)); // m_hsizer->AddStretchSpacer(32); // StretchSpacer has a strange behavior under OSX, so @@ -3192,6 +3196,7 @@ void Tab::update_btns_enabling() if (m_btn_edit_ph_printer) m_btn_edit_ph_printer->SetToolTip( m_preset_bundle->physical_printers.has_selection() ? _L("Edit physical printer") : _L("Add physical printer")); + m_h_buttons_sizer->Layout(); } void Tab::update_preset_choice() diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 448e4be050..5adeb278a4 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -173,6 +173,7 @@ protected: ScalableButton* m_btn_edit_ph_printer {nullptr}; ScalableButton* m_btn_hide_incompatible_presets; wxBoxSizer* m_hsizer; + wxBoxSizer* m_h_buttons_sizer; wxBoxSizer* m_left_sizer; wxTreeCtrl* m_treectrl; diff --git a/src/slic3r/GUI/format.hpp b/src/slic3r/GUI/format.hpp index 51beafff13..15a3f04c38 100644 --- a/src/slic3r/GUI/format.hpp +++ b/src/slic3r/GUI/format.hpp @@ -7,12 +7,35 @@ // though C++20 format uses a different template pattern for position independent parameters. // This wrapper also manages implicit conversion from wxString to UTF8 and format_wxstr() variants are provided to format into wxString. -#include - #include -namespace Slic3r { -namespace GUI { +namespace Slic3r::internal::format { + // Wrapper around wxScopedCharBuffer to indicate that the content is UTF8 formatted. + struct utf8_buffer { + // wxScopedCharBuffer is reference counted, therefore copying by value is cheap. + wxScopedCharBuffer data; + }; + inline std::ostream& operator<<(std::ostream& os, const utf8_buffer &v) { + os << v.data.data(); + return os; + } + // Accept wxString and convert it to UTF8 to be processed by Slic3r::format(). + inline const utf8_buffer cook(const wxString& arg) { + return utf8_buffer{ arg.ToUTF8() }; + } + // Vojtech seemingly does not understand perfect forwarding: + // Why Slic3r::internal::format::cook(T&& arg) is taken for non-const wxString reference? + inline const utf8_buffer cook(wxString& arg) { + return utf8_buffer{ arg.ToUTF8() }; + } + inline const utf8_buffer cook(wxString&& arg) { + return utf8_buffer{ arg.ToUTF8() }; + } +} + +#include + +namespace Slic3r::GUI { // Format input mixing UTF8 encoded strings (const char*, std::string) and wxStrings, return a wxString. template @@ -42,42 +65,6 @@ inline std::string format(const wxString& fmt, TArgs&&... args) { return Slic3r::format(fmt.ToUTF8().data(), std::forward(args)...); } -} // namespace GUI - -namespace internal { - namespace format { - // Wrapper around wxScopedCharBuffer to indicate that the content is UTF8 formatted. - struct utf8_buffer { - // wxScopedCharBuffer is reference counted, therefore copying by value is cheap. - wxScopedCharBuffer data; - }; - // Accept wxString and convert it to UTF8 to be processed by Slic3r::format(). - inline const utf8_buffer cook(const wxString &arg) { - return utf8_buffer { arg.ToUTF8() }; - } - // Vojtech seemingly does not understand perfect forwarding: - // Why Slic3r::internal::format::cook(T&& arg) is taken for non-const wxString reference? - inline const utf8_buffer cook(wxString &arg) { - return utf8_buffer { arg.ToUTF8() }; - } - inline const utf8_buffer cook(wxString &&arg) { - return utf8_buffer{ arg.ToUTF8() }; - } - } -} - -} // namespace Slic3r - -namespace boost { - namespace io { - namespace detail { - // Adaptor for boost::format to accept wxString converted to UTF8. - inline std::ostream& operator<<(std::ostream& os, const Slic3r::internal::format::utf8_buffer& str) { - os << str.data.data(); - return os; - } - } - } -} +} // namespace Slic3r::GUI #endif /* slic3r_GUI_format_hpp_ */ diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index f3fe2aee5c..3abc4bd19d 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(${_TEST_NAME}_tests test_voronoi.cpp test_optimizers.cpp test_png_io.cpp + test_surface_mesh.cpp test_timeutils.cpp test_quadric_edge_collapse.cpp test_triangulation.cpp diff --git a/tests/libslic3r/test_surface_mesh.cpp b/tests/libslic3r/test_surface_mesh.cpp new file mode 100644 index 0000000000..34ff356679 --- /dev/null +++ b/tests/libslic3r/test_surface_mesh.cpp @@ -0,0 +1,122 @@ +#include +#include + + +#include + +using namespace Slic3r; + + +// Generate a broken cube mesh. Face 8 is inverted, face 11 is missing. +indexed_triangle_set its_make_cube_broken(double xd, double yd, double zd) +{ + auto x = float(xd), y = float(yd), z = float(zd); + return { + { {0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7}, + {0, 4, 7}, {0, 7, 1}, {1, 7, 6}, {1, 6, 2}, + {2, 5, 6}, {2, 5, 3}, {4, 0, 3} /*missing face*/ }, + { {x, y, 0}, {x, 0, 0}, {0, 0, 0}, {0, y, 0}, + {x, y, z}, {0, y, z}, {0, 0, z}, {x, 0, z} } + }; +} + + + +TEST_CASE("SurfaceMesh on a cube", "[SurfaceMesh]") { + indexed_triangle_set cube = its_make_cube(1., 1., 1.); + SurfaceMesh sm(cube); + const Halfedge_index hi_first = sm.halfedge(Face_index(0)); + Halfedge_index hi = hi_first; + + REQUIRE(! hi_first.is_invalid()); + + SECTION("next / prev halfedge") { + hi = sm.next(hi); + REQUIRE(hi != hi_first); + hi = sm.next(hi); + hi = sm.next(hi); + REQUIRE(hi == hi_first); + hi = sm.prev(hi); + REQUIRE(hi != hi_first); + hi = sm.prev(hi); + hi = sm.prev(hi); + REQUIRE(hi == hi_first); + } + + SECTION("next_around_target") { + // Check that we get to the same halfedge after applying next_around_target + // four times. + const Vertex_index target_vert = sm.target(hi_first); + for (int i=0; i<4;++i) { + hi = sm.next_around_target(hi); + REQUIRE((hi == hi_first) == (i == 3)); + REQUIRE(sm.is_same_vertex(sm.target(hi), target_vert)); + REQUIRE(! sm.is_border(hi)); + } + } + + SECTION("iterate around target and source") { + hi = sm.next_around_target(hi); + hi = sm.prev_around_target(hi); + hi = sm.prev_around_source(hi); + hi = sm.next_around_source(hi); + REQUIRE(hi == hi_first); + } + + SECTION("opposite") { + const Vertex_index target = sm.target(hi); + const Vertex_index source = sm.source(hi); + hi = sm.opposite(hi); + REQUIRE(sm.is_same_vertex(target, sm.source(hi))); + REQUIRE(sm.is_same_vertex(source, sm.target(hi))); + hi = sm.opposite(hi); + REQUIRE(hi == hi_first); + } + + SECTION("halfedges walk") { + for (int i=0; i<4; ++i) { + hi = sm.next(hi); + hi = sm.opposite(hi); + } + REQUIRE(hi == hi_first); + } + + SECTION("point accessor") { + Halfedge_index hi = sm.halfedge(Face_index(0)); + hi = sm.opposite(hi); + hi = sm.prev(hi); + hi = sm.opposite(hi); + REQUIRE(hi.face() == Face_index(6)); + REQUIRE(sm.point(sm.target(hi)).isApprox(cube.vertices[7])); + } +} + + + + +TEST_CASE("SurfaceMesh on a broken cube", "[SurfaceMesh]") { + indexed_triangle_set cube = its_make_cube_broken(1., 1., 1.); + SurfaceMesh sm(cube); + + SECTION("Check inverted face") { + Halfedge_index hi = sm.halfedge(Face_index(8)); + for (int i=0; i<3; ++i) { + REQUIRE(! hi.is_invalid()); + REQUIRE(sm.is_border(hi)); + } + REQUIRE(hi == sm.halfedge(Face_index(8))); + hi = sm.opposite(hi); + REQUIRE(hi.is_invalid()); + } + + SECTION("missing face") { + Halfedge_index hi = sm.halfedge(Face_index(0)); + for (int i=0; i<3; ++i) + hi = sm.next_around_source(hi); + hi = sm.next(hi); + REQUIRE(sm.is_border(hi)); + REQUIRE(! hi.is_invalid()); + hi = sm.opposite(hi); + REQUIRE(hi.is_invalid()); + } +}